defmodule Doctor.Reporters.Full do @moduledoc """ This reporter generates a full documentation coverage report and lists all the files in the project along with whether they pass or fail. """ @behaviour Doctor.Reporter alias Doctor.{Reporters.OutputUtils, ReportUtils} alias Elixir.IO.ANSI @doc_cov_width 9 @spec_cov_width 10 @module_width 41 @file_width 58 @functions_width 11 @missed_docs_width 9 @missed_specs_width 10 @module_doc_width 12 @struct_type_spec_width 11 @doc """ Generate a full Doctor report and print to STDOUT """ @impl true def generate_report(module_reports, args) do print_divider() print_header() Enum.each(module_reports, fn module_report -> doc_cov = massage_coverage(module_report.doc_coverage) spec_cov = massage_coverage(module_report.spec_coverage) module_doc = massage_module_doc(module_report) struct_type_spec = massage_struct_type_spec(module_report.has_struct_type_spec) output_line = OutputUtils.generate_table_line([ {doc_cov, @doc_cov_width}, {spec_cov, @spec_cov_width}, {module_report.module, @module_width}, {module_report.file, @file_width}, {module_report.functions, @functions_width}, {module_report.missed_docs, @missed_docs_width}, {module_report.missed_specs, @missed_specs_width}, {module_doc, @module_doc_width}, {struct_type_spec, @struct_type_spec_width, 0} ]) if ReportUtils.module_passed_validation?(module_report, args) do unless args.failed do Mix.shell().info(output_line) end else Mix.shell().info(ANSI.red() <> output_line <> ANSI.reset()) end end) overall_errors = ReportUtils.doctor_report_errors(module_reports, args) overall_passed = ReportUtils.count_total_passed_modules(module_reports, args) overall_failed = ReportUtils.count_total_failed_modules(module_reports, args) overall_doc_coverage = ReportUtils.calc_overall_doc_coverage(module_reports) overall_moduledoc_coverage = ReportUtils.calc_overall_moduledoc_coverage(module_reports) overall_spec_coverage = ReportUtils.calc_overall_spec_coverage(module_reports) print_footer( overall_errors, overall_passed, overall_failed, overall_doc_coverage, overall_moduledoc_coverage, overall_spec_coverage ) end defp print_header() do output_header = OutputUtils.generate_table_line([ {"Doc Cov", @doc_cov_width}, {"Spec Cov", @spec_cov_width}, {"Module", @module_width}, {"File", @file_width}, {"Functions", @functions_width}, {"No Docs", @missed_docs_width}, {"No Specs", @missed_specs_width}, {"Module Doc", @module_doc_width}, {"Struct Spec", @struct_type_spec_width, 0} ]) Mix.shell().info(output_header) end defp print_divider do "-" |> String.duplicate(171) |> Mix.shell().info() end defp print_footer(errors, passed, failed, doc_coverage, moduledoc_coverage, spec_coverage) do doc_coverage = Decimal.round(doc_coverage, 1) moduledoc_coverage = Decimal.round(moduledoc_coverage, 1) spec_coverage = Decimal.round(spec_coverage, 1) print_divider() Mix.shell().info("Summary:\n") Mix.shell().info("Passed Modules: #{passed}") Mix.shell().info("Failed Modules: #{failed}") Mix.shell().info("Total Doc Coverage: #{doc_coverage}%") Mix.shell().info("Total Moduledoc Coverage: #{moduledoc_coverage}%") Mix.shell().info("Total Spec Coverage: #{spec_coverage}%\n") msg = case errors do [] -> "Doctor validation has passed!" [err] -> """ #{ANSI.red()}Doctor validation has failed because #{err}.#{ANSI.reset()} """ errs -> """ #{ANSI.red()}Doctor validation has failed because: * #{Enum.map_join(errs, ".\n * ", &String.capitalize(&1))}.\ #{ANSI.reset()} """ end Mix.shell().info(msg) end defp massage_coverage(coverage) do if coverage do "#{Decimal.round(coverage)}%" else "N/A" end end defp massage_module_doc(%{is_protocol_implementation: true}), do: "N/A" defp massage_module_doc(%{has_module_doc: true}), do: "Yes" defp massage_module_doc(%{has_module_doc: false}), do: "No" defp massage_struct_type_spec(:not_struct), do: "N/A" defp massage_struct_type_spec(true), do: "Yes" defp massage_struct_type_spec(false), do: "No" end