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,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
- require_relative '../shared_examples/formatted_command_examples'
5
-
6
- RSpec.describe CovLoupe::Commands::TotalsCommand do
7
- let(:root) { (FIXTURES_DIR / 'project1').to_s }
8
- let(:cli_context) { CovLoupe::CoverageCLI.new }
9
- let(:command) { described_class.new(cli_context) }
10
-
11
- before do
12
- cli_context.config.root = root
13
- cli_context.config.resultset = 'coverage'
14
- cli_context.config.format = :table
15
- end
16
-
17
- describe '#execute' do
18
- context 'with table format' do
19
- it 'prints aggregated totals for the project' do
20
- output = capture_command_output(command, [])
21
-
22
- expect(output).to include('│', 'Lines', '50.00%')
23
- end
24
- end
25
-
26
- it_behaves_like 'a command with formatted output', [], ['lines', 'files', 'percentage']
27
-
28
- it 'raises when unexpected arguments are provided' do
29
- expect do
30
- command.execute(['extra'])
31
- end.to raise_error(CovLoupe::UsageError, include('totals'))
32
- end
33
- end
34
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
- require_relative '../shared_examples/formatted_command_examples'
5
-
6
- RSpec.describe CovLoupe::Commands::UncoveredCommand do
7
- let(:root) { (FIXTURES_DIR / 'project1').to_s }
8
- let(:cli_context) { CovLoupe::CoverageCLI.new }
9
- let(:command) { described_class.new(cli_context) }
10
-
11
- before do
12
- cli_context.config.root = root
13
- cli_context.config.resultset = 'coverage'
14
- cli_context.config.format = :table
15
- cli_context.config.source_mode = nil
16
- end
17
-
18
- describe '#execute' do
19
- context 'with table format' do
20
- it 'prints uncovered line numbers with the summary' do
21
- output = capture_command_output(command, ['lib/bar.rb'])
22
-
23
- expect(output).to include('│', 'lib/bar.rb', '33.33%')
24
- end
25
- end
26
-
27
- context 'when the file is fully covered' do
28
- before do
29
- mock_presenter(
30
- CovLoupe::Presenters::CoverageUncoveredPresenter,
31
- absolute_payload: {
32
- 'file' => 'lib/perfect.rb',
33
- 'uncovered' => [],
34
- 'summary' => { 'covered' => 10, 'total' => 10, 'percentage' => 100.0 }
35
- },
36
- relative_path: 'lib/perfect.rb'
37
- )
38
- end
39
-
40
- it 'prints a success message instead of a table' do
41
- output = capture_command_output(command, ['lib/perfect.rb'])
42
-
43
- expect(output).to include('All lines covered!', '100.00%')
44
- expect(output).not_to include('│')
45
- end
46
- end
47
-
48
- context 'with stale data' do
49
- before { stub_staleness_check('L') }
50
-
51
- it_behaves_like 'a command with formatted output', ['lib/foo.rb'],
52
- { 'file' => 'lib/foo.rb', 'uncovered' => [2], 'summary' => nil, 'stale' => 'L' }
53
- end
54
- end
55
- end
@@ -1,213 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
- require 'tempfile'
5
-
6
- RSpec.describe CovLoupe::Commands::ValidateCommand do
7
- let(:root) { (FIXTURES_DIR / 'project1').to_s }
8
-
9
- def with_temp_predicate(content)
10
- Tempfile.create(['predicate', '.rb']) do |file|
11
- file.write(content)
12
- file.flush
13
- yield file.path
14
- end
15
- end
16
-
17
- describe 'validate subcommand with file' do
18
- it 'exits 0 when predicate returns truthy value' do
19
- with_temp_predicate("->(model) { true }\n") do |path|
20
- _out, _err, status = run_cli_with_status(
21
- '--root', root,
22
- '--resultset', 'coverage',
23
- 'validate', path
24
- )
25
- expect(status).to eq(0)
26
- end
27
- end
28
-
29
- it 'exits 1 when predicate returns falsy value' do
30
- with_temp_predicate("->(model) { false }\n") do |path|
31
- _out, _err, status = run_cli_with_status(
32
- '--root', root,
33
- '--resultset', 'coverage',
34
- 'validate', path
35
- )
36
- expect(status).to eq(1)
37
- end
38
- end
39
-
40
- it 'exits 2 when predicate raises an error' do
41
- with_temp_predicate("->(model) { raise 'Boom!' }\n") do |path|
42
- _out, err, status = run_cli_with_status(
43
- '--root', root,
44
- '--resultset', 'coverage',
45
- 'validate', path
46
- )
47
- expect(status).to eq(2)
48
- expect(err).to include('Predicate error: Boom!')
49
- end
50
- end
51
-
52
- it 'shows backtrace when predicate errors with --error-mode debug' do
53
- with_temp_predicate("->(model) { raise 'Boom!' }\n") do |path|
54
- _out, err, status = run_cli_with_status(
55
- '--error-mode', 'debug',
56
- '--root', root,
57
- '--resultset', 'coverage',
58
- 'validate', path
59
- )
60
- expect(status).to eq(2)
61
- expect(err).to include('Predicate error: Boom!')
62
- # With trace mode, should show backtrace
63
- expect(err).to match(/predicate.*\.rb:\d+/)
64
- end
65
- end
66
-
67
- it 'exits 2 when predicate file is not found' do
68
- _out, err, status = run_cli_with_status(
69
- '--root', root,
70
- '--resultset', 'coverage',
71
- 'validate', '/nonexistent/predicate.rb'
72
- )
73
- expect(status).to eq(2)
74
- expect(err).to include('Predicate file not found')
75
- end
76
-
77
- it 'exits 2 when predicate has syntax error' do
78
- with_temp_predicate("-> { this is invalid syntax\n") do |path|
79
- _out, err, status = run_cli_with_status(
80
- '--root', root,
81
- '--resultset', 'coverage',
82
- 'validate', path
83
- )
84
- expect(status).to eq(2)
85
- expect(err).to include('Syntax error in predicate file')
86
- end
87
- end
88
-
89
- it 'exits 2 when predicate is not callable' do
90
- with_temp_predicate("42\n") do |path|
91
- _out, err, status = run_cli_with_status(
92
- '--root', root,
93
- '--resultset', 'coverage',
94
- 'validate', path
95
- )
96
- expect(status).to eq(2)
97
- expect(err).to include('Predicate must be callable')
98
- end
99
- end
100
-
101
- it 'provides model to predicate that can query coverage' do
102
- # Test that the predicate receives a working CoverageModel
103
- with_temp_predicate(<<~RUBY) do |path|
104
- ->(model) do
105
- # Access coverage data via the model
106
- summary = model.summary_for('lib/foo.rb')
107
- summary['summary']['percentage'] > 50 # Should be true for foo.rb
108
- end
109
- RUBY
110
- _out, _err, status = run_cli_with_status(
111
- '--root', root,
112
- '--resultset', 'coverage',
113
- 'validate', path
114
- )
115
- expect(status).to eq(0)
116
- end
117
- end
118
- end
119
-
120
- describe 'validate subcommand with -i/--inline flag' do
121
- it 'exits 0 when predicate code returns truthy value' do
122
- _out, _err, status = run_cli_with_status(
123
- '--root', root,
124
- '--resultset', 'coverage',
125
- 'validate', '-i', '->(model) { true }'
126
- )
127
- expect(status).to eq(0)
128
- end
129
-
130
- it 'exits 1 when predicate code returns falsy value' do
131
- _out, _err, status = run_cli_with_status(
132
- '--root', root,
133
- '--resultset', 'coverage',
134
- 'validate', '-i', '->(model) { false }'
135
- )
136
- expect(status).to eq(1)
137
- end
138
-
139
- it 'exits 2 when predicate code raises an error' do
140
- _out, err, status = run_cli_with_status(
141
- '--root', root,
142
- '--resultset', 'coverage',
143
- 'validate', '-i', "->(model) { raise 'Boom!' }"
144
- )
145
- expect(status).to eq(2)
146
- expect(err).to include('Predicate error: Boom!')
147
- end
148
-
149
- it 'exits 2 when predicate code has syntax error' do
150
- _out, err, status = run_cli_with_status(
151
- '--root', root,
152
- '--resultset', 'coverage',
153
- 'validate', '-i', '-> { invalid syntax'
154
- )
155
- expect(status).to eq(2)
156
- expect(err).to include('Syntax error in predicate code')
157
- end
158
-
159
- it 'exits 2 when predicate code is not callable' do
160
- _out, err, status = run_cli_with_status(
161
- '--root', root,
162
- '--resultset', 'coverage',
163
- 'validate', '-i', '42'
164
- )
165
- expect(status).to eq(2)
166
- expect(err).to include('Predicate must be callable')
167
- end
168
-
169
- it 'provides model to predicate that can query coverage' do
170
- code = <<~RUBY.strip
171
- ->(model) { model.summary_for('lib/foo.rb')['summary']['percentage'] > 50 }
172
- RUBY
173
- _out, _err, status = run_cli_with_status(
174
- '--root', root,
175
- '--resultset', 'coverage',
176
- 'validate', '-i', code
177
- )
178
- expect(status).to eq(0)
179
- end
180
- end
181
-
182
- describe 'error handling' do
183
- it 'raises error when no file or -i flag provided' do
184
- _out, err, status = run_cli_with_status(
185
- '--root', root,
186
- '--resultset', 'coverage',
187
- 'validate'
188
- )
189
- expect(status).to eq(1)
190
- expect(err).to include('validate <file> | -i <code>')
191
- end
192
-
193
- it 'raises error when -i flag provided without code' do
194
- _out, err, status = run_cli_with_status(
195
- '--root', root,
196
- '--resultset', 'coverage',
197
- 'validate', '-i'
198
- )
199
- expect(status).to eq(1)
200
- expect(err).to include('validate -i <code>')
201
- end
202
-
203
- it 'raises error when unknown option is provided' do
204
- _out, err, status = run_cli_with_status(
205
- '--root', root,
206
- '--resultset', 'coverage',
207
- 'validate', '--unknown-option'
208
- )
209
- expect(status).to eq(1)
210
- expect(err).to include('Unknown option for validate: --unknown-option')
211
- end
212
- end
213
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
- require_relative '../shared_examples/formatted_command_examples'
5
-
6
- RSpec.describe CovLoupe::Commands::VersionCommand do
7
- let(:cli_context) { CovLoupe::CoverageCLI.new }
8
- let(:command) { described_class.new(cli_context) }
9
-
10
- before do
11
- cli_context.config.format = :table
12
- end
13
-
14
- describe '#execute' do
15
- context 'with table format' do
16
- it 'prints version, gem root, and documentation info in text mode' do
17
- output = capture_command_output(command, [])
18
-
19
- expect(output).to include('│', CovLoupe::VERSION, 'Gem Root', 'Documentation',
20
- 'README.md')
21
- end
22
-
23
- it 'includes a valid gem root path that exists' do
24
- output = capture_command_output(command, [])
25
-
26
- # Extract gem root from table output
27
- gem_root_line = output.lines.find { |line| line.include?('Gem Root') }
28
- expect(gem_root_line).not_to be_nil
29
-
30
- parts = gem_root_line.split('│')
31
- gem_root = parts[-2].strip
32
- expect(File.directory?(gem_root)).to be true
33
- end
34
- end
35
-
36
- it_behaves_like 'a command with formatted output', [], ['version', 'gem_root']
37
- end
38
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::Constants do
6
- describe 'OPTIONS_EXPECTING_ARGUMENT' do
7
- subject(:options) { described_class::OPTIONS_EXPECTING_ARGUMENT }
8
-
9
- it 'exists' do
10
- expect(options).not_to be_nil
11
- end
12
-
13
- it 'is frozen' do
14
- expect(options).to be_frozen
15
- end
16
-
17
- it 'contains expected CLI options' do
18
- expected_options = %w[
19
- -r --resultset
20
- -R --root
21
- -f --format
22
- -o --sort-order
23
- -s --source
24
- -c --context-lines
25
- -S --staleness
26
- -g --tracked-globs
27
- -l --log-file
28
- --error-mode
29
- ]
30
-
31
- expect(options).to eq(expected_options)
32
- end
33
-
34
- it 'contains only strings' do
35
- expect(options).to all(be_a(String))
36
- end
37
-
38
- it 'contains options that start with dashes' do
39
- expect(options).to all(start_with('-'))
40
- end
41
- end
42
-
43
- describe 'usage by other classes' do
44
- it 'is used by ModeDetector' do
45
- expect(CovLoupe::ModeDetector::OPTIONS_EXPECTING_ARGUMENT)
46
- .to equal(CovLoupe::Constants::OPTIONS_EXPECTING_ARGUMENT)
47
- end
48
-
49
- it 'is used by CoverageCLI' do
50
- expect(CovLoupe::CoverageCLI::OPTIONS_EXPECTING_ARGUMENT)
51
- .to equal(CovLoupe::Constants::OPTIONS_EXPECTING_ARGUMENT)
52
- end
53
-
54
- it 'ensures both classes reference the same object' do
55
- cli_options = CovLoupe::CoverageCLI::OPTIONS_EXPECTING_ARGUMENT
56
- detector_options = CovLoupe::ModeDetector::OPTIONS_EXPECTING_ARGUMENT
57
-
58
- expect(cli_options).to equal(detector_options)
59
- end
60
- end
61
- end
@@ -1,267 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::Formatters::SourceFormatter do
6
- subject(:formatter) { described_class.new(color_enabled: color_enabled) }
7
-
8
- let(:color_enabled) { false }
9
- let(:model) { instance_double(CovLoupe::CoverageModel) }
10
- let(:path) { 'lib/foo.rb' }
11
- let(:abs_path) { File.expand_path(path) }
12
- let(:source_content) do
13
- <<~RUBY
14
- class Foo
15
- def bar
16
- puts 'bar'
17
- end
18
- end
19
- RUBY
20
- end
21
- let(:coverage_lines) { [1, 1, 0, nil, nil] } # Line 3 is uncovered
22
-
23
- before do
24
- allow(model).to receive(:raw_for).with(path).and_return(
25
- 'file' => abs_path,
26
- 'lines' => coverage_lines
27
- )
28
- allow(File).to receive(:file?).with(abs_path).and_return(true)
29
- allow(File).to receive(:readlines).with(abs_path, chomp: true)
30
- .and_return(source_content.lines(chomp: true))
31
- end
32
-
33
- describe '#format_source_for' do
34
- context 'when source is available' do
35
- it 'renders formatted source lines with line numbers and markers' do
36
- # Full mode should print every line with coverage markers and numbering.
37
- result = formatter.format_source_for(model, path, mode: :full)
38
-
39
- expect(result.lines(chomp: true)).to eq(
40
- [
41
- ' Line | Source',
42
- '------ ---+-------------------------------------------------------------',
43
- ' 1 ✓ | class Foo',
44
- ' 2 ✓ | def bar',
45
- " 3 · | puts 'bar'",
46
- ' 4 | end',
47
- ' 5 | end'
48
- ]
49
- )
50
- end
51
-
52
- it 'marks covered lines with a checkmark' do
53
- # Two covered lines should each get a ✓ in the rendered output.
54
- result = formatter.format_source_for(model, path, mode: :full)
55
- # covered: true -> '✓', false -> '·', nil -> ' '
56
- expect(result.count('✓')).to eq(2)
57
- expect(result.lines[2]).to match(/\b1\s+✓ \| class Foo/)
58
- expect(result.lines[3]).to match(/\b2\s+✓ \| def bar/)
59
- end
60
-
61
- it 'marks uncovered lines with a dot' do
62
- # The single uncovered line should be marked with a dot.
63
- result = formatter.format_source_for(model, path, mode: :full)
64
- expect(result.count('·')).to eq(1)
65
- expect(result.lines[4]).to match(/\b3\s+· \| puts 'bar'/)
66
- end
67
-
68
- it 'returns only header when mode is nil (default)' do
69
- # Default mode skips body rows but still emits the header scaffold.
70
- result = formatter.format_source_for(model, path)
71
- # Example header-only output:
72
- # Line | Source
73
- # ------ ---+-------------------------------------------------------------
74
- expect(result).not_to include('class Foo')
75
- expect(result).to include('Line', 'Source')
76
- end
77
- end
78
-
79
- context 'when source file is not found' do
80
- it 'returns fallback message' do
81
- # Simulate missing file; formatter should not raise and should return a placeholder.
82
- allow(File).to receive(:file?).with(abs_path).and_return(false)
83
- expect(formatter.format_source_for(model, path)).to eq('[source not available]')
84
- end
85
- end
86
-
87
- context 'when raw coverage data is missing' do
88
- it 'returns fallback message' do
89
- # No coverage entry for the path should also trigger the placeholder.
90
- allow(model).to receive(:raw_for).with(path).and_return(nil)
91
- expect(formatter.format_source_for(model, path)).to eq('[source not available]')
92
- end
93
- end
94
-
95
- context 'when an error occurs during formatting' do
96
- it 'returns fallback message instead of crashing' do
97
- # Create a pathological coverage array with an object that raises on to_i
98
- bad_object = Object.new
99
- def bad_object.to_i = raise(StandardError, 'Bad data')
100
- def bad_object.nil? = false
101
-
102
- bad_coverage = [1, 1, bad_object, nil, nil]
103
-
104
- allow(model).to receive(:raw_for).with(path)
105
- .and_return('file' => abs_path, 'lines' => bad_coverage)
106
-
107
- result = formatter.format_source_for(model, path, mode: :full)
108
- expect(result).to eq('[source not available]')
109
- end
110
-
111
- it 'propagates ArgumentError' do
112
- expect do
113
- formatter.format_source_for(model, path, mode: :full, context: -1)
114
- end.to raise_error(ArgumentError, 'Context lines cannot be negative')
115
- end
116
- end
117
-
118
- context 'with color enabled' do
119
- let(:color_enabled) { true }
120
-
121
- it 'includes ANSI color codes' do
122
- # Markers should be wrapped with green/red ANSI sequences when colors are on.
123
- # Example colored line: " 1 \e[32m✓\e[0m | class Foo"
124
- result = formatter.format_source_for(model, path, mode: :full)
125
- expect(result).to include("\e[32m", "\e[31m") # green for checkmark, red for dot
126
- expect(result.lines[2]).to include("\e[32m✓\e[0m") # line 1 checkmark is green
127
- expect(result.lines[3]).to include("\e[32m✓\e[0m") # line 2 checkmark is green
128
- expect(result.lines[4]).to include("\e[31m·\e[0m") # line 3 dot is red
129
- end
130
- end
131
- end
132
-
133
- describe '#build_source_payload' do
134
- it 'returns row data when source is available' do
135
- # Payload should mirror the row hashes used by CLI formatting.
136
- result = formatter.build_source_payload(model, path, mode: :full)
137
- expect(result).to be_a(Array)
138
- expect(result.size).to eq(5)
139
- expect(result.first).to include('code' => 'class Foo', 'line' => 1)
140
- end
141
-
142
- it 'returns nil when raw coverage is missing' do
143
- # Without coverage data, there is no payload to build.
144
- allow(model).to receive(:raw_for).with(path).and_return(nil)
145
- expect(formatter.build_source_payload(model, path)).to be_nil
146
- end
147
-
148
- it 'returns nil when source file is missing' do
149
- # Missing source file should also produce a nil payload.
150
- allow(File).to receive(:file?).with(abs_path).and_return(false)
151
- expect(formatter.build_source_payload(model, path)).to be_nil
152
- end
153
- end
154
-
155
- describe '#build_source_rows' do
156
- it 'raises error for negative context count' do
157
- # Negative context should raise ArgumentError
158
- expect do
159
- formatter.build_source_rows(
160
- source_content.lines(chomp: true),
161
- coverage_lines,
162
- mode: :uncovered,
163
- context: -1
164
- )
165
- end.to raise_error(ArgumentError, 'Context lines cannot be negative')
166
- end
167
-
168
- it 'handles default context (2 lines)' do
169
- # With the default context of 2, uncovered lines pull in surrounding rows.
170
- rows = formatter.build_source_rows(
171
- source_content.lines(chomp: true),
172
- coverage_lines,
173
- mode: :uncovered,
174
- context: 2
175
- )
176
- # Uncovered is line 3. Context 2 means lines 1..5 (indexes 0..4).
177
- # Total line count is 5, so all lines should be included.
178
- expect(rows.size).to eq(5)
179
- end
180
-
181
- it 'handles bad context input (non-numeric)' do
182
- # Non-numeric context coerces to 0 via to_i, so only the miss is included.
183
- rows = formatter.build_source_rows(
184
- source_content.lines(chomp: true),
185
- coverage_lines,
186
- mode: :uncovered,
187
- context: 'bad'
188
- )
189
- # "bad".to_i is 0 so context should be 0.
190
- # Uncovered is line 3.
191
- expect(rows.size).to eq(1)
192
- expect(rows.first['line']).to eq(3)
193
- end
194
-
195
- it 'handles context input that raises error on to_i conversion' do
196
- # Create an object where to_i raises an error
197
- bad_context = Object.new
198
- def bad_context.to_i = raise(StandardError, 'Cannot convert')
199
-
200
- # Falling back to default context should still include surrounding lines.
201
- rows = formatter.build_source_rows(
202
- source_content.lines(chomp: true),
203
- coverage_lines,
204
- mode: :uncovered,
205
- context: bad_context
206
- )
207
- # Should fall back to default context of 2
208
- # Uncovered is line 3. Context 2 means lines 1..5 (indexes 0..4).
209
- # Total lines is 5. So all lines should be included.
210
- expect(rows.size).to eq(5)
211
- end
212
-
213
- it 'handles nil coverage lines defensively' do
214
- # Nil coverage array should not raise; hits/covered become nil.
215
- # This covers the "coverage data missing entirely" path; in real coverage we'd see 1/0 hits.
216
- # Expected rows when coverage is nil:
217
- # [
218
- # { 'line' => 1, 'code' => 'class Foo', 'hits' => nil, 'covered' => nil },
219
- # { 'line' => 2, 'code' => ' def bar', 'hits' => nil, 'covered' => nil },
220
- # { 'line' => 3, 'code' => " puts 'bar'", 'hits' => nil, 'covered' => nil },
221
- # { 'line' => 4, 'code' => ' end', 'hits' => nil, 'covered' => nil },
222
- # { 'line' => 5, 'code' => 'end', 'hits' => nil, 'covered' => nil }
223
- # ]
224
- # And the formatted output (markers blank because coverage is missing) would be:
225
- # Line | Source
226
- # ------ ---+-------------------------------------------------------------
227
- # 1 | class Foo
228
- # 2 | def bar
229
- # 3 | puts 'bar'
230
- # 4 | end
231
- # 5 | end
232
- rows = formatter.build_source_rows(
233
- source_content.lines(chomp: true),
234
- nil,
235
- mode: :full,
236
- context: 2
237
- )
238
- expect(rows.size).to eq(5)
239
- expect(rows.first['hits']).to be_nil
240
- end
241
- end
242
-
243
- describe '#format_detailed_rows' do
244
- it 'formats rows into a table' do
245
- # Detailed mode should align numeric columns and boolean covered flags.
246
- rows = [
247
- { 'line' => 1, 'hits' => 5, 'covered' => true },
248
- { 'line' => 2, 'hits' => 0, 'covered' => false }
249
- ]
250
- # Expected table:
251
- # Line Hits Covered
252
- # ----- ---- -------
253
- # 1 5 yes
254
- # 2 0 no
255
- result = formatter.format_detailed_rows(rows)
256
- expect(result).to include('Line', 'Hits', 'Covered', '5', 'yes', 'no')
257
- end
258
- end
259
-
260
- describe 'private #fetch_raw error handling' do
261
- it 'returns nil if model raises error' do
262
- # fetch_raw should swallow model errors and return nil instead of propagating.
263
- allow(model).to receive(:raw_for).and_raise(StandardError)
264
- expect(formatter.send(:fetch_raw, model, path)).to be_nil
265
- end
266
- end
267
- end