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,142 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::AppConfig do
6
- describe '#initialize' do
7
- it 'creates a config with default values' do
8
- config = described_class.new
9
- expect(config.root).to eq('.')
10
- expect(config.format).to eq(:table)
11
- expect(config.sort_order).to eq(:descending)
12
- expect(config.source_context).to eq(2)
13
- expect(config.error_mode).to eq(:log)
14
- expect(config.staleness).to eq(:off)
15
- expect(config.resultset).to be_nil
16
- expect(config.source_mode).to be_nil
17
- expect(config.tracked_globs).to be_nil
18
- expect(config.log_file).to be_nil
19
- end
20
-
21
- it 'allows overriding defaults via keyword arguments' do
22
- config = described_class.new(
23
- root: '/custom',
24
- format: :json,
25
- sort_order: :descending,
26
- staleness: :error
27
- )
28
- expect(config.root).to eq('/custom')
29
- expect(config.format).to eq(:json)
30
- expect(config.sort_order).to eq(:descending)
31
- expect(config.staleness).to eq(:error)
32
- end
33
-
34
- it 'is mutable (struct fields can be changed)' do
35
- config = described_class.new
36
- config.root = '/new/root'
37
- config.format = :json
38
- expect(config.root).to eq('/new/root')
39
- expect(config.format).to eq(:json)
40
- end
41
- end
42
-
43
- describe '#model_options' do
44
- it 'returns hash suitable for CoverageModel.new' do
45
- config = described_class.new(
46
- root: '/custom/root',
47
- resultset: '/custom/.resultset.json',
48
- staleness: :error,
49
- tracked_globs: ['lib/**/*.rb']
50
- )
51
-
52
- options = config.model_options
53
- expect(options).to eq({
54
- root: '/custom/root',
55
- resultset: '/custom/.resultset.json',
56
- staleness: :error,
57
- tracked_globs: ['lib/**/*.rb']
58
- })
59
- end
60
-
61
- it 'handles nil values correctly' do
62
- config = described_class.new
63
- options = config.model_options
64
- expect(options[:root]).to eq('.')
65
- expect(options[:resultset]).to be_nil
66
- expect(options[:staleness]).to eq(:off)
67
- expect(options[:tracked_globs]).to be_nil
68
- end
69
- end
70
-
71
- describe '#formatter_options' do
72
- it 'returns hash suitable for SourceFormatter.new' do
73
- config = described_class.new(color: true)
74
- options = config.formatter_options
75
- expect(options).to eq({ color_enabled: true })
76
- end
77
-
78
- it 'handles false color setting' do
79
- config = described_class.new(color: false)
80
- options = config.formatter_options
81
- expect(options).to eq({ color_enabled: false })
82
- end
83
- end
84
-
85
- describe 'struct behavior' do
86
- it 'supports equality comparison' do
87
- config1 = described_class.new(root: '/foo', format: :json)
88
- config2 = described_class.new(root: '/foo', format: :json)
89
- config3 = described_class.new(root: '/bar', format: :json)
90
-
91
- expect(config1).to eq(config2)
92
- expect(config1).not_to eq(config3)
93
- end
94
-
95
- it 'provides readable inspect output' do
96
- config = described_class.new(root: '/test', format: :json)
97
- output = config.inspect
98
- expect(output).to include('root="/test"')
99
- expect(output).to include('format=:json')
100
- end
101
-
102
- it 'converts to hash' do
103
- config = described_class.new(root: '/test', format: :json)
104
- hash = config.to_h
105
- expect(hash).to be_a(Hash)
106
- expect(hash[:root]).to eq('/test')
107
- expect(hash[:format]).to eq(:json)
108
- end
109
- end
110
-
111
- describe 'symbol enumerated values' do
112
- it 'uses symbols for format' do
113
- config = described_class.new(format: :json)
114
- expect(config.format).to eq(:json)
115
- expect(config.format).to be_a(Symbol)
116
- end
117
-
118
- it 'uses symbols for sort_order' do
119
- config = described_class.new(sort_order: :descending)
120
- expect(config.sort_order).to eq(:descending)
121
- expect(config.sort_order).to be_a(Symbol)
122
- end
123
-
124
- it 'uses symbols for staleness' do
125
- config = described_class.new(staleness: :error)
126
- expect(config.staleness).to eq(:error)
127
- expect(config.staleness).to be_a(Symbol)
128
- end
129
-
130
- it 'uses symbols for error_mode' do
131
- config = described_class.new(error_mode: :debug)
132
- expect(config.error_mode).to eq(:debug)
133
- expect(config.error_mode).to be_a(Symbol)
134
- end
135
-
136
- it 'uses symbols for source_mode' do
137
- config = described_class.new(source_mode: :uncovered)
138
- expect(config.source_mode).to eq(:uncovered)
139
- expect(config.source_mode).to be_a(Symbol)
140
- end
141
- end
142
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::BaseTool do
6
- let(:handler) { CovLoupe::ErrorHandler.new(error_mode: :log, logger: test_logger) }
7
- let(:test_logger) do
8
- Class.new do
9
- attr_reader :messages
10
-
11
- def initialize = @messages = []
12
- def error(msg) = @messages << msg
13
- end.new
14
- end
15
-
16
- let(:orig_handler) do
17
- CovLoupe.error_handler
18
- rescue
19
- nil
20
- end
21
-
22
- before do
23
- CovLoupe.error_handler = handler
24
- setup_mcp_response_stub
25
- end
26
-
27
- after do
28
- CovLoupe.error_handler = orig_handler if orig_handler
29
- end
30
-
31
- shared_examples 'friendly response and logged' do
32
- it 'returns friendly text' do
33
- resp = described_class.handle_mcp_error(error, tool, error_mode: :log)
34
- expect(resp).to be_a(MCP::Tool::Response)
35
- expect(resp.payload.first['text']).to match(expected_pattern)
36
- end
37
-
38
- it 'respects error_mode :off' do
39
- resp = described_class.handle_mcp_error(error, tool, error_mode: :off)
40
- expect(resp).to be_a(MCP::Tool::Response)
41
- expect(resp.payload.first['text']).to match(expected_pattern)
42
- end
43
- end
44
-
45
- context 'with CovLoupe::Error' do
46
- let(:error) { CovLoupe::UsageError.new('invalid args') }
47
- let(:tool) { 'coverage_summary' }
48
- let(:expected_pattern) { /Error: invalid args/ }
49
- let(:log_fragment) { 'invalid args' }
50
-
51
- it_behaves_like 'friendly response and logged'
52
- end
53
-
54
- context 'with standard error' do
55
- let(:error) { Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.rb') }
56
- let(:tool) { 'coverage_raw' }
57
- let(:expected_pattern) { /Error: .*File not found: missing.rb/ }
58
- let(:log_fragment) { 'File not found' }
59
-
60
- it_behaves_like 'friendly response and logged'
61
- end
62
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::CoverageCLI do
6
- let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
- let(:cli) { described_class.new }
8
-
9
- before do
10
- cli.config.root = root
11
- cli.config.resultset = 'coverage'
12
- cli.config.staleness = :off
13
- cli.config.tracked_globs = nil
14
- end
15
-
16
- describe '#show_default_report' do
17
- it 'prints JSON summary using relativized payload when json mode is enabled' do
18
- cli.config.format = :json
19
-
20
- output = nil
21
- silence_output do |stdout, _stderr|
22
- cli.show_default_report(sort_order: :ascending, output: stdout)
23
- output = stdout.string
24
- end
25
-
26
- payload = JSON.parse(output)
27
-
28
- expect(payload['files']).to be_an(Array)
29
- expect(payload['files'].first['file']).to eq('lib/bar.rb').or eq('lib/foo.rb')
30
- expect(payload['counts']).to include('total', 'ok', 'stale')
31
- end
32
- end
33
- end
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe 'CLI enumerated option parsing' do
6
- def parse!(argv)
7
- cli = CovLoupe::CoverageCLI.new
8
- cli.send(:parse_options!, argv.dup)
9
- cli
10
- end
11
-
12
- describe 'accepts short and long forms' do
13
- cases = [
14
- { argv: ['--sort-order', 'a', 'list'], accessor: :sort_order, expected: :ascending },
15
- { argv: ['--sort-order', 'd', 'list'], accessor: :sort_order, expected: :descending },
16
- { argv: ['--sort-order', 'ascending', 'list'], accessor: :sort_order, expected: :ascending },
17
- { argv: ['--sort-order', 'descending', 'list'], accessor: :sort_order,
18
- expected: :descending },
19
-
20
- { argv: ['--source', 'f', 'summary', 'lib/foo.rb'], accessor: :source_mode, expected: :full },
21
- { argv: ['--source', 'u', 'summary', 'lib/foo.rb'], accessor: :source_mode,
22
- expected: :uncovered },
23
- { argv: ['--source', 'full', 'summary', 'lib/foo.rb'], accessor: :source_mode,
24
- expected: :full },
25
- { argv: ['--source', 'uncovered', 'summary', 'lib/foo.rb'], accessor: :source_mode,
26
- expected: :uncovered },
27
-
28
- { argv: ['-S', 'e', 'list'], accessor: :staleness, expected: :error },
29
- { argv: ['-S', 'o', 'list'], accessor: :staleness, expected: :off },
30
- { argv: ['--staleness', 'e', 'list'], accessor: :staleness, expected: :error },
31
- { argv: ['--staleness', 'o', 'list'], accessor: :staleness, expected: :off },
32
-
33
- { argv: ['--error-mode', 'off', 'list'], accessor: :error_mode, expected: :off },
34
- { argv: ['--error-mode', 'o', 'list'], accessor: :error_mode, expected: :off },
35
- { argv: ['--error-mode', 'log', 'list'], accessor: :error_mode, expected: :log },
36
- { argv: ['--error-mode', 'debug', 'list'], accessor: :error_mode, expected: :debug }
37
- ]
38
-
39
- cases.each do |c|
40
- it "parses #{c[:argv].join(' ')}" do
41
- cli = parse!(c[:argv])
42
- expect(cli.config.public_send(c[:accessor])).to eq(c[:expected])
43
- end
44
- end
45
- end
46
-
47
- describe 'rejects invalid values' do
48
- invalid_cases = [
49
- { argv: ['--sort-order', 'asc', 'list'] },
50
- { argv: ['--source', 'x', 'summary', 'lib/foo.rb'] },
51
- { argv: ['-S', 'x', 'list'] },
52
- { argv: ['--staleness', 'x', 'list'] },
53
- { argv: ['--error-mode', 'bad', 'list'] },
54
- { argv: ['--error-mode', 'on', 'list'] },
55
- { argv: ['--error-mode', 'trace', 'list'] }
56
- ]
57
-
58
- invalid_cases.each do |c|
59
- it "exits 1 for #{c[:argv].join(' ')}" do
60
- _out, err, status = run_cli_with_status(*c[:argv])
61
- expect(status).to eq(1)
62
- expect(err).to include('Error:')
63
- expect(err).to include('invalid argument')
64
- end
65
- end
66
- end
67
-
68
- describe 'missing value hints' do
69
- it 'exits 1 when -S is provided without a value' do
70
- _out, err, status = run_cli_with_status('-S', 'list')
71
- expect(status).to eq(1)
72
- expect(err).to include('invalid argument')
73
- end
74
-
75
- it 'exits 1 when --staleness is provided without a value' do
76
- _out, err, status = run_cli_with_status('--staleness', 'list')
77
- expect(status).to eq(1)
78
- expect(err).to include('invalid argument')
79
- end
80
-
81
- it 'exits 1 when --source is provided without a value' do
82
- _out, err, status = run_cli_with_status('--source', 'summary', 'lib/foo.rb')
83
- expect(status).to eq(1)
84
- # Depending on OptParse implementation for required argument, it might say "missing argument"
85
- # But usually it consumes next arg. If 'summary' is consumed as argument for source:
86
- # normalize_source_mode('summary') -> raises InvalidArgument.
87
- expect(err).to include('invalid argument')
88
- end
89
- end
90
- end
@@ -1,184 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::CoverageCLI do
6
- let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
-
8
- it 'shows help and exits 0' do
9
- out, err, status = run_cli_with_status('--help')
10
- expect(status).to eq(0)
11
- expect(out).to match(/Usage:.*cov-loupe/)
12
- expect(out).to include(
13
- 'Repository: https://github.com/keithrbennett/cov-loupe',
14
- 'Subcommands:'
15
- )
16
- expect(out).to match(/Version:.*#{CovLoupe::VERSION}/)
17
- expect(err).to eq('')
18
- end
19
-
20
- shared_examples 'maps error to exit 1 with message' do
21
- before do
22
- # Build a fake model that raises the specified error from the specified method
23
- fake_model = Class.new do
24
- def initialize(*)
25
- end
26
- end
27
- error_to_raise = raised_error
28
- fake_model.define_method(model_method) { |*| raise error_to_raise }
29
- stub_const('CovLoupe::CoverageModel', fake_model)
30
- end
31
-
32
- it 'exits with status 1 and friendly message' do
33
- _out, err, status = run_cli_with_status(*invoke_args)
34
- expect(status).to eq(1)
35
- expect(err).to include(expected_message)
36
- end
37
- end
38
-
39
- context 'when mapping ENOENT' do
40
- let(:model_method) { :summary_for }
41
- let(:raised_error) { Errno::ENOENT.new('No such file or directory @ rb_sysopen - missing.rb') }
42
- let(:invoke_args) { ['--root', root, '--resultset', 'coverage', 'summary', 'lib/missing.rb'] }
43
- let(:expected_message) { 'File error: File not found: lib/missing.rb' }
44
-
45
- it_behaves_like 'maps error to exit 1 with message'
46
- end
47
-
48
- context 'when mapping EACCES' do
49
- let(:model_method) { :raw_for }
50
- let(:raised_error) { Errno::EACCES.new('Permission denied @ rb_sysopen - secret.rb') }
51
- let(:invoke_args) { ['--root', root, '--resultset', 'coverage', 'raw', 'lib/secret.rb'] }
52
- let(:expected_message) { 'Permission denied: lib/secret.rb' }
53
-
54
- it_behaves_like 'maps error to exit 1 with message'
55
- end
56
-
57
- it 'emits detailed stale coverage info and exits 1' do
58
- mock_resultset_with_timestamp(root, VERY_OLD_TIMESTAMP, coverage: {
59
- File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, 1] }
60
- })
61
-
62
- _out, err, status = run_cli_with_status('--root', root, '--resultset', 'coverage',
63
- '--staleness', 'error', 'summary', 'lib/foo.rb')
64
- expect(status).to eq(1)
65
- expect(err).to include('Coverage data stale:')
66
- expect(err).to match(/File\s+- time:/)
67
- expect(err).to match('Coverage\s+- time:')
68
- expect(err).to match(/Delta\s+- file is [+-]?\d+s newer than coverage/)
69
- expect(err).to match('Resultset\s+-')
70
- end
71
-
72
- it 'honors --no-strict-staleness to disable checks' do
73
- mock_resultset_with_timestamp(root, VERY_OLD_TIMESTAMP, coverage: {
74
- File.join(root, 'lib', 'foo.rb') => { 'lines' => [1, 0, 1] }
75
- })
76
-
77
- _out, err, status = run_cli_with_status('--root', root, '--resultset', 'coverage',
78
- '--staleness', 'off', 'summary', 'lib/foo.rb')
79
- expect(status).to eq(0)
80
- expect(err).to eq('')
81
- end
82
-
83
- it 'handles source rendering errors gracefully with fallback message' do
84
- # Test that source rendering with problematic coverage data doesn't crash
85
- # This is a regression test for the "can't convert nil into Integer" crash
86
- # that was previously mentioned in comments
87
- out, err, status = run_cli_with_status(
88
- '--root', root, '--resultset', 'coverage', '--source', 'uncovered', '--context-lines', '2',
89
- '--no-color', 'uncovered', 'lib/foo.rb'
90
- )
91
-
92
- expect(status).to eq(0)
93
- expect(err).to eq('')
94
- expect(out).to match(/File:\s+lib\/foo\.rb/)
95
- expect(out).to include('│') # Table format
96
- expect(out).to show_source_table_or_fallback
97
- end
98
-
99
- it 'renders source with full mode without crashing' do
100
- # Additional regression test for source rendering with full mode
101
- out, err, status = run_cli_with_status(
102
- '--root', root, '--resultset', 'coverage', '--source', 'full', '--no-color',
103
- 'summary', 'lib/foo.rb'
104
- )
105
-
106
- expect(status).to eq(0)
107
- expect(err).to eq('')
108
- expect(out).to include('lib/foo.rb')
109
- expect(out).to include('66.67%')
110
- expect(out).to show_source_table_or_fallback
111
- end
112
-
113
- it 'shows fallback message when source file is unreadable' do
114
- # Test the fallback path when source files can't be read
115
- # Temporarily rename the source file to make it unreadable
116
- foo_path = File.join(root, 'lib', 'foo.rb')
117
- temp_path = "#{foo_path}.hidden"
118
-
119
- begin
120
- File.rename(foo_path, temp_path) if File.exist?(foo_path)
121
-
122
- out, err, status = run_cli_with_status(
123
- '--root', root, '--resultset', 'coverage', '--source', 'full', '--no-color',
124
- 'summary', 'lib/foo.rb'
125
- )
126
-
127
- expect(status).to eq(0)
128
- expect(err).to eq('')
129
- expect(out).to include('lib/foo.rb')
130
- expect(out).to include('66.67%')
131
- expect(out).to include('[source not available]')
132
- ensure
133
- # Restore the file
134
- File.rename(temp_path, foo_path) if File.exist?(temp_path)
135
- end
136
- end
137
-
138
- describe 'invalid option handling' do
139
- it 'suggests subcommand for --subcommand-like option' do
140
- _out, err, status = run_cli_with_status('--summary')
141
- expect(status).to eq(1)
142
- expect(err).to include(
143
- "Error: '--summary' is not a valid option. Did you mean the 'summary' subcommand?"
144
- )
145
- expect(err).to include('Try: cov-loupe summary [args]')
146
- end
147
-
148
- it 'reports invalid enum value for --opt=value' do
149
- _out, err, status = run_cli_with_status('--staleness=bogus', 'list')
150
- expect(status).to eq(1)
151
- expect(err).to include('invalid argument: --staleness=bogus')
152
- end
153
-
154
- it 'reports invalid enum value for --opt value' do
155
- _out, err, status = run_cli_with_status('--staleness', 'bogus', 'list')
156
- expect(status).to eq(1)
157
- expect(err).to include('invalid argument: bogus')
158
- end
159
-
160
- it 'handles generic invalid options' do
161
- _out, err, status = run_cli_with_status('--no-such-option')
162
- expect(status).to eq(1)
163
- expect(err).to include('Error: invalid option: --no-such-option')
164
- end
165
- end
166
-
167
- describe 'subcommand error handling' do
168
- it 'handles generic exceptions from subcommands' do
169
- # Stub the CommandFactory to return a command that raises a StandardError
170
- fake_command = Class.new do
171
- def initialize(_cli) = nil
172
- def execute(_args) = raise(StandardError, 'Unexpected error in subcommand')
173
- end
174
-
175
- allow(CovLoupe::Commands::CommandFactory).to receive(:create)
176
- .and_return(fake_command.new(nil))
177
-
178
- _out, err, status = run_cli_with_status('--root', root, '--resultset', 'coverage', 'summary',
179
- 'lib/foo.rb')
180
- expect(status).to eq(1)
181
- expect(err).to include('Unexpected error in subcommand')
182
- end
183
- end
184
- end
@@ -1,123 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::CoverageCLI, 'format option' do
6
- let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
-
8
- def run_cli(*argv)
9
- cli = CovLoupe::CoverageCLI.new
10
- output = nil
11
- silence_output do |stdout, _stderr|
12
- cli.send(:run, argv)
13
- output = stdout.string
14
- end
15
- output
16
- end
17
-
18
- describe 'format normalization' do
19
- it 'normalizes short format aliases' do
20
- output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'j', 'list')
21
- expect(output).to include('"files":', '"percentage":')
22
- data = JSON.parse(output)
23
- expect(data['files']).to be_an(Array)
24
- end
25
-
26
- it 'normalizes table format' do
27
- output = run_cli('--root', root, '--resultset', 'coverage', '--format', 't', 'list')
28
- expect(output).to include('File', '%') # Table output
29
- expect(output).not_to include('"files"') # Not JSON
30
- end
31
-
32
- it 'supports yaml format' do
33
- output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'y', 'list')
34
- expect(output).to include('---', 'files:', 'file:')
35
- end
36
-
37
- it 'supports awesome_print format' do
38
- output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'a', 'list')
39
- # AwesomePrint output contains colored/formatted structure
40
- expect(output).to match(/:files|"files"/)
41
- end
42
- end
43
-
44
- describe 'option order requirements' do
45
- it 'works with format option before subcommand' do
46
- output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'json', 'list')
47
- data = JSON.parse(output)
48
- expect(data).to have_key('files')
49
- end
50
-
51
- it 'shows helpful error when global option comes after subcommand' do
52
- _out, err, status = run_cli_with_status(
53
- '--root', root, '--resultset', 'coverage', 'list', '--format', 'json'
54
- )
55
- expect(status).to eq(1)
56
- expect(err).to include(
57
- 'Global option(s) must come BEFORE the subcommand',
58
- 'You used: list --format',
59
- 'Correct: --format list',
60
- 'Example:'
61
- )
62
- end
63
- end
64
-
65
- describe 'format with different subcommands' do
66
- it 'works with totals subcommand' do
67
- output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'json', 'totals')
68
- data = JSON.parse(output)
69
- expect(data).to have_key('lines')
70
- expect(data).to have_key('percentage')
71
- end
72
-
73
- it 'works with summary subcommand' do
74
- output = run_cli('--root', root, '--resultset', 'coverage', '--format', 'json',
75
- 'summary', 'lib/foo.rb')
76
- data = JSON.parse(output)
77
- expect(data).to have_key('file')
78
- expect(data).to have_key('summary')
79
- end
80
-
81
- it 'works with version subcommand' do
82
- output = run_cli('--format', 'json', 'version')
83
- data = JSON.parse(output)
84
- expect(data).to have_key('version')
85
- expect(data).to have_key('gem_root')
86
- end
87
- end
88
-
89
- describe 'comprehensive misplaced option detection' do
90
- # Array of test cases: [description, args_array, expected_option_in_error]
91
- [
92
- # Short-form options
93
- ['short -f after list', ['list', '-f', 'json'], '-f'],
94
- ['short -r after totals', ['totals', '-r', '.resultset.json'], '-r'],
95
- ['short -R after list', ['list', '-R', '/tmp'], '-R'],
96
- ['short -o after list', ['list', '-o', 'a'], '-o'],
97
- ['short -s after list', ['list', '-s', 'full'], '-s'],
98
- ['short -S after list', ['list', '-S', 'error'], '-S'],
99
-
100
- # Long-form options
101
- ['--sort-order after list', ['list', '--sort-order', 'ascending'], '--sort-order'],
102
- ['--source after list', ['list', '--source', 'full'], '--source'],
103
- ['--staleness after totals', ['totals', '--staleness', 'error'], '--staleness'],
104
- ['--color after list', ['list', '--color'], '--color'],
105
- ['--no-color after list', ['list', '--no-color'], '--no-color'],
106
- ['--log-file after list', ['list', '--log-file', '/tmp/test.log'], '--log-file'],
107
-
108
- # Different subcommands
109
- ['option after version', ['version', '--format', 'json'], '--format'],
110
- ['option after summary', ['summary', 'lib/foo.rb', '--format', 'json'], '--format'],
111
- ['option after raw', ['raw', 'lib/foo.rb', '-f', 'json'], '-f'],
112
- ['option after detailed', ['detailed', 'lib/foo.rb', '-f', 'json'], '-f'],
113
- ['option after uncovered', ['uncovered', 'lib/foo.rb', '--root', '/tmp'], '--root']
114
- ].each do |desc, args, option|
115
- it "detects #{desc}" do
116
- _out, err, status = run_cli_with_status(*args)
117
- expect(status).to eq(1)
118
- expect(err).to include('Global option(s) must come BEFORE the subcommand')
119
- expect(err).to include(option)
120
- end
121
- end
122
- end
123
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe CovLoupe::CoverageCLI, 'json format options' do
6
- let(:root) { (FIXTURES_DIR / 'project1').to_s }
7
-
8
- def run_cli_output(*argv)
9
- cli = CovLoupe::CoverageCLI.new
10
- output = nil
11
- silence_output do |stdout, _stderr|
12
- cli.send(:run, argv)
13
- output = stdout.string
14
- end
15
- output
16
- end
17
-
18
- describe 'JSON format options' do
19
- it 'produces compact JSON with -f j' do
20
- output = run_cli_output('--root', root, '--resultset', 'coverage', '-f', 'j', 'list')
21
-
22
- expect(output.strip.lines.count).to eq(1)
23
- data = JSON.parse(output)
24
- expect(data['files']).to be_an(Array)
25
- end
26
-
27
- it 'produces pretty JSON with -f pretty-json' do
28
- output = run_cli_output('--root', root, '--resultset', 'coverage', '-f', 'pretty-json',
29
- 'list')
30
- expect(output.strip.lines.count).to be > 1
31
- data = JSON.parse(output)
32
- expect(data['files']).to be_an(Array)
33
- end
34
-
35
- it 'produces pretty JSON with -f pretty_json (underscore variant)' do
36
- output = run_cli_output('--root', root, '--resultset', 'coverage', '-f', 'pretty_json',
37
- 'list')
38
- expect(output.strip.lines.count).to be > 1
39
- data = JSON.parse(output)
40
- expect(data['files']).to be_an(Array)
41
- end
42
-
43
- it 'produces compact JSON with -f json' do
44
- output = run_cli_output('--root', root, '--resultset', 'coverage', '-f', 'json', 'list')
45
- expect(output.strip.lines.count).to eq(1)
46
- data = JSON.parse(output)
47
- expect(data['files']).to be_an(Array)
48
- end
49
- end
50
- end