cov-loupe 3.0.0 → 4.0.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +230 -0
  3. data/CLAUDE.md +5 -0
  4. data/CODE_OF_CONDUCT.md +62 -0
  5. data/CONTRIBUTING.md +102 -0
  6. data/GEMINI.md +5 -0
  7. data/README.md +154 -51
  8. data/RELEASE_NOTES.md +452 -0
  9. data/dev/images/cov-loupe-icon-lores.png +0 -0
  10. data/dev/images/cov-loupe-icon-square.png +0 -0
  11. data/dev/images/cov-loupe-icon.png +0 -0
  12. data/dev/images/cov-loupe-logo.png +0 -0
  13. data/dev/prompts/README.md +74 -0
  14. data/dev/prompts/archive/architectural-review-and-actions-prompt.md +53 -0
  15. data/dev/prompts/archive/investigate-and-report-issues-prompt.md +33 -0
  16. data/dev/prompts/archive/produce-action-items-prompt.md +25 -0
  17. data/dev/prompts/guidelines/ai-code-evaluator-guidelines.md +337 -0
  18. data/dev/prompts/improve/refactor-test-suite.md +18 -0
  19. data/dev/prompts/improve/simplify-code-logic.md +133 -0
  20. data/dev/prompts/improve/update-documentation.md +21 -0
  21. data/dev/prompts/review/comprehensive-codebase-review.md +176 -0
  22. data/dev/prompts/review/identify-action-items.md +143 -0
  23. data/dev/prompts/review/verify-code-changes.md +54 -0
  24. data/dev/prompts/validate/create-screencast-outline.md +234 -0
  25. data/dev/prompts/validate/test-documentation-examples.md +180 -0
  26. data/docs/QUICKSTART.md +63 -0
  27. data/docs/assets/images/cov-loupe-logo-lores.png +0 -0
  28. data/docs/assets/images/cov-loupe-logo.png +0 -0
  29. data/docs/assets/images/favicon.png +0 -0
  30. data/docs/assets/stylesheets/branding.css +16 -0
  31. data/docs/assets/stylesheets/extra.css +15 -0
  32. data/docs/code_of_conduct.md +1 -0
  33. data/docs/contributing.md +1 -0
  34. data/docs/dev/ARCHITECTURE.md +56 -11
  35. data/docs/dev/DEVELOPMENT.md +116 -12
  36. data/docs/dev/FUTURE_ENHANCEMENTS.md +14 -0
  37. data/docs/dev/README.md +3 -2
  38. data/docs/dev/RELEASING.md +2 -0
  39. data/docs/dev/arch-decisions/README.md +10 -7
  40. data/docs/dev/arch-decisions/application-architecture.md +259 -0
  41. data/docs/dev/arch-decisions/coverage-data-quality.md +193 -0
  42. data/docs/dev/arch-decisions/output-character-mode.md +217 -0
  43. data/docs/dev/arch-decisions/path-resolution.md +90 -0
  44. data/docs/dev/arch-decisions/{004-x-arch-decision.md → policy-validation.md} +32 -28
  45. data/docs/dev/arch-decisions/{005-x-arch-decision.md → simplecov-integration.md} +47 -44
  46. data/docs/dev/presentations/cov-loupe-presentation.md +15 -13
  47. data/docs/examples/mcp-inputs.md +3 -0
  48. data/docs/examples/prompts.md +3 -0
  49. data/docs/examples/success_predicates.md +3 -0
  50. data/docs/fixtures/demo_project/.resultset.json +170 -0
  51. data/docs/fixtures/demo_project/README.md +6 -0
  52. data/docs/fixtures/demo_project/app/controllers/admin/audit_logs_controller.rb +19 -0
  53. data/docs/fixtures/demo_project/app/controllers/orders_controller.rb +26 -0
  54. data/docs/fixtures/demo_project/app/models/order.rb +20 -0
  55. data/docs/fixtures/demo_project/app/models/user.rb +19 -0
  56. data/docs/fixtures/demo_project/lib/api/client.rb +22 -0
  57. data/docs/fixtures/demo_project/lib/ops/jobs/cleanup_job.rb +16 -0
  58. data/docs/fixtures/demo_project/lib/ops/jobs/report_job.rb +17 -0
  59. data/docs/fixtures/demo_project/lib/payments/processor.rb +15 -0
  60. data/docs/fixtures/demo_project/lib/payments/refund_service.rb +15 -0
  61. data/docs/fixtures/demo_project/lib/payments/reporting/exporter.rb +16 -0
  62. data/docs/index.md +1 -0
  63. data/docs/license.md +3 -0
  64. data/docs/release_notes.md +3 -0
  65. data/docs/user/ADVANCED_USAGE.md +208 -115
  66. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +2 -0
  67. data/docs/user/CLI_USAGE.md +276 -101
  68. data/docs/user/ERROR_HANDLING.md +4 -4
  69. data/docs/user/EXAMPLES.md +121 -128
  70. data/docs/user/INSTALLATION.md +9 -28
  71. data/docs/user/LIBRARY_API.md +227 -122
  72. data/docs/user/MCP_INTEGRATION.md +114 -203
  73. data/docs/user/README.md +5 -1
  74. data/docs/user/TROUBLESHOOTING.md +49 -27
  75. data/docs/user/installing-a-prelease-version-of-covloupe.md +43 -0
  76. data/docs/user/{V2-BREAKING-CHANGES.md → migrations/MIGRATING_TO_V2.md} +62 -72
  77. data/docs/user/migrations/MIGRATING_TO_V3.md +72 -0
  78. data/docs/user/migrations/MIGRATING_TO_V4.md +591 -0
  79. data/docs/user/migrations/README.md +22 -0
  80. data/docs/user/prompts/README.md +9 -0
  81. data/docs/user/prompts/non-web-coverage-analysis-prompt.md +103 -0
  82. data/docs/user/prompts/rails-coverage-analysis-prompt.md +94 -0
  83. data/docs/user/prompts/use-cli-not-mcp-prompt.md +53 -0
  84. data/examples/cli_demo.sh +77 -0
  85. data/examples/filter_and_table_demo-output.md +114 -0
  86. data/examples/filter_and_table_demo.rb +174 -0
  87. data/examples/fixtures/demo_project/coverage/.resultset.json +10 -0
  88. data/examples/mcp-inputs/README.md +66 -0
  89. data/examples/mcp-inputs/coverage_detailed.json +1 -0
  90. data/examples/mcp-inputs/coverage_raw.json +1 -0
  91. data/examples/mcp-inputs/coverage_summary.json +1 -0
  92. data/examples/mcp-inputs/list.json +1 -0
  93. data/examples/mcp-inputs/uncovered_lines.json +1 -0
  94. data/examples/prompts/README.md +27 -0
  95. data/examples/prompts/custom_resultset.txt +2 -0
  96. data/examples/prompts/detailed_with_source.txt +2 -0
  97. data/examples/prompts/list_lowest.txt +2 -0
  98. data/examples/prompts/summary.txt +2 -0
  99. data/examples/prompts/uncovered.txt +2 -0
  100. data/examples/success_predicates/README.md +198 -0
  101. data/examples/success_predicates/all_files_above_threshold_predicate.rb +21 -0
  102. data/examples/success_predicates/directory_specific_thresholds_predicate.rb +30 -0
  103. data/examples/success_predicates/project_coverage_minimum_predicate.rb +6 -0
  104. data/lib/cov_loupe/base_tool.rb +229 -20
  105. data/lib/cov_loupe/cli.rb +132 -23
  106. data/lib/cov_loupe/commands/base_command.rb +25 -6
  107. data/lib/cov_loupe/commands/command_factory.rb +0 -1
  108. data/lib/cov_loupe/commands/detailed_command.rb +10 -5
  109. data/lib/cov_loupe/commands/list_command.rb +2 -1
  110. data/lib/cov_loupe/commands/raw_command.rb +7 -5
  111. data/lib/cov_loupe/commands/summary_command.rb +12 -7
  112. data/lib/cov_loupe/commands/totals_command.rb +74 -10
  113. data/lib/cov_loupe/commands/uncovered_command.rb +7 -5
  114. data/lib/cov_loupe/commands/validate_command.rb +11 -3
  115. data/lib/cov_loupe/commands/version_command.rb +6 -4
  116. data/lib/cov_loupe/{app_config.rb → config/app_config.rb} +13 -5
  117. data/lib/cov_loupe/config/app_context.rb +43 -0
  118. data/lib/cov_loupe/config/boolean_type.rb +91 -0
  119. data/lib/cov_loupe/config/logger.rb +92 -0
  120. data/lib/cov_loupe/{option_normalizers.rb → config/option_normalizers.rb} +55 -24
  121. data/lib/cov_loupe/{option_parser_builder.rb → config/option_parser_builder.rb} +46 -24
  122. data/lib/cov_loupe/coverage/coverage_calculator.rb +53 -0
  123. data/lib/cov_loupe/coverage/coverage_reporter.rb +63 -0
  124. data/lib/cov_loupe/coverage/coverage_table_formatter.rb +133 -0
  125. data/lib/cov_loupe/{error_handler.rb → errors/error_handler.rb} +21 -33
  126. data/lib/cov_loupe/{errors.rb → errors/errors.rb} +48 -71
  127. data/lib/cov_loupe/formatters/formatters.rb +75 -0
  128. data/lib/cov_loupe/formatters/source_formatter.rb +18 -7
  129. data/lib/cov_loupe/formatters/table_formatter.rb +80 -0
  130. data/lib/cov_loupe/loaders/all.rb +15 -0
  131. data/lib/cov_loupe/loaders/all_cli.rb +10 -0
  132. data/lib/cov_loupe/loaders/all_mcp.rb +23 -0
  133. data/lib/cov_loupe/loaders/resultset_loader.rb +147 -0
  134. data/lib/cov_loupe/mcp_server.rb +3 -2
  135. data/lib/cov_loupe/model/model.rb +520 -0
  136. data/lib/cov_loupe/model/model_data.rb +13 -0
  137. data/lib/cov_loupe/model/model_data_cache.rb +116 -0
  138. data/lib/cov_loupe/option_parsers/env_options_parser.rb +17 -6
  139. data/lib/cov_loupe/option_parsers/error_helper.rb +16 -10
  140. data/lib/cov_loupe/output_chars.rb +192 -0
  141. data/lib/cov_loupe/paths/glob_utils.rb +100 -0
  142. data/lib/cov_loupe/{path_relativizer.rb → paths/path_relativizer.rb} +5 -13
  143. data/lib/cov_loupe/paths/path_utils.rb +265 -0
  144. data/lib/cov_loupe/paths/volume_case_sensitivity.rb +173 -0
  145. data/lib/cov_loupe/presenters/base_coverage_presenter.rb +9 -13
  146. data/lib/cov_loupe/presenters/coverage_payload_presenter.rb +21 -0
  147. data/lib/cov_loupe/presenters/payload_caching.rb +23 -0
  148. data/lib/cov_loupe/presenters/project_coverage_presenter.rb +73 -21
  149. data/lib/cov_loupe/presenters/project_totals_presenter.rb +16 -10
  150. data/lib/cov_loupe/repositories/coverage_repository.rb +149 -0
  151. data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +90 -76
  152. data/lib/cov_loupe/resolvers/{resolver_factory.rb → resolver_helpers.rb} +6 -5
  153. data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +40 -12
  154. data/lib/cov_loupe/scripts/command_execution.rb +113 -0
  155. data/lib/cov_loupe/scripts/latest_ci_status.rb +97 -0
  156. data/lib/cov_loupe/scripts/pre_release_check.rb +164 -0
  157. data/lib/cov_loupe/scripts/setup_doc_server.rb +23 -0
  158. data/lib/cov_loupe/scripts/start_doc_server.rb +24 -0
  159. data/lib/cov_loupe/staleness/stale_status.rb +23 -0
  160. data/lib/cov_loupe/staleness/staleness_checker.rb +328 -0
  161. data/lib/cov_loupe/staleness/staleness_message_formatter.rb +91 -0
  162. data/lib/cov_loupe/tools/coverage_detailed_tool.rb +14 -15
  163. data/lib/cov_loupe/tools/coverage_raw_tool.rb +14 -14
  164. data/lib/cov_loupe/tools/coverage_summary_tool.rb +16 -16
  165. data/lib/cov_loupe/tools/coverage_table_tool.rb +139 -21
  166. data/lib/cov_loupe/tools/coverage_totals_tool.rb +31 -13
  167. data/lib/cov_loupe/tools/help_tool.rb +16 -20
  168. data/lib/cov_loupe/tools/list_tool.rb +65 -0
  169. data/lib/cov_loupe/tools/uncovered_lines_tool.rb +14 -14
  170. data/lib/cov_loupe/tools/validate_tool.rb +18 -24
  171. data/lib/cov_loupe/tools/version_tool.rb +8 -3
  172. data/lib/cov_loupe/version.rb +1 -1
  173. data/lib/cov_loupe.rb +83 -55
  174. metadata +184 -154
  175. data/docs/dev/BRANCH_ONLY_COVERAGE.md +0 -158
  176. data/docs/dev/arch-decisions/001-x-arch-decision.md +0 -95
  177. data/docs/dev/arch-decisions/002-x-arch-decision.md +0 -159
  178. data/docs/dev/arch-decisions/003-x-arch-decision.md +0 -165
  179. data/lib/cov_loupe/app_context.rb +0 -26
  180. data/lib/cov_loupe/constants.rb +0 -22
  181. data/lib/cov_loupe/coverage_reporter.rb +0 -31
  182. data/lib/cov_loupe/formatters.rb +0 -51
  183. data/lib/cov_loupe/mode_detector.rb +0 -56
  184. data/lib/cov_loupe/model.rb +0 -339
  185. data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +0 -14
  186. data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +0 -14
  187. data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +0 -14
  188. data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +0 -14
  189. data/lib/cov_loupe/resultset_loader.rb +0 -131
  190. data/lib/cov_loupe/staleness_checker.rb +0 -247
  191. data/lib/cov_loupe/table_formatter.rb +0 -64
  192. data/lib/cov_loupe/tools/all_files_coverage_tool.rb +0 -51
  193. data/lib/cov_loupe/util.rb +0 -88
  194. data/spec/MCP_INTEGRATION_TESTS_README.md +0 -111
  195. data/spec/TIMESTAMPS.md +0 -48
  196. data/spec/all_files_coverage_tool_spec.rb +0 -53
  197. data/spec/app_config_spec.rb +0 -142
  198. data/spec/base_tool_spec.rb +0 -62
  199. data/spec/cli/show_default_report_spec.rb +0 -33
  200. data/spec/cli_enumerated_options_spec.rb +0 -90
  201. data/spec/cli_error_spec.rb +0 -184
  202. data/spec/cli_format_spec.rb +0 -123
  203. data/spec/cli_json_options_spec.rb +0 -50
  204. data/spec/cli_source_spec.rb +0 -44
  205. data/spec/cli_spec.rb +0 -192
  206. data/spec/cli_table_spec.rb +0 -28
  207. data/spec/cli_usage_spec.rb +0 -42
  208. data/spec/commands/base_command_spec.rb +0 -107
  209. data/spec/commands/command_factory_spec.rb +0 -76
  210. data/spec/commands/detailed_command_spec.rb +0 -34
  211. data/spec/commands/list_command_spec.rb +0 -28
  212. data/spec/commands/raw_command_spec.rb +0 -69
  213. data/spec/commands/summary_command_spec.rb +0 -34
  214. data/spec/commands/totals_command_spec.rb +0 -34
  215. data/spec/commands/uncovered_command_spec.rb +0 -55
  216. data/spec/commands/validate_command_spec.rb +0 -213
  217. data/spec/commands/version_command_spec.rb +0 -38
  218. data/spec/constants_spec.rb +0 -61
  219. data/spec/cov_loupe/formatters/source_formatter_spec.rb +0 -267
  220. data/spec/cov_loupe/formatters_spec.rb +0 -76
  221. data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +0 -79
  222. data/spec/cov_loupe_model_spec.rb +0 -454
  223. data/spec/cov_loupe_module_spec.rb +0 -37
  224. data/spec/cov_loupe_opts_spec.rb +0 -185
  225. data/spec/coverage_reporter_spec.rb +0 -102
  226. data/spec/coverage_table_tool_spec.rb +0 -59
  227. data/spec/coverage_totals_tool_spec.rb +0 -37
  228. data/spec/error_handler_spec.rb +0 -197
  229. data/spec/error_mode_spec.rb +0 -139
  230. data/spec/errors_edge_cases_spec.rb +0 -312
  231. data/spec/errors_stale_spec.rb +0 -83
  232. data/spec/file_based_mcp_tools_spec.rb +0 -99
  233. data/spec/help_tool_spec.rb +0 -26
  234. data/spec/integration_spec.rb +0 -789
  235. data/spec/logging_fallback_spec.rb +0 -128
  236. data/spec/mcp_logging_spec.rb +0 -44
  237. data/spec/mcp_server_integration_spec.rb +0 -23
  238. data/spec/mcp_server_spec.rb +0 -106
  239. data/spec/mode_detector_spec.rb +0 -153
  240. data/spec/model_error_handling_spec.rb +0 -269
  241. data/spec/model_staleness_spec.rb +0 -79
  242. data/spec/option_normalizers_spec.rb +0 -203
  243. data/spec/option_parsers/env_options_parser_spec.rb +0 -221
  244. data/spec/option_parsers/error_helper_spec.rb +0 -222
  245. data/spec/path_relativizer_spec.rb +0 -98
  246. data/spec/presenters/coverage_detailed_presenter_spec.rb +0 -19
  247. data/spec/presenters/coverage_raw_presenter_spec.rb +0 -15
  248. data/spec/presenters/coverage_summary_presenter_spec.rb +0 -15
  249. data/spec/presenters/coverage_uncovered_presenter_spec.rb +0 -16
  250. data/spec/presenters/project_coverage_presenter_spec.rb +0 -87
  251. data/spec/presenters/project_totals_presenter_spec.rb +0 -144
  252. data/spec/resolvers/coverage_line_resolver_spec.rb +0 -282
  253. data/spec/resolvers/resolver_factory_spec.rb +0 -61
  254. data/spec/resolvers/resultset_path_resolver_spec.rb +0 -60
  255. data/spec/resultset_loader_spec.rb +0 -167
  256. data/spec/shared_examples/README.md +0 -115
  257. data/spec/shared_examples/coverage_presenter_examples.rb +0 -66
  258. data/spec/shared_examples/file_based_mcp_tools.rb +0 -179
  259. data/spec/shared_examples/formatted_command_examples.rb +0 -64
  260. data/spec/shared_examples/mcp_tool_text_json_response.rb +0 -16
  261. data/spec/spec_helper.rb +0 -127
  262. data/spec/staleness_checker_spec.rb +0 -374
  263. data/spec/staleness_more_spec.rb +0 -42
  264. data/spec/support/cli_helpers.rb +0 -22
  265. data/spec/support/control_flow_helpers.rb +0 -20
  266. data/spec/support/fake_mcp.rb +0 -40
  267. data/spec/support/io_helpers.rb +0 -29
  268. data/spec/support/mcp_helpers.rb +0 -35
  269. data/spec/support/mcp_runner.rb +0 -66
  270. data/spec/support/mocking_helpers.rb +0 -30
  271. data/spec/table_format_spec.rb +0 -70
  272. data/spec/tools/validate_tool_spec.rb +0 -132
  273. data/spec/tools_error_handling_spec.rb +0 -130
  274. data/spec/util_spec.rb +0 -154
  275. data/spec/version_spec.rb +0 -123
  276. data/spec/version_tool_spec.rb +0 -141
  277. /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/bar.rb +0 -0
  278. /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/foo.rb +0 -0
  279. /data/lib/cov_loupe/{config_parser.rb → config/config_parser.rb} +0 -0
  280. /data/lib/cov_loupe/{predicate_evaluator.rb → config/predicate_evaluator.rb} +0 -0
  281. /data/lib/cov_loupe/{error_handler_factory.rb → errors/error_handler_factory.rb} +0 -0
@@ -1,76 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::Formatters do
6
- describe '.formatter_for' do
7
- it 'returns a lambda for known format' do
8
- expect(described_class.formatter_for(:json)).to respond_to(:call)
9
- end
10
-
11
- it 'raises ArgumentError for unknown format' do
12
- expect { described_class.formatter_for(:unknown) }
13
- .to raise_error(ArgumentError, /Unknown format: unknown/)
14
- end
15
- end
16
-
17
- describe '.ensure_requirements_for' do
18
- it 'requires the library if needed' do
19
- # We rely on the fact that 'yaml' is in FORMAT_REQUIRES
20
- expect(described_class).to receive(:require).with('yaml')
21
- described_class.ensure_requirements_for(:yaml)
22
- end
23
-
24
- it 'does nothing if no requirement' do
25
- expect(described_class).not_to receive(:require)
26
- described_class.ensure_requirements_for(:json) # JSON already required by app
27
- end
28
- end
29
-
30
- describe '.format' do
31
- let(:obj) { { 'foo' => 'bar' } }
32
-
33
- [
34
- [:json, '{"foo":"bar"}', :eq],
35
- [:pretty_json, "{\n \"foo\": \"bar\"\n}", :include],
36
- [:table, { 'foo' => 'bar' }, :eq],
37
- [:yaml, "---\nfoo: bar\n", :include]
38
- ].each do |format, expected, matcher|
39
- it "formats as #{format}" do
40
- result = described_class.format(obj, format)
41
- expect(result).to send(matcher, expected)
42
- end
43
- end
44
-
45
- context 'when a required gem is missing' do
46
- before do
47
- error = LoadError.new('cannot load such file -- awesome_print')
48
- allow(described_class).to receive(:require).with('awesome_print').and_raise(error)
49
- end
50
-
51
- it 'raises a helpful LoadError' do
52
- expect { described_class.format(obj, :awesome_print) }
53
- .to raise_error(LoadError, /requires the 'awesome_print' gem/)
54
- end
55
- end
56
-
57
- context 'when awesome_print is available' do
58
- before do
59
- # Stub require on the module for ensure_requirements_for
60
- allow(described_class).to receive(:require).with('awesome_print')
61
-
62
- # Stub global require for the lambda's internal require
63
- allow(Kernel).to receive(:require).and_call_original
64
- allow(Kernel).to receive(:require).with('awesome_print').and_return(true)
65
-
66
- # Mock .ai on the object
67
- allow(obj).to receive(:ai).and_return('awesome output')
68
- end
69
-
70
- it 'formats using awesome_print' do
71
- result = described_class.format(obj, :awesome_print)
72
- expect(result).to eq('awesome output')
73
- end
74
- end
75
- end
76
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::Presenters::BaseCoveragePresenter do
6
- let(:model) { instance_double(CovLoupe::CoverageModel) }
7
- let(:path) { 'lib/foo.rb' }
8
- let(:presenter) { described_class.new(model: model, path: path) }
9
-
10
- describe '#initialize' do
11
- it 'sets model and path' do
12
- expect(presenter.model).to eq(model)
13
- expect(presenter.path).to eq(path)
14
- end
15
- end
16
-
17
- describe '#absolute_payload' do
18
- it 'raises NotImplementedError because build_payload is abstract' do
19
- expect { presenter.absolute_payload }.to raise_error(NotImplementedError)
20
- end
21
- end
22
-
23
- context 'with a concrete implementation' do
24
- let(:concrete_class) do
25
- Class.new(described_class) do
26
- # Provide a concrete implementation of the abstract build_payload method
27
- # for testing the BaseCoveragePresenter functionality
28
- def build_payload
29
- { 'file' => path, 'data' => 'test' }
30
- end
31
- end
32
- end
33
- let(:presenter) { concrete_class.new(model: model, path: path) }
34
- let(:payload_with_stale) { { 'file' => path, 'data' => 'test', 'stale' => false } }
35
-
36
- before do
37
- allow(model).to receive(:staleness_for).with(path).and_return(false)
38
- allow(model).to receive(:relativize).with(payload_with_stale).and_return(payload_with_stale)
39
- end
40
-
41
- describe '#absolute_payload' do
42
- it 'merges stale status into payload' do
43
- expect(presenter.absolute_payload).to include('stale' => false)
44
- expect(presenter.absolute_payload).to include('data' => 'test')
45
- end
46
-
47
- it 'caches the result' do
48
- r1 = presenter.absolute_payload
49
- r2 = presenter.absolute_payload
50
- expect(r1).to equal(r2)
51
- end
52
- end
53
-
54
- describe '#relativized_payload' do
55
- it 'delegates to model.relativize' do
56
- expect(model).to receive(:relativize).with(presenter.absolute_payload)
57
- presenter.relativized_payload
58
- end
59
-
60
- it 'caches the result' do
61
- presenter.relativized_payload
62
- expect(model).to have_received(:relativize).once
63
- presenter.relativized_payload
64
- end
65
- end
66
-
67
- describe '#stale' do
68
- it 'delegates to absolute_payload' do
69
- expect(presenter.stale).to be(false)
70
- end
71
- end
72
-
73
- describe '#relative_path' do
74
- it 'delegates to relativized_payload' do
75
- expect(presenter.relative_path).to eq('lib/foo.rb')
76
- end
77
- end
78
- end
79
- end
@@ -1,454 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::CoverageModel do
6
- subject(:model) { described_class.new(root: root) }
7
-
8
- let(:root) { (FIXTURES_DIR / 'project1').to_s }
9
-
10
-
11
- describe 'initialization error handling' do
12
- it 'raises FileError when File.read raises Errno::ENOENT directly' do
13
- # Stub find_resultset to return a path, but File.read to raise ENOENT
14
- allow(CovLoupe::CovUtil).to receive(:find_resultset)
15
- .and_return('/some/path/.resultset.json')
16
- allow(JSON).to receive(:load_file).with('/some/path/.resultset.json')
17
- .and_raise(Errno::ENOENT, 'No such file')
18
-
19
- expect do
20
- described_class.new(root: root, resultset: '/some/path/.resultset.json')
21
- end.to raise_error(CovLoupe::FileError, /Coverage data not found/)
22
- end
23
-
24
- it 'raises ResultsetNotFoundError when resultset file does not exist' do
25
- expect do
26
- described_class.new(root: root, resultset: '/nonexistent/path/.resultset.json')
27
- end.to raise_error(CovLoupe::ResultsetNotFoundError, /Specified resultset not found/)
28
- end
29
- end
30
-
31
- describe 'raw_for' do
32
- it 'returns absolute file and lines array' do
33
- data = model.raw_for('lib/foo.rb')
34
- expect(data['file']).to eq(File.expand_path('lib/foo.rb', root))
35
- expect(data['lines']).to eq([1, 0, nil, 2])
36
- end
37
- end
38
-
39
- describe 'summary_for' do
40
- it 'computes covered/total/percentage' do
41
- data = model.summary_for('lib/foo.rb')
42
- expect(data['summary']['total']).to eq(3)
43
- expect(data['summary']['covered']).to eq(2)
44
- expect(data['summary']['percentage']).to be_within(0.01).of(66.67)
45
- end
46
- end
47
-
48
- describe '#relativize' do
49
- it 'returns a copy with file paths relative to the root' do
50
- data = model.summary_for('lib/foo.rb')
51
- relative = model.relativize(data)
52
-
53
- expect(relative['file']).to eq('lib/foo.rb')
54
- expect(data['file']).not_to eq(relative['file'])
55
- expect(relative).not_to equal(data)
56
- end
57
- end
58
-
59
- describe 'uncovered_for' do
60
- it 'lists uncovered executable line numbers' do
61
- data = model.uncovered_for('lib/foo.rb')
62
- expect(data['uncovered']).to eq([2])
63
- expect(data['summary']['total']).to eq(3)
64
- end
65
- end
66
-
67
- describe 'detailed_for' do
68
- it 'returns per-line details for non-nil lines' do
69
- data = model.detailed_for('lib/foo.rb')
70
- expect(data['lines']).to eq([
71
- { 'line' => 1, 'hits' => 1, 'covered' => true },
72
- { 'line' => 2, 'hits' => 0, 'covered' => false },
73
- { 'line' => 4, 'hits' => 2, 'covered' => true }
74
- ])
75
- end
76
- end
77
-
78
- describe 'staleness_for' do
79
- it 'returns the staleness character for a file' do
80
- checker = instance_double(CovLoupe::StalenessChecker, off?: false)
81
- allow(CovLoupe::StalenessChecker).to receive(:new).and_return(checker)
82
- allow(checker).to receive(:stale_for_file?) do |file_abs, _|
83
- if file_abs == File.expand_path('lib/foo.rb', root)
84
- 'T'
85
- else
86
- false
87
- end
88
- end
89
-
90
- expect(model.staleness_for('lib/foo.rb')).to eq('T')
91
- expect(model.staleness_for('lib/bar.rb')).to be(false)
92
- end
93
-
94
- it 'returns false when an exception occurs during staleness check' do
95
- # Stub the checker to raise an error
96
- checker = instance_double(CovLoupe::StalenessChecker, off?: false)
97
- allow(CovLoupe::StalenessChecker).to receive(:new).and_return(checker)
98
- allow(checker).to receive(:stale_for_file?)
99
- .and_raise(StandardError, 'Something went wrong')
100
-
101
- # The rescue clause should catch the error and return false
102
- expect(model.staleness_for('lib/foo.rb')).to be(false)
103
- end
104
-
105
- it 'returns false when coverage data is not found for the file' do
106
- # Try to get staleness for a file that doesn't exist in coverage
107
- expect(model.staleness_for('lib/nonexistent.rb')).to be(false)
108
- end
109
- end
110
-
111
- describe 'all_files' do
112
- it 'sorts descending (default) by percentage then by file path' do
113
- files = model.all_files
114
- # lib/foo.rb has 66.67%, lib/bar.rb has 33.33%
115
- expect(files.first['file']).to eq(File.expand_path('lib/foo.rb', root))
116
- expect(files.first['percentage']).to be_within(0.01).of(66.67)
117
- expect(files.last['file']).to eq(File.expand_path('lib/bar.rb', root))
118
- end
119
-
120
- it 'sorts ascending by percentage then by file path' do
121
- files = model.all_files(sort_order: :ascending)
122
- expect(files.first['file']).to eq(File.expand_path('lib/bar.rb', root))
123
- expect(files.first['percentage']).to be_within(0.01).of(33.33)
124
- expect(files.last['file']).to eq(File.expand_path('lib/foo.rb', root))
125
- end
126
-
127
- it 'filters rows when tracked_globs are provided' do
128
- files = model.all_files(tracked_globs: ['lib/foo.rb'])
129
-
130
- expect(files.length).to eq(1)
131
- expect(files.first['file']).to eq(File.expand_path('lib/foo.rb', root))
132
- end
133
-
134
- it 'combines results from multiple tracked_globs patterns' do
135
- abs_bar = File.expand_path('lib/bar.rb', root)
136
-
137
- files = model.all_files(tracked_globs: ['lib/foo.rb', abs_bar])
138
-
139
- expect(files.map { |f| f['file'] }).to contain_exactly(
140
- File.expand_path('lib/foo.rb', root),
141
- abs_bar
142
- )
143
- end
144
-
145
- it 'handles files with paths that cannot be relativized' do
146
- # Create a custom row with a path from a Windows-style drive (C:/) that will cause ArgumentError
147
- # when trying to make it relative to a Unix-style root
148
- custom_rows = [
149
- {
150
- 'file' => 'C:/Windows/system32/file.rb',
151
- 'percentage' => 100.0,
152
- 'covered' => 10,
153
- 'total' => 10,
154
- 'stale' => false
155
- }
156
- ]
157
-
158
- # This should trigger the ArgumentError rescue in filter_rows_by_globs
159
- # When the path cannot be made relative (different path types), it falls back to using the absolute path
160
- output = model.format_table(custom_rows, tracked_globs: ['C:/Windows/**/*.rb'])
161
-
162
- # The file should be included because the absolute path fallback matches the glob
163
- expect(output).to include('C:/Windows/system32/file.rb')
164
- end
165
- end
166
-
167
- describe '#project_totals' do
168
- it 'aggregates coverage totals across all files' do
169
- totals = model.project_totals
170
-
171
- expect(totals['lines']).to include('total' => 6, 'covered' => 3, 'uncovered' => 3)
172
- expect(totals['percentage']).to be_within(0.01).of(50.0)
173
- expect(totals['files']).to include('total' => 2)
174
- expect(totals['files']['ok'] + totals['files']['stale']).to eq(totals['files']['total'])
175
- end
176
-
177
- it 'respects tracked_globs filtering' do
178
- totals = model.project_totals(tracked_globs: ['lib/foo.rb'])
179
-
180
- expect(totals['lines']).to include('total' => 3, 'covered' => 2, 'uncovered' => 1)
181
- expect(totals['files']).to include('total' => 1)
182
- end
183
- end
184
-
185
- describe 'resolve method error handling' do
186
- it 'raises FileError when coverage_lines is nil after lookup' do
187
- # Stub lookup_lines to return nil without raising
188
- allow(CovLoupe::CovUtil).to receive(:lookup_lines).and_return(nil)
189
-
190
- expect do
191
- model.summary_for('lib/nonexistent.rb')
192
- end.to raise_error(CovLoupe::FileError, /No coverage data found for file/)
193
- end
194
-
195
- it 'converts Errno::ENOENT to FileNotFoundError during resolve' do
196
- # We need to trigger Errno::ENOENT inside the resolve method
197
- # Stub the checker's check_file! method to raise Errno::ENOENT
198
- checker = instance_double(CovLoupe::StalenessChecker, off?: false)
199
- allow(CovLoupe::StalenessChecker).to receive(:new).and_return(checker)
200
- allow(checker).to receive(:check_file!)
201
- .and_raise(Errno::ENOENT, 'No such file or directory')
202
-
203
- # Create a model with staleness checking enabled to trigger the check_file! call
204
- stale_model = described_class.new(root: root, staleness: :error)
205
-
206
- expect do
207
- stale_model.summary_for('lib/foo.rb')
208
- end.to raise_error(CovLoupe::FileNotFoundError, /File not found/)
209
- end
210
-
211
- it 'raises FileError when lookup_lines raises RuntimeError' do
212
- allow(CovLoupe::CovUtil).to receive(:lookup_lines)
213
- .and_raise(RuntimeError, 'Could not find coverage data')
214
-
215
- expect do
216
- model.summary_for('lib/some_file.rb')
217
- end.to raise_error(CovLoupe::FileError, /No coverage data found for file/)
218
- end
219
- end
220
-
221
- describe 'resultset directory handling' do
222
- it 'accepts a directory containing .resultset.json' do
223
- model = described_class.new(root: root, resultset: 'coverage')
224
- data = model.summary_for('lib/foo.rb')
225
- expect(data['summary']['total']).to eq(3)
226
- expect(data['summary']['covered']).to eq(2)
227
- end
228
- end
229
-
230
- describe 'branch-only coverage resultsets' do
231
- let(:branch_root) { (FIXTURES_DIR / 'branch_only_project').to_s }
232
- let(:branch_model) { described_class.new(root: branch_root) }
233
-
234
- it 'computes summaries by synthesizing branch data' do
235
- data = branch_model.summary_for('lib/branch_only.rb')
236
-
237
- expect(data['summary']['total']).to eq(5)
238
- expect(data['summary']['covered']).to eq(3)
239
- expect(data['summary']['percentage']).to be_within(0.01).of(60.0)
240
- end
241
-
242
- it 'returns detailed data using branch-derived hits' do
243
- data = branch_model.detailed_for('lib/branch_only.rb')
244
-
245
- expect(data['lines']).to eq([
246
- { 'line' => 6, 'hits' => 3, 'covered' => true },
247
- { 'line' => 7, 'hits' => 0, 'covered' => false },
248
- { 'line' => 13, 'hits' => 0, 'covered' => false },
249
- { 'line' => 14, 'hits' => 2, 'covered' => true },
250
- { 'line' => 16, 'hits' => 2, 'covered' => true }
251
- ])
252
- end
253
-
254
- it 'identifies uncovered lines based on branch hits' do
255
- data = branch_model.uncovered_for('lib/branch_only.rb')
256
-
257
- expect(data['uncovered']).to eq([7, 13])
258
- end
259
-
260
- it 'includes branch-only files in all_files results' do
261
- files = branch_model.all_files(sort_order: :ascending)
262
- branch_path = File.expand_path('lib/branch_only.rb', branch_root)
263
- another_path = File.expand_path('lib/another.rb', branch_root)
264
-
265
- expect(files.map { |f| f['file'] }).to contain_exactly(branch_path, another_path)
266
-
267
- branch_entry = files.find { |f| f['file'] == branch_path }
268
- another_entry = files.find { |f| f['file'] == another_path }
269
-
270
- expect(branch_entry['total']).to eq(5)
271
- expect(branch_entry['covered']).to eq(3)
272
- expect(another_entry['total']).to eq(1)
273
- expect(another_entry['covered']).to eq(0)
274
- end
275
- end
276
-
277
- describe 'multiple suites in resultset' do
278
- let(:resultset_path) { '/tmp/multi_suite_resultset.json' }
279
- let(:suite_a_cov) do
280
- {
281
- File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, nil, 2] }
282
- }
283
- end
284
- let(:suite_b_cov) do
285
- {
286
- File.join(root, 'lib', 'bar.rb') => { 'lines' => [0, 1, 1] }
287
- }
288
- end
289
- let(:resultset) do
290
- {
291
- 'RSpec' => { 'timestamp' => 100, 'coverage' => suite_a_cov },
292
- 'Cucumber' => { 'timestamp' => 200, 'coverage' => suite_b_cov }
293
- }
294
- end
295
-
296
- let(:shared_file) { File.join(root, 'lib', 'foo.rb') }
297
- let(:suite_a_cov_combined) do
298
- {
299
- shared_file => { 'lines' => [1, 0, nil, 0] }
300
- }
301
- end
302
- let(:suite_b_cov_combined) do
303
- {
304
- shared_file => { 'lines' => [0, 3, nil, 1] }
305
- }
306
- end
307
- let(:resultset_combined) do
308
- {
309
- 'RSpec' => { 'timestamp' => 100, 'coverage' => suite_a_cov_combined },
310
- 'Cucumber' => { 'timestamp' => 150, 'coverage' => suite_b_cov_combined }
311
- }
312
- end
313
-
314
- before do
315
- allow(CovLoupe::CovUtil).to receive(:find_resultset).and_wrap_original do
316
- |original, search_root, resultset: nil|
317
- root_match = File.absolute_path(search_root) == File.absolute_path(root)
318
- resultset_empty = resultset.nil? || resultset.to_s.empty?
319
- if root_match && resultset_empty
320
- resultset_path
321
- else
322
- original.call(search_root, resultset: resultset)
323
- end
324
- end
325
- # This line might need to be removed as we now mock JSON.load_file directly
326
- end
327
-
328
- it 'merges coverage data from multiple suites while keeping latest timestamp' do
329
- allow(JSON).to receive(:load_file).with(resultset_path).and_return(resultset)
330
-
331
- model = described_class.new(root: root)
332
- files = model.all_files(sort_order: :ascending)
333
-
334
- expect(files.map { |f| File.basename(f['file']) }).to include('foo.rb', 'bar.rb')
335
-
336
- timestamp = model.instance_variable_get(:@cov_timestamp)
337
- expect(timestamp).to eq(200)
338
- end
339
-
340
- it 'combines coverage arrays when the same file appears in multiple suites' do
341
- allow(JSON).to receive(:load_file).with(resultset_path).and_return(resultset_combined)
342
-
343
- model = described_class.new(root: root)
344
- detailed = model.detailed_for('lib/foo.rb')
345
- hits_by_line = detailed['lines'].each_with_object({}) do |row, acc|
346
- acc[row['line']] = row['hits']
347
- end
348
-
349
- expect(hits_by_line[1]).to eq(1)
350
- expect(hits_by_line[2]).to eq(3)
351
- expect(hits_by_line[4]).to eq(1)
352
- end
353
- end
354
-
355
- describe 'format_table' do
356
- it 'returns a formatted table string with all files coverage data' do
357
- output = model.format_table
358
-
359
- # Should contain table structure
360
- expect(output).to include('┌', '┬', '┐', '│', '├', '┼', '┤', '└', '┴', '┘')
361
-
362
- # Should contain headers
363
- expect(output).to include('File', '%', 'Covered', 'Total', 'Stale')
364
-
365
- # Should contain file data
366
- expect(output).to include('lib/foo.rb', 'lib/bar.rb')
367
-
368
- # Should contain summary
369
- expect(output).to include('Files: total', ', ok ', ', stale ')
370
- end
371
-
372
- it 'returns "No coverage data found" when rows is empty' do
373
- rows = []
374
- output = model.format_table(rows)
375
- expect(output).to eq('No coverage data found')
376
- end
377
-
378
- it 'accepts custom rows parameter' do
379
- custom_rows = [
380
- {
381
- 'file' => '/path/to/file1.rb',
382
- 'percentage' => 100.0,
383
- 'covered' => 10,
384
- 'total' => 10,
385
- 'stale' => false
386
- },
387
- {
388
- 'file' => '/path/to/file2.rb',
389
- 'percentage' => 50.0,
390
- 'covered' => 5,
391
- 'total' => 10,
392
- 'stale' => 'M'
393
- },
394
- {
395
- 'file' => '/path/to/file3.rb',
396
- 'percentage' => 75.0,
397
- 'covered' => 15,
398
- 'total' => 20,
399
- 'stale' => 'T'
400
- }
401
- ]
402
-
403
- output = model.format_table(custom_rows)
404
-
405
- expect(output).to include('file1.rb')
406
- expect(output).to include('file2.rb')
407
- expect(output).to include('file3.rb')
408
- expect(output).to include('100.00')
409
- expect(output).to include('50.00')
410
- expect(output).to include('75.00')
411
- expect(output).to include('M')
412
- expect(output).to include('T')
413
- expect(output).not_to include('!')
414
- staleness_msg = 'Staleness: M = Missing file, T = Timestamp (source newer), ' \
415
- 'L = Line count mismatch'
416
- expect(output).to include(staleness_msg)
417
- end
418
-
419
- it 'accepts sort_order parameter' do
420
- # Test that sort_order parameter is passed through correctly
421
- output_asc = model.format_table(sort_order: :ascending)
422
- output_desc = model.format_table(sort_order: :descending)
423
-
424
- # Both should be valid table outputs
425
- expect(output_asc).to include('┌')
426
- expect(output_desc).to include('┌')
427
- expect(output_asc).to include('Files: total')
428
- expect(output_desc).to include('Files: total')
429
- end
430
-
431
- it 'sorts table output correctly when provided with custom rows' do
432
- # Get all files data to use as custom rows
433
- all_files_data = model.all_files
434
-
435
- # Test ascending sort with custom rows
436
- output_asc = model.format_table(all_files_data, sort_order: :ascending)
437
- lines_asc = output_asc.split("\n")
438
- bar_line_asc = lines_asc.find { |line| line.include?('bar.rb') }
439
- foo_line_asc = lines_asc.find { |line| line.include?('foo.rb') }
440
-
441
- # In ascending order, bar.rb (33.33%) should come before foo.rb (66.67%)
442
- expect(lines_asc.index(bar_line_asc)).to be < lines_asc.index(foo_line_asc)
443
-
444
- # Test descending sort with custom rows
445
- output_desc = model.format_table(all_files_data, sort_order: :descending)
446
- lines_desc = output_desc.split("\n")
447
- bar_line_desc = lines_desc.find { |line| line.include?('bar.rb') }
448
- foo_line_desc = lines_desc.find { |line| line.include?('foo.rb') }
449
-
450
- # In descending order, foo.rb (66.67%) should come before bar.rb (33.33%)
451
- expect(lines_desc.index(foo_line_desc)).to be < lines_desc.index(bar_line_desc)
452
- end
453
- end
454
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe do
6
- # Mode detection tests moved to mode_detector_spec.rb
7
- # These tests verify the integration with ModeDetector
8
- describe 'mode detection integration' do
9
- it 'uses ModeDetector for CLI mode detection' do
10
- allow(described_class::ModeDetector).to receive(:cli_mode?).with(['--force-cli'])
11
- .and_return(true)
12
- cli = instance_double(described_class::CoverageCLI, run: nil)
13
- allow(described_class::CoverageCLI).to receive(:new).and_return(cli)
14
-
15
- described_class.run(['--force-cli'])
16
-
17
- expect(described_class::ModeDetector).to have_received(:cli_mode?).with(['--force-cli'])
18
- expect(described_class::CoverageCLI).to have_received(:new)
19
- expect(cli).to have_received(:run)
20
- end
21
- end
22
-
23
- # When no thread-local context exists, active_log_file= creates one
24
- # from the default context rather than modifying an existing one.
25
- describe '.active_log_file=' do
26
- it 'creates context from default when no current context exists' do
27
- Thread.current[:cov_loupe_context] = nil
28
-
29
- described_class.active_log_file = '/tmp/test.log'
30
-
31
- expect(described_class.context).not_to be_nil
32
- expect(described_class.active_log_file).to eq('/tmp/test.log')
33
- ensure
34
- described_class.active_log_file = File::NULL
35
- end
36
- end
37
- end