simplecov-mcp 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/README.md +379 -32
- data/exe/simplecov-mcp +19 -2
- data/lib/simple_cov/mcp.rb +9 -0
- data/lib/simple_cov_mcp/base_tool.rb +54 -0
- data/lib/simple_cov_mcp/cli.rb +390 -0
- data/lib/simple_cov_mcp/error_handler.rb +131 -0
- data/lib/simple_cov_mcp/error_handler_factory.rb +38 -0
- data/lib/simple_cov_mcp/errors.rb +176 -0
- data/lib/simple_cov_mcp/mcp_server.rb +30 -0
- data/lib/simple_cov_mcp/model.rb +104 -0
- data/lib/simple_cov_mcp/staleness_checker.rb +125 -0
- data/lib/simple_cov_mcp/tools/all_files_coverage_tool.rb +63 -0
- data/lib/simple_cov_mcp/tools/coverage_detailed_tool.rb +29 -0
- data/lib/simple_cov_mcp/tools/coverage_raw_tool.rb +29 -0
- data/lib/simple_cov_mcp/tools/coverage_summary_tool.rb +29 -0
- data/lib/simple_cov_mcp/tools/coverage_table_tool.rb +61 -0
- data/lib/simple_cov_mcp/tools/help_tool.rb +133 -0
- data/lib/simple_cov_mcp/tools/uncovered_lines_tool.rb +29 -0
- data/lib/simple_cov_mcp/tools/version_tool.rb +31 -0
- data/lib/simple_cov_mcp/util.rb +122 -0
- data/lib/simple_cov_mcp/version.rb +5 -0
- data/lib/simple_cov_mcp.rb +102 -0
- data/lib/simplecov_mcp.rb +2 -3
- data/spec/all_files_coverage_tool_spec.rb +46 -0
- data/spec/base_tool_spec.rb +58 -0
- data/spec/cli_error_spec.rb +103 -0
- data/spec/cli_json_source_spec.rb +92 -0
- data/spec/cli_source_spec.rb +37 -0
- data/spec/cli_spec.rb +72 -0
- data/spec/cli_table_spec.rb +28 -0
- data/spec/cli_usage_spec.rb +58 -0
- data/spec/coverage_table_tool_spec.rb +64 -0
- data/spec/error_handler_spec.rb +72 -0
- data/spec/errors_stale_spec.rb +49 -0
- data/spec/fixtures/project1/lib/bar.rb +4 -0
- data/spec/fixtures/project1/lib/foo.rb +5 -0
- data/spec/help_tool_spec.rb +47 -0
- data/spec/legacy_shim_spec.rb +13 -0
- data/spec/mcp_server_spec.rb +78 -0
- data/spec/model_staleness_spec.rb +49 -0
- data/spec/simplecov_mcp_model_spec.rb +51 -32
- data/spec/spec_helper.rb +37 -7
- data/spec/staleness_more_spec.rb +39 -0
- data/spec/util_spec.rb +78 -0
- data/spec/version_spec.rb +10 -0
- metadata +56 -13
- data/lib/simplecov/mcp/base_tool.rb +0 -18
- data/lib/simplecov/mcp/cli.rb +0 -98
- data/lib/simplecov/mcp/model.rb +0 -59
- data/lib/simplecov/mcp/tools/all_files_coverage.rb +0 -28
- data/lib/simplecov/mcp/tools/coverage_detailed.rb +0 -22
- data/lib/simplecov/mcp/tools/coverage_raw.rb +0 -22
- data/lib/simplecov/mcp/tools/coverage_summary.rb +0 -22
- data/lib/simplecov/mcp/tools/uncovered_lines.rb +0 -22
- data/lib/simplecov/mcp/util.rb +0 -94
- data/lib/simplecov/mcp/version.rb +0 -8
- data/lib/simplecov/mcp.rb +0 -28
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::CoverageModel do
|
6
|
+
let(:root) { (FIXTURES / 'project1').to_s }
|
7
|
+
|
8
|
+
def with_stubbed_coverage_timestamp(ts) = begin
|
9
|
+
allow(SimpleCovMcp::CovUtil).to receive(:latest_timestamp).and_return(ts)
|
10
|
+
yield
|
11
|
+
end
|
12
|
+
|
13
|
+
it "raises stale error when staleness mode is 'error' and file is newer" do
|
14
|
+
with_stubbed_coverage_timestamp(0) do
|
15
|
+
model = described_class.new(root: root, staleness: 'error')
|
16
|
+
expect {
|
17
|
+
model.summary_for('lib/foo.rb')
|
18
|
+
}.to raise_error(SimpleCovMcp::CoverageDataStaleError, /stale/i)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "does not check staleness when mode is 'off'" do
|
23
|
+
with_stubbed_coverage_timestamp(0) do
|
24
|
+
model = described_class.new(root: root, staleness: 'off')
|
25
|
+
expect { model.summary_for('lib/foo.rb') }.not_to raise_error
|
26
|
+
end
|
27
|
+
end
|
28
|
+
it 'all_files raises project-level stale when any source file is newer than coverage' do
|
29
|
+
with_stubbed_coverage_timestamp(0) do
|
30
|
+
model = described_class.new(root: root, staleness: 'error')
|
31
|
+
expect { model.all_files }.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'all_files detects new files via tracked_globs' do
|
36
|
+
with_stubbed_coverage_timestamp(Time.now.to_i) do
|
37
|
+
tmp = File.join(root, 'lib', 'brand_new_file.rb')
|
38
|
+
begin
|
39
|
+
File.write(tmp, "# new file\n")
|
40
|
+
model = described_class.new(root: root, staleness: 'error')
|
41
|
+
expect {
|
42
|
+
model.all_files(tracked_globs: ['lib/**/*.rb'])
|
43
|
+
}.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
|
44
|
+
ensure
|
45
|
+
File.delete(tmp) if File.exist?(tmp)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -1,54 +1,73 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require 'spec_helper'
|
4
4
|
|
5
|
-
RSpec.describe
|
6
|
-
let(:root) { (FIXTURES /
|
5
|
+
RSpec.describe SimpleCovMcp::CoverageModel do
|
6
|
+
let(:root) { (FIXTURES / 'project1').to_s }
|
7
7
|
subject(:model) { described_class.new(root: root) }
|
8
8
|
|
9
|
-
describe
|
10
|
-
it
|
11
|
-
data = model.raw_for(
|
12
|
-
expect(data[
|
13
|
-
expect(data[
|
9
|
+
describe 'raw_for' do
|
10
|
+
it 'returns absolute file and lines array' do
|
11
|
+
data = model.raw_for('lib/foo.rb')
|
12
|
+
expect(data['file']).to eq(File.expand_path('lib/foo.rb', root))
|
13
|
+
expect(data['lines']).to eq([1, 0, nil, 2])
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
describe
|
18
|
-
it
|
19
|
-
data = model.summary_for(
|
20
|
-
expect(data[
|
21
|
-
expect(data[
|
22
|
-
expect(data[
|
17
|
+
describe 'summary_for' do
|
18
|
+
it 'computes covered/total/pct' do
|
19
|
+
data = model.summary_for('lib/foo.rb')
|
20
|
+
expect(data['summary']['total']).to eq(3)
|
21
|
+
expect(data['summary']['covered']).to eq(2)
|
22
|
+
expect(data['summary']['pct']).to be_within(0.01).of(66.67)
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
describe
|
27
|
-
it
|
28
|
-
data = model.uncovered_for(
|
29
|
-
expect(data[
|
30
|
-
expect(data[
|
26
|
+
describe 'uncovered_for' do
|
27
|
+
it 'lists uncovered executable line numbers' do
|
28
|
+
data = model.uncovered_for('lib/foo.rb')
|
29
|
+
expect(data['uncovered']).to eq([2])
|
30
|
+
expect(data['summary']['total']).to eq(3)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
describe
|
35
|
-
it
|
36
|
-
data = model.detailed_for(
|
37
|
-
expect(data[
|
38
|
-
{ line
|
39
|
-
{ line
|
40
|
-
{ line
|
34
|
+
describe 'detailed_for' do
|
35
|
+
it 'returns per-line details for non-nil lines' do
|
36
|
+
data = model.detailed_for('lib/foo.rb')
|
37
|
+
expect(data['lines']).to eq([
|
38
|
+
{ 'line' => 1, 'hits' => 1, 'covered' => true },
|
39
|
+
{ 'line' => 2, 'hits' => 0, 'covered' => false },
|
40
|
+
{ 'line' => 4, 'hits' => 2, 'covered' => true }
|
41
41
|
])
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
-
describe
|
46
|
-
it
|
45
|
+
describe 'all_files' do
|
46
|
+
it 'sorts ascending by percentage then by file path' do
|
47
47
|
files = model.all_files(sort_order: :ascending)
|
48
|
-
expect(files.first[
|
49
|
-
expect(files.first[
|
50
|
-
expect(files.last[
|
48
|
+
expect(files.first['file']).to eq(File.expand_path('lib/bar.rb', root))
|
49
|
+
expect(files.first['percentage']).to be_within(0.01).of(33.33)
|
50
|
+
expect(files.last['file']).to eq(File.expand_path('lib/foo.rb', root))
|
51
51
|
end
|
52
52
|
end
|
53
|
-
end
|
54
53
|
|
54
|
+
describe 'resultset directory handling' do
|
55
|
+
it 'accepts a directory containing .resultset.json' do
|
56
|
+
model = described_class.new(root: root, resultset: 'coverage')
|
57
|
+
data = model.summary_for('lib/foo.rb')
|
58
|
+
expect(data['summary']['total']).to eq(3)
|
59
|
+
expect(data['summary']['covered']).to eq(2)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'uses SIMPLECOV_RESULTSET when it is a directory' do
|
63
|
+
begin
|
64
|
+
ENV['SIMPLECOV_RESULTSET'] = 'coverage'
|
65
|
+
model = described_class.new(root: root)
|
66
|
+
data = model.summary_for('lib/foo.rb')
|
67
|
+
expect(data['summary']['covered']).to eq(2)
|
68
|
+
ensure
|
69
|
+
ENV.delete('SIMPLECOV_RESULTSET')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,19 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
# Enable SimpleCov for this project (coverage output in ./coverage)
|
4
|
+
begin
|
5
|
+
require 'simplecov'
|
6
|
+
SimpleCov.start do
|
7
|
+
enable_coverage :branch if SimpleCov.respond_to?(:enable_coverage)
|
8
|
+
add_filter %r{^/spec/}
|
9
|
+
track_files 'lib/**/*.rb'
|
10
|
+
end
|
11
|
+
rescue LoadError
|
12
|
+
warn 'SimpleCov not available; skipping coverage'
|
13
|
+
end
|
14
|
+
|
15
|
+
ENV.delete('SIMPLECOV_RESULTSET')
|
4
16
|
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
17
|
+
require 'rspec'
|
18
|
+
require 'pathname'
|
19
|
+
require 'json'
|
8
20
|
|
9
|
-
require
|
21
|
+
require 'simple_cov_mcp'
|
10
22
|
|
11
|
-
FIXTURES = Pathname.new(File.expand_path(
|
23
|
+
FIXTURES = Pathname.new(File.expand_path('fixtures', __dir__))
|
12
24
|
|
13
25
|
RSpec.configure do |config|
|
14
|
-
config.example_status_persistence_file_path =
|
26
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
15
27
|
config.disable_monkey_patching!
|
16
28
|
config.order = :random
|
17
29
|
Kernel.srand config.seed
|
18
30
|
end
|
19
31
|
|
32
|
+
# Shared test helpers
|
33
|
+
module TestIOHelpers
|
34
|
+
# Suppress stdout/stderr within the given block, yielding the StringIOs
|
35
|
+
def silence_output
|
36
|
+
original_stdout = $stdout
|
37
|
+
original_stderr = $stderr
|
38
|
+
$stdout = StringIO.new
|
39
|
+
$stderr = StringIO.new
|
40
|
+
yield $stdout, $stderr
|
41
|
+
ensure
|
42
|
+
$stdout = original_stdout
|
43
|
+
$stderr = original_stderr
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
RSpec.configure do |config|
|
48
|
+
config.include TestIOHelpers
|
49
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'Additional staleness cases' do
|
6
|
+
let(:root) { (FIXTURES / 'project1').to_s }
|
7
|
+
|
8
|
+
describe SimpleCovMcp::CoverageModel do
|
9
|
+
it 'raises file-level stale when source and coverage lengths differ' do
|
10
|
+
# Ensure time is not the triggering factor
|
11
|
+
allow(SimpleCovMcp::CovUtil).to receive(:latest_timestamp).and_return(Time.now.to_i)
|
12
|
+
model = SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage', staleness: 'error')
|
13
|
+
# bar.rb has 3 coverage entries but 4 source lines in fixtures
|
14
|
+
expect {
|
15
|
+
model.summary_for('lib/bar.rb')
|
16
|
+
}.to raise_error(SimpleCovMcp::CoverageDataStaleError, /stale/i)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe SimpleCovMcp::StalenessChecker do
|
21
|
+
it 'flags deleted files present only in coverage' do
|
22
|
+
checker = described_class.new(root: root, resultset: 'coverage', mode: 'error')
|
23
|
+
coverage_map = {
|
24
|
+
File.join(root, 'lib', 'does_not_exist_anymore.rb') => { 'lines' => [1] }
|
25
|
+
}
|
26
|
+
expect {
|
27
|
+
checker.check_project!(coverage_map)
|
28
|
+
}.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'does not raise for empty tracked_globs when nothing else is stale' do
|
32
|
+
allow(SimpleCovMcp::CovUtil).to receive(:latest_timestamp).and_return(Time.now.to_i)
|
33
|
+
checker = described_class.new(root: root, resultset: 'coverage', mode: 'error', tracked_globs: [])
|
34
|
+
expect {
|
35
|
+
checker.check_project!({})
|
36
|
+
}.not_to raise_error
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/spec/util_spec.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SimpleCovMcp::CovUtil do
|
6
|
+
let(:root) { (FIXTURES / 'project1').to_s }
|
7
|
+
let(:resultset_file) { File.join(root, 'coverage', '.resultset.json') }
|
8
|
+
|
9
|
+
it 'latest_timestamp returns integer from fixture' do
|
10
|
+
ts = described_class.latest_timestamp(root, resultset: 'coverage')
|
11
|
+
expect(ts).to be_a(Integer)
|
12
|
+
expect(ts).to eq(1_720_000_000)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'find_resultset honors SIMPLECOV_RESULTSET file path' do
|
16
|
+
begin
|
17
|
+
ENV['SIMPLECOV_RESULTSET'] = resultset_file
|
18
|
+
path = described_class.find_resultset(root)
|
19
|
+
expect(path).to eq(File.absolute_path(resultset_file, root))
|
20
|
+
ensure
|
21
|
+
ENV.delete('SIMPLECOV_RESULTSET')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'lookup_lines supports cwd-stripping and basename fallbacks' do
|
26
|
+
lines = [1, 0]
|
27
|
+
|
28
|
+
# Exact key
|
29
|
+
cov = { '/abs/path/foo.rb' => { 'lines' => lines } }
|
30
|
+
expect(described_class.lookup_lines(cov, '/abs/path/foo.rb')).to eq(lines)
|
31
|
+
|
32
|
+
# CWD strip fallback
|
33
|
+
begin
|
34
|
+
allow(Dir).to receive(:pwd).and_return('/cwd')
|
35
|
+
cov = { 'sub/foo.rb' => { 'lines' => lines } }
|
36
|
+
expect(described_class.lookup_lines(cov, '/cwd/sub/foo.rb')).to eq(lines)
|
37
|
+
ensure
|
38
|
+
# no-op
|
39
|
+
end
|
40
|
+
|
41
|
+
# Basename fallback
|
42
|
+
cov = { '/some/where/else/foo.rb' => { 'lines' => lines } }
|
43
|
+
expect(described_class.lookup_lines(cov, '/another/place/foo.rb')).to eq(lines)
|
44
|
+
|
45
|
+
# Missing raises a helpful string error
|
46
|
+
cov = {}
|
47
|
+
expect {
|
48
|
+
described_class.lookup_lines(cov, '/nowhere/foo.rb')
|
49
|
+
}.to raise_error(RuntimeError, /No coverage entry found/)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'summary handles edge cases and coercion' do
|
53
|
+
expect(described_class.summary([])).to include('pct' => 100.0, 'total' => 0, 'covered' => 0)
|
54
|
+
expect(described_class.summary([nil, nil])).to include('pct' => 100.0, 'total' => 0, 'covered' => 0)
|
55
|
+
expect(described_class.summary(['1', '0', nil])).to include('pct' => 50.0, 'total' => 2, 'covered' => 1)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'uncovered and detailed ignore nils' do
|
59
|
+
arr = [1, 0, nil, 2]
|
60
|
+
expect(described_class.uncovered(arr)).to eq([2])
|
61
|
+
expect(described_class.detailed(arr)).to eq([
|
62
|
+
{ 'line' => 1, 'hits' => 1, 'covered' => true },
|
63
|
+
{ 'line' => 2, 'hits' => 0, 'covered' => false },
|
64
|
+
{ 'line' => 4, 'hits' => 2, 'covered' => true }
|
65
|
+
])
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'load_latest_coverage raises CoverageDataError on invalid JSON via model' do
|
69
|
+
Dir.mktmpdir do |dir|
|
70
|
+
bad = File.join(dir, '.resultset.json')
|
71
|
+
File.write(bad, '{not-json')
|
72
|
+
expect {
|
73
|
+
SimpleCovMcp::CoverageModel.new(root: root, resultset: dir)
|
74
|
+
}.to raise_error(SimpleCovMcp::CoverageDataError, /Invalid coverage data format/)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'Version constant' do
|
6
|
+
it 'exposes a semver-like version string' do
|
7
|
+
expect(SimpleCovMcp::VERSION).to be_a(String)
|
8
|
+
expect(SimpleCovMcp::VERSION).to match(/\A\d+\.\d+\.\d+(?:[.-][0-9A-Za-z]+)?\z/)
|
9
|
+
end
|
10
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simplecov-mcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Keith R. Bennett
|
@@ -57,6 +57,20 @@ dependencies:
|
|
57
57
|
- - "~>"
|
58
58
|
- !ruby/object:Gem::Version
|
59
59
|
version: '3.0'
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: simplecov
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0.21'
|
67
|
+
type: :development
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0.21'
|
60
74
|
description: Provides an MCP (Model Context Protocol) server and a CLI to inspect
|
61
75
|
SimpleCov coverage, including per-file summaries and uncovered lines.
|
62
76
|
email:
|
@@ -68,20 +82,49 @@ extra_rdoc_files: []
|
|
68
82
|
files:
|
69
83
|
- README.md
|
70
84
|
- exe/simplecov-mcp
|
71
|
-
- lib/
|
72
|
-
- lib/
|
73
|
-
- lib/
|
74
|
-
- lib/
|
75
|
-
- lib/
|
76
|
-
- lib/
|
77
|
-
- lib/
|
78
|
-
- lib/
|
79
|
-
- lib/
|
80
|
-
- lib/
|
81
|
-
- lib/
|
85
|
+
- lib/simple_cov/mcp.rb
|
86
|
+
- lib/simple_cov_mcp.rb
|
87
|
+
- lib/simple_cov_mcp/base_tool.rb
|
88
|
+
- lib/simple_cov_mcp/cli.rb
|
89
|
+
- lib/simple_cov_mcp/error_handler.rb
|
90
|
+
- lib/simple_cov_mcp/error_handler_factory.rb
|
91
|
+
- lib/simple_cov_mcp/errors.rb
|
92
|
+
- lib/simple_cov_mcp/mcp_server.rb
|
93
|
+
- lib/simple_cov_mcp/model.rb
|
94
|
+
- lib/simple_cov_mcp/staleness_checker.rb
|
95
|
+
- lib/simple_cov_mcp/tools/all_files_coverage_tool.rb
|
96
|
+
- lib/simple_cov_mcp/tools/coverage_detailed_tool.rb
|
97
|
+
- lib/simple_cov_mcp/tools/coverage_raw_tool.rb
|
98
|
+
- lib/simple_cov_mcp/tools/coverage_summary_tool.rb
|
99
|
+
- lib/simple_cov_mcp/tools/coverage_table_tool.rb
|
100
|
+
- lib/simple_cov_mcp/tools/help_tool.rb
|
101
|
+
- lib/simple_cov_mcp/tools/uncovered_lines_tool.rb
|
102
|
+
- lib/simple_cov_mcp/tools/version_tool.rb
|
103
|
+
- lib/simple_cov_mcp/util.rb
|
104
|
+
- lib/simple_cov_mcp/version.rb
|
82
105
|
- lib/simplecov_mcp.rb
|
106
|
+
- spec/all_files_coverage_tool_spec.rb
|
107
|
+
- spec/base_tool_spec.rb
|
108
|
+
- spec/cli_error_spec.rb
|
109
|
+
- spec/cli_json_source_spec.rb
|
110
|
+
- spec/cli_source_spec.rb
|
111
|
+
- spec/cli_spec.rb
|
112
|
+
- spec/cli_table_spec.rb
|
113
|
+
- spec/cli_usage_spec.rb
|
114
|
+
- spec/coverage_table_tool_spec.rb
|
115
|
+
- spec/error_handler_spec.rb
|
116
|
+
- spec/errors_stale_spec.rb
|
117
|
+
- spec/fixtures/project1/lib/bar.rb
|
118
|
+
- spec/fixtures/project1/lib/foo.rb
|
119
|
+
- spec/help_tool_spec.rb
|
120
|
+
- spec/legacy_shim_spec.rb
|
121
|
+
- spec/mcp_server_spec.rb
|
122
|
+
- spec/model_staleness_spec.rb
|
83
123
|
- spec/simplecov_mcp_model_spec.rb
|
84
124
|
- spec/spec_helper.rb
|
125
|
+
- spec/staleness_more_spec.rb
|
126
|
+
- spec/util_spec.rb
|
127
|
+
- spec/version_spec.rb
|
85
128
|
homepage: https://github.com/keithrbennett/simplecov-mcp
|
86
129
|
licenses:
|
87
130
|
- MIT
|
@@ -100,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
143
|
- !ruby/object:Gem::Version
|
101
144
|
version: '0'
|
102
145
|
requirements: []
|
103
|
-
rubygems_version: 3.7.
|
146
|
+
rubygems_version: 3.7.2
|
104
147
|
specification_version: 4
|
105
148
|
summary: MCP server + CLI for SimpleCov coverage data
|
106
149
|
test_files: []
|
@@ -1,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Simplecov
|
4
|
-
module Mcp
|
5
|
-
class BaseTool < ::MCP::Tool
|
6
|
-
INPUT_SCHEMA = {
|
7
|
-
type: "object",
|
8
|
-
properties: {
|
9
|
-
path: { type: "string", description: "Absolute or project-relative file path" },
|
10
|
-
root: { type: "string", description: "Project root for resolution", default: "." }
|
11
|
-
},
|
12
|
-
required: ["path"]
|
13
|
-
}
|
14
|
-
def self.input_schema_def = INPUT_SCHEMA
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
data/lib/simplecov/mcp/cli.rb
DELETED
@@ -1,98 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Simplecov
|
4
|
-
module Mcp
|
5
|
-
class CoverageCLI
|
6
|
-
def initialize
|
7
|
-
@root = "."
|
8
|
-
end
|
9
|
-
|
10
|
-
def run(argv)
|
11
|
-
if force_cli?(argv)
|
12
|
-
show_default_report
|
13
|
-
else
|
14
|
-
run_mcp_server
|
15
|
-
end
|
16
|
-
rescue => e
|
17
|
-
CovUtil.log("CLI fatal error: #{e.class}: #{e.message}\n#{e.backtrace&.join("\n")}")
|
18
|
-
raise
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def force_cli?(argv)
|
24
|
-
return true if ENV["COVERAGE_MCP_CLI"] == "1"
|
25
|
-
return true if argv.include?("--cli") || argv.include?("--report")
|
26
|
-
# If interactive TTY, prefer CLI; else (e.g., pipes), run MCP.
|
27
|
-
return STDIN.tty?
|
28
|
-
end
|
29
|
-
|
30
|
-
def show_default_report
|
31
|
-
model = CoverageModel.new(root: @root)
|
32
|
-
file_summaries = model.all_files(sort_order: :ascending).map do |row|
|
33
|
-
row.dup.tap do |h|
|
34
|
-
h[:file] = Pathname.new(h[:file]).relative_path_from(Pathname.new(Dir.pwd)).to_s
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
# Format as table with box-style borders
|
39
|
-
max_file_length = file_summaries.map { |f| f[:file].length }.max.to_i
|
40
|
-
max_file_length = [max_file_length, "File".length].max
|
41
|
-
|
42
|
-
# Calculate maximum numeric values for proper column widths
|
43
|
-
max_covered = file_summaries.map { |f| f[:covered].to_s.length }.max
|
44
|
-
max_total = file_summaries.map { |f| f[:total].to_s.length }.max
|
45
|
-
|
46
|
-
# Define column widths
|
47
|
-
file_width = max_file_length + 2 # Extra padding
|
48
|
-
pct_width = 8
|
49
|
-
covered_width = [max_covered, "Covered".length].max + 2
|
50
|
-
total_width = [max_total, "Total".length].max + 2
|
51
|
-
|
52
|
-
# Horizontal line for each column span
|
53
|
-
h_line = ->(col_width) { '─' * (col_width + 2) }
|
54
|
-
|
55
|
-
# Border line lambda
|
56
|
-
border_line = ->(left, middle, right) {
|
57
|
-
left + h_line.(file_width) +
|
58
|
-
middle + h_line.(pct_width) +
|
59
|
-
middle + h_line.(covered_width) +
|
60
|
-
middle + h_line.(total_width) +
|
61
|
-
right
|
62
|
-
}
|
63
|
-
|
64
|
-
# Top border
|
65
|
-
puts border_line.call("┌", "┬", "┐")
|
66
|
-
|
67
|
-
# Header row
|
68
|
-
printf "│ %-#{file_width}s │ %#{pct_width}s │ %#{covered_width}s │ %#{total_width}s │\n",
|
69
|
-
"File", " %", "Covered", "Total"
|
70
|
-
|
71
|
-
# Header separator
|
72
|
-
puts border_line.call("├", "┼", "┤")
|
73
|
-
|
74
|
-
# Data rows
|
75
|
-
file_summaries.each do |file_data|
|
76
|
-
printf "│ %-#{file_width}s │ %#{pct_width - 1}.2f%% │ %#{covered_width}d │ %#{total_width}d │\n",
|
77
|
-
file_data[:file],
|
78
|
-
file_data[:percentage],
|
79
|
-
file_data[:covered],
|
80
|
-
file_data[:total]
|
81
|
-
end
|
82
|
-
|
83
|
-
# Bottom border
|
84
|
-
puts border_line.call("└", "┴", "┘")
|
85
|
-
end
|
86
|
-
|
87
|
-
def run_mcp_server
|
88
|
-
server = ::MCP::Server.new(
|
89
|
-
name: "ruby_coverage_server",
|
90
|
-
version: Simplecov::Mcp::VERSION,
|
91
|
-
tools: [CoverageRaw, CoverageSummary, UncoveredLines, CoverageDetailed, AllFilesCoverage]
|
92
|
-
)
|
93
|
-
::MCP::Server::Transports::StdioTransport.new(server).open
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
data/lib/simplecov/mcp/model.rb
DELETED
@@ -1,59 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Simplecov
|
4
|
-
module Mcp
|
5
|
-
class CoverageModel
|
6
|
-
def initialize(root: ".")
|
7
|
-
@root = File.absolute_path(root || ".")
|
8
|
-
@cov = CovUtil.load_latest_coverage(@root)
|
9
|
-
end
|
10
|
-
|
11
|
-
# Returns { file: <abs>, lines: [hits|nil,...] }
|
12
|
-
def raw_for(path)
|
13
|
-
abs, arr = resolve(path)
|
14
|
-
{ file: abs, lines: arr }
|
15
|
-
end
|
16
|
-
|
17
|
-
# Returns { file: <abs>, summary: {"covered"=>, "total"=>, "pct"=>} }
|
18
|
-
def summary_for(path)
|
19
|
-
abs, arr = resolve(path)
|
20
|
-
{ file: abs, summary: CovUtil.summary(arr) }
|
21
|
-
end
|
22
|
-
|
23
|
-
# Returns { file: <abs>, uncovered: [line,...], summary: {...} }
|
24
|
-
def uncovered_for(path)
|
25
|
-
abs, arr = resolve(path)
|
26
|
-
{ file: abs, uncovered: CovUtil.uncovered(arr), summary: CovUtil.summary(arr) }
|
27
|
-
end
|
28
|
-
|
29
|
-
# Returns { file: <abs>, lines: [{line:,hits:,covered:},...], summary: {...} }
|
30
|
-
def detailed_for(path)
|
31
|
-
abs, arr = resolve(path)
|
32
|
-
{ file: abs, lines: CovUtil.detailed(arr), summary: CovUtil.summary(arr) }
|
33
|
-
end
|
34
|
-
|
35
|
-
# Returns [ { file:, covered:, total:, percentage: }, ... ]
|
36
|
-
def all_files(sort_order: :ascending)
|
37
|
-
rows = @cov.map do |abs_path, data|
|
38
|
-
next unless data["lines"].is_a?(Array)
|
39
|
-
s = CovUtil.summary(data["lines"])
|
40
|
-
{ file: abs_path, covered: s["covered"], total: s["total"], percentage: s["pct"] }
|
41
|
-
end.compact
|
42
|
-
|
43
|
-
rows.sort! do |a, b|
|
44
|
-
pct_cmp = (sort_order.to_s == "descending") ? (b[:percentage] <=> a[:percentage]) : (a[:percentage] <=> b[:percentage])
|
45
|
-
pct_cmp == 0 ? (a[:file] <=> b[:file]) : pct_cmp
|
46
|
-
end
|
47
|
-
rows
|
48
|
-
end
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
def resolve(path)
|
53
|
-
abs = File.absolute_path(path, @root)
|
54
|
-
[abs, CovUtil.lookup_lines(@cov, abs)]
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Simplecov
|
4
|
-
module Mcp
|
5
|
-
class AllFilesCoverage < ::MCP::Tool
|
6
|
-
description "Return coverage percentage for all files in the project"
|
7
|
-
input_schema(
|
8
|
-
type: "object",
|
9
|
-
properties: {
|
10
|
-
root: { type: "string", description: "Project root for resolution", default: "." },
|
11
|
-
sort_order: { type: "string", description: "Sort order for coverage percentage: ascending or descending", default: "ascending", enum: ["ascending", "descending"] }
|
12
|
-
}
|
13
|
-
)
|
14
|
-
class << self
|
15
|
-
def call(root: ".", sort_order: "ascending", server_context:)
|
16
|
-
model = CoverageModel.new(root: root)
|
17
|
-
files = model.all_files(sort_order: sort_order)
|
18
|
-
::MCP::Tool::Response.new([{ type: "json", json: { files: files } }],
|
19
|
-
meta: { mimeType: "application/json" })
|
20
|
-
rescue => e
|
21
|
-
CovUtil.log("AllFilesCoverage error: #{e.class}: #{e.message}")
|
22
|
-
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.class}: #{e.message}" }])
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
@@ -1,22 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Simplecov
|
4
|
-
module Mcp
|
5
|
-
class CoverageDetailed < BaseTool
|
6
|
-
description "Verbose per-line objects [{line,hits,covered}] (token-heavy)"
|
7
|
-
input_schema(**input_schema_def)
|
8
|
-
class << self
|
9
|
-
def call(path:, root: ".", server_context:)
|
10
|
-
model = CoverageModel.new(root: root)
|
11
|
-
data = model.detailed_for(path)
|
12
|
-
::MCP::Tool::Response.new([{ type: "json", json: data }],
|
13
|
-
meta: { mimeType: "application/json" })
|
14
|
-
rescue => e
|
15
|
-
CovUtil.log("CoverageDetailed error: #{e.class}: #{e.message}")
|
16
|
-
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.class}: #{e.message}" }])
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|