Skip to content

Commit 590da07

Browse files
committed
Pin schema canonical to a versioned path and reference it from the payload
Move the canonical schema to schemas/coverage-v1.0.schema.json with a versioned $id, so each version is immutable. Keep schemas/coverage.schema.json as a convenience alias for "the latest" that mirrors the canonical except for $id, title, and description. A new spec asserts the two stay structurally identical so the alias cannot drift. Add a top-level $schema field to every coverage.json holding the versioned canonical URL, so each emitted document is self-describing and consumers can resolve the exact contract without out-of-band knowledge. meta.schema_version stays as the human-readable companion.
1 parent 1aaf166 commit 590da07

10 files changed

Lines changed: 304 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Unreleased
2525
* `# :nocov:` toggle comments (and the configurable `SimpleCov.nocov_token` / `SimpleCov.skip_token`) are deprecated in favor of the new `# simplecov:disable` / `# simplecov:enable` directives. Each file that still uses `# :nocov:` emits a one-time deprecation warning to stderr at load time pointing at the recommended replacement, and any call to `SimpleCov.nocov_token` or `SimpleCov.skip_token` (getter or setter) likewise warns. The directive will be removed in a future release.
2626

2727
## Enhancements
28-
* JSON formatter: `coverage.json` now carries a `meta.schema_version` field (`"major.minor"`, currently `"1.0"`) describing which version of the schema it conforms to. A formal JSON Schema (draft-07) is published at `schemas/coverage.schema.json` so downstream tools can validate inputs, generate types, or pin to a known shape. The schema version is independent of the gem version: additive changes bump minor, removals or shape changes bump major.
28+
* JSON formatter: `coverage.json` now carries a top-level `$schema` field holding the URL of the versioned canonical JSON Schema the document conforms to, plus a human-readable `meta.schema_version` (`"major.minor"`, currently `"1.0"`). The versioned canonical lives at `schemas/coverage-v1.0.schema.json` and is immutable per version, an unversioned convenience alias at `schemas/coverage.schema.json` always tracks the latest. Downstream tools can validate inputs, generate types, or pin to a known shape, and the document-level `$schema` makes each payload self-describing. The schema version is independent of the gem version: additive changes bump minor, removals or shape changes bump major and ship as a new `schemas/coverage-vX.0.schema.json` file so prior-version consumers stay valid.
2929
* Added `SimpleCov::ParallelAdapters` — a pluggable adapter interface for parallel test runners. SimpleCov's coordination with parallel test runners (deciding which worker does final-result work, waiting for siblings, knowing how many resultsets to expect) now routes through an adapter chain rather than hard-coding the `parallel_tests` gem's API. Two adapters ship: `ParallelTestsAdapter` wraps the historical grosser/parallel_tests gem (precise, gem-API-based); `GenericAdapter` handles any runner that follows the `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` env-var convention without shipping a Ruby API. The practical impact: **parallel_rspec (and any similar env-var-only runner) now works out of the box** — previously every worker thought it was the "final" one and they clobbered each other's resultsets. Custom runners can register their own adapter via `SimpleCov::ParallelAdapters.register MyAdapter`, where `MyAdapter` subclasses `SimpleCov::ParallelAdapters::Base` and overrides the four contract methods (`active?`, `first_worker?`, `wait_for_siblings`, `expected_worker_count`). See #1065.
3030
* Added `SimpleCov.ignore_branches` for opting out of synthetic `:else` branches that Ruby's `Coverage` library reports for constructs with no literal `else` keyword — exhaustive `case/in` pattern matches, `case/when` without `else`, `||=` / `&&=`, and `if` / `unless` without `else`. Variadic; only `:implicit_else` is supported today, with room for future synthetic branch types. Calling it without (or before) `enable_coverage :branch` is harmless — the setting is stored and applies once branch coverage is enabled. Explicit `else` arms still count. See #1033.
3131
* Added `SimpleCov.cover` for declaring a positive coverage scope (the long-requested allowlist counterpart to `add_filter`). Accepts string globs, Regexps, blocks, or arrays of those; multiple calls union. When any `cover` matcher is configured the report drops every source file that doesn't match at least one of them, and string-glob matchers also expand on disk so files that exist but were never required during the run still appear in the report (at 0% coverage). Resolves the long-standing requests in #696 and #869. The companion `SimpleCov.no_default_skips` opts out of the filters that `SimpleCov.start` installs (hidden files, `vendor/bundle/`, test directories) so users who want to opt out wholesale don't have to call `clear_filters` themselves.

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,17 +1153,23 @@ SimpleCov.formatter = SimpleCov::Formatter::JSONFormatter
11531153
11541154
### `coverage.json` schema
11551155

1156-
The `coverage.json` payload is treated as a public contract. Every emitted file carries a `meta.schema_version` (`"major.minor"`) describing which version of the schema it conforms to. A JSON Schema (draft-07) lives in the repo at [`schemas/coverage.schema.json`](schemas/coverage.schema.json); downstream tools can use it to validate inputs, generate types, or pin themselves to a known shape.
1156+
The `coverage.json` payload is treated as a public contract. Every emitted file carries:
1157+
1158+
- a top-level `$schema` URL pointing at the exact versioned schema the document conforms to (machine-resolvable, so consumers like IDEs and validators auto-fetch the right contract),
1159+
- a `meta.schema_version` (`"major.minor"`) for human-readable inspection.
1160+
1161+
JSON Schema (draft-07) is published in the repo. The **versioned canonical** lives at [`schemas/coverage-v1.0.schema.json`](schemas/coverage-v1.0.schema.json) and is immutable per version: long-lived integrations should pin to it. A convenience alias at [`schemas/coverage.schema.json`](schemas/coverage.schema.json) always tracks the latest, and may shift when a new SimpleCov release bumps the schema.
11571162

11581163
The schema version is independent of the gem version:
11591164

11601165
- Additive changes (new fields) bump the **minor** segment. Existing consumers keep working.
1161-
- Removals or shape changes bump the **major** segment.
1166+
- Removals or shape changes bump the **major** segment, and ship as a new `schemas/coverage-vX.0.schema.json` file so v1.x consumers stay valid.
11621167

11631168
The current version is **1.0**. Top-level structure:
11641169

11651170
```jsonc
11661171
{
1172+
"$schema": "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v1.0.schema.json",
11671173
"meta": { /* schema_version, simplecov_version, command_name, project_name, timestamp, root, branch_coverage, method_coverage */ },
11681174
"total": { /* aggregate stats for lines (and branches / methods when enabled) */ },
11691175
"coverage": { "<project-relative path>": { /* per-file lines, source, branches, methods, etc. */ } },

lib/simplecov/formatter/json_formatter/result_hash_formatter.rb

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,29 @@ class JSONFormatter
1010
# Builds the hash that JSONFormatter serializes to coverage.json:
1111
# meta, per-file coverage data, group totals, and aggregate stats.
1212
class ResultHashFormatter
13+
# Bump SCHEMA_VERSION (and SCHEMA_URL) when the JSON shape
14+
# changes. Additive changes bump the minor segment, removals or
15+
# shape changes bump the major segment. The versioned file at
16+
# schemas/coverage-vX.Y.schema.json is the canonical artifact
17+
# consumers should pin to, schemas/coverage.schema.json is a
18+
# convenience alias that always tracks the latest. See the
19+
# `coverage.json` schema section of the README for the rationale.
20+
SCHEMA_VERSION = "1.0"
21+
SCHEMA_URL = "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v#{SCHEMA_VERSION}.schema.json".freeze
22+
private_constant :SCHEMA_VERSION, :SCHEMA_URL
23+
1324
def initialize(result)
1425
@result = result
1526
end
1627

1728
def format
1829
{
19-
meta: format_meta,
20-
total: format_coverage_statistics(@result.coverage_statistics),
21-
coverage: format_files,
22-
groups: format_groups,
23-
errors: ErrorsFormatter.new(@result).call
30+
:$schema => SCHEMA_URL,
31+
:meta => format_meta,
32+
:total => format_coverage_statistics(@result.coverage_statistics),
33+
:coverage => format_files,
34+
:groups => format_groups,
35+
:errors => ErrorsFormatter.new(@result).call
2436
}
2537
end
2638

@@ -37,13 +49,6 @@ def format_groups
3749
end
3850
end
3951

40-
# Bump SCHEMA_VERSION when the JSON shape changes. Additive
41-
# changes bump the minor segment; removals or shape changes bump
42-
# major. See schemas/coverage.schema.json for the contract this
43-
# version describes.
44-
SCHEMA_VERSION = "1.0"
45-
private_constant :SCHEMA_VERSION
46-
4752
def format_meta
4853
{
4954
schema_version: SCHEMA_VERSION,

schemas/coverage-v1.0.schema.json

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$id": "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v1.0.schema.json",
4+
"title": "SimpleCov coverage.json",
5+
"description": "Schema for the coverage.json file emitted by SimpleCov's JSONFormatter. Versioned independently of the gem. Non-breaking additions bump the minor segment of schema_version, breaking changes bump the major segment. The versioned file at schemas/coverage-vX.Y.schema.json is the canonical artifact for that version, schemas/coverage.schema.json is a convenience alias for the latest.",
6+
"type": "object",
7+
"required": ["$schema", "meta", "total", "coverage", "groups", "errors"],
8+
"additionalProperties": false,
9+
"properties": {
10+
"$schema": {
11+
"type": "string",
12+
"format": "uri",
13+
"description": "URL of the JSON Schema this document conforms to. Pinned to the versioned canonical URL so documents stay validatable against the exact contract they were emitted under, even after the schema evolves."
14+
},
15+
"meta": {
16+
"type": "object",
17+
"required": [
18+
"schema_version",
19+
"simplecov_version",
20+
"command_name",
21+
"project_name",
22+
"timestamp",
23+
"root",
24+
"branch_coverage",
25+
"method_coverage"
26+
],
27+
"additionalProperties": false,
28+
"properties": {
29+
"schema_version": {
30+
"type": "string",
31+
"const": "1.0",
32+
"description": "Schema major.minor. Additive changes bump minor; breaking changes bump major. Update this `const` whenever the schema version is bumped so documents claiming a different version don't quietly validate against this contract."
33+
},
34+
"simplecov_version": {
35+
"type": "string",
36+
"description": "The version of the SimpleCov gem that produced this file."
37+
},
38+
"command_name": {"type": "string"},
39+
"project_name": {"type": "string"},
40+
"timestamp": {
41+
"type": "string",
42+
"format": "date-time",
43+
"description": "ISO 8601 timestamp with millisecond precision."
44+
},
45+
"root": {
46+
"type": "string",
47+
"description": "Absolute path to SimpleCov.root at the time of write."
48+
},
49+
"branch_coverage": {"type": "boolean"},
50+
"method_coverage": {"type": "boolean"}
51+
}
52+
},
53+
"total": {"$ref": "#/definitions/totals"},
54+
"coverage": {
55+
"type": "object",
56+
"description": "Map of project-relative file paths to per-file coverage data.",
57+
"additionalProperties": {"$ref": "#/definitions/source_file"}
58+
},
59+
"groups": {
60+
"type": "object",
61+
"description": "Map of group names to per-group totals plus the list of files in the group.",
62+
"additionalProperties": {"$ref": "#/definitions/group"}
63+
},
64+
"errors": {
65+
"type": "object",
66+
"description": "Threshold violations from minimum_coverage, minimum_coverage_by_file, minimum_coverage_by_group, and maximum_coverage_drop. Empty object when no thresholds were violated.",
67+
"additionalProperties": false,
68+
"properties": {
69+
"minimum_coverage": {
70+
"type": "object",
71+
"description": "Keyed by criterion: lines, branches, methods.",
72+
"additionalProperties": {"$ref": "#/definitions/expected_actual"}
73+
},
74+
"minimum_coverage_by_file": {
75+
"type": "object",
76+
"description": "Keyed by criterion, then by project-relative filename.",
77+
"additionalProperties": {
78+
"type": "object",
79+
"additionalProperties": {"$ref": "#/definitions/expected_actual"}
80+
}
81+
},
82+
"minimum_coverage_by_group": {
83+
"type": "object",
84+
"description": "Keyed by group name, then by criterion.",
85+
"additionalProperties": {
86+
"type": "object",
87+
"additionalProperties": {"$ref": "#/definitions/expected_actual"}
88+
}
89+
},
90+
"maximum_coverage_drop": {
91+
"type": "object",
92+
"description": "Keyed by criterion: lines, branches, methods.",
93+
"additionalProperties": {"$ref": "#/definitions/maximum_actual"}
94+
}
95+
}
96+
}
97+
},
98+
"definitions": {
99+
"totals": {
100+
"type": "object",
101+
"required": ["lines"],
102+
"additionalProperties": false,
103+
"properties": {
104+
"lines": {"$ref": "#/definitions/line_statistic"},
105+
"branches": {"$ref": "#/definitions/coverage_statistic"},
106+
"methods": {"$ref": "#/definitions/coverage_statistic"}
107+
}
108+
},
109+
"source_file": {
110+
"type": "object",
111+
"required": ["lines", "source", "lines_covered_percent", "covered_lines", "missed_lines", "total_lines"],
112+
"additionalProperties": false,
113+
"properties": {
114+
"lines": {
115+
"type": "array",
116+
"description": "Per-source-line coverage. Element index N corresponds to source line N+1. Integer hit-count, null for non-relevant (blank/comment) lines, or the string \"ignored\" for lines inside a simplecov:disable / :nocov: region.",
117+
"items": {"$ref": "#/definitions/line_coverage"}
118+
},
119+
"source": {
120+
"type": "array",
121+
"description": "Source lines, in order. Same length as the lines array.",
122+
"items": {"type": "string"}
123+
},
124+
"lines_covered_percent": {"type": "number"},
125+
"covered_lines": {"type": "integer", "minimum": 0},
126+
"missed_lines": {"type": "integer", "minimum": 0},
127+
"total_lines": {"type": "integer", "minimum": 0},
128+
"branches": {
129+
"type": "array",
130+
"items": {"$ref": "#/definitions/branch"}
131+
},
132+
"branches_covered_percent": {"type": "number"},
133+
"covered_branches": {"type": "integer", "minimum": 0},
134+
"missed_branches": {"type": "integer", "minimum": 0},
135+
"total_branches": {"type": "integer", "minimum": 0},
136+
"methods": {
137+
"type": "array",
138+
"items": {"$ref": "#/definitions/method"}
139+
},
140+
"methods_covered_percent": {"type": "number"},
141+
"covered_methods": {"type": "integer", "minimum": 0},
142+
"missed_methods": {"type": "integer", "minimum": 0},
143+
"total_methods": {"type": "integer", "minimum": 0}
144+
}
145+
},
146+
"branch": {
147+
"type": "object",
148+
"required": ["type", "start_line", "end_line", "coverage", "inline", "report_line"],
149+
"additionalProperties": false,
150+
"properties": {
151+
"type": {
152+
"type": "string",
153+
"description": "Branch kind from Ruby's Coverage library (e.g. \"then\", \"else\", \"when\")."
154+
},
155+
"start_line": {"type": "integer", "minimum": 1},
156+
"end_line": {"type": "integer", "minimum": 1},
157+
"coverage": {"$ref": "#/definitions/line_coverage"},
158+
"inline": {"type": "boolean"},
159+
"report_line": {"type": "integer", "minimum": 1}
160+
}
161+
},
162+
"method": {
163+
"type": "object",
164+
"required": ["name", "start_line", "end_line", "coverage"],
165+
"additionalProperties": false,
166+
"properties": {
167+
"name": {
168+
"type": "string",
169+
"description": "Qualified method name, e.g. \"Foo#bar\", \"Foo.bar\", or \"Foo::Bar#baz\"."
170+
},
171+
"start_line": {"type": "integer", "minimum": 1},
172+
"end_line": {"type": "integer", "minimum": 1},
173+
"coverage": {"$ref": "#/definitions/line_coverage"}
174+
}
175+
},
176+
"group": {
177+
"type": "object",
178+
"required": ["lines", "files"],
179+
"additionalProperties": false,
180+
"properties": {
181+
"lines": {"$ref": "#/definitions/line_statistic"},
182+
"branches": {"$ref": "#/definitions/coverage_statistic"},
183+
"methods": {"$ref": "#/definitions/coverage_statistic"},
184+
"files": {
185+
"type": "array",
186+
"description": "Project-relative paths of the files that fell into this group.",
187+
"items": {"type": "string"}
188+
}
189+
}
190+
},
191+
"line_statistic": {
192+
"type": "object",
193+
"required": ["covered", "missed", "omitted", "total", "percent", "strength"],
194+
"additionalProperties": false,
195+
"properties": {
196+
"covered": {"type": "integer", "minimum": 0},
197+
"missed": {"type": "integer", "minimum": 0},
198+
"omitted": {
199+
"type": "integer",
200+
"minimum": 0,
201+
"description": "Lines that cannot be covered (blank, comment, etc.). Only present on line stats."
202+
},
203+
"total": {"type": "integer", "minimum": 0},
204+
"percent": {"type": "number"},
205+
"strength": {"type": "number"}
206+
}
207+
},
208+
"coverage_statistic": {
209+
"type": "object",
210+
"required": ["covered", "missed", "total", "percent", "strength"],
211+
"additionalProperties": false,
212+
"properties": {
213+
"covered": {"type": "integer", "minimum": 0},
214+
"missed": {"type": "integer", "minimum": 0},
215+
"total": {"type": "integer", "minimum": 0},
216+
"percent": {"type": "number"},
217+
"strength": {"type": "number"}
218+
}
219+
},
220+
"line_coverage": {
221+
"description": "Integer hit-count for executable lines/branches/methods, null for non-relevant lines, or the literal string \"ignored\" for code inside a simplecov:disable region.",
222+
"oneOf": [
223+
{"type": "integer", "minimum": 0},
224+
{"type": "null"},
225+
{"type": "string", "const": "ignored"}
226+
]
227+
},
228+
"expected_actual": {
229+
"type": "object",
230+
"required": ["expected", "actual"],
231+
"additionalProperties": false,
232+
"properties": {
233+
"expected": {"type": "number"},
234+
"actual": {"type": "number"}
235+
}
236+
},
237+
"maximum_actual": {
238+
"type": "object",
239+
"required": ["maximum", "actual"],
240+
"additionalProperties": false,
241+
"properties": {
242+
"maximum": {"type": "number"},
243+
"actual": {"type": "number"}
244+
}
245+
}
246+
}
247+
}

schemas/coverage.schema.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
{
22
"$schema": "http://json-schema.org/draft-07/schema#",
33
"$id": "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage.schema.json",
4-
"title": "SimpleCov coverage.json",
5-
"description": "Schema for the coverage.json file emitted by SimpleCov's JSONFormatter. Versioned independently of the gem; non-breaking additions bump the minor segment of schema_version, breaking changes bump the major segment.",
4+
"title": "SimpleCov coverage.json (latest)",
5+
"description": "Convenience alias for the latest coverage.json schema. Mirrors schemas/coverage-v1.0.schema.json except for the $id (which is the unversioned URL). For long-lived integrations, pin to the versioned canonical (schemas/coverage-vX.Y.schema.json) so the contract you validate against does not silently shift when a new SimpleCov release bumps the schema.",
66
"type": "object",
7-
"required": ["meta", "total", "coverage", "groups", "errors"],
7+
"required": ["$schema", "meta", "total", "coverage", "groups", "errors"],
88
"additionalProperties": false,
99
"properties": {
10+
"$schema": {
11+
"type": "string",
12+
"format": "uri",
13+
"description": "URL of the JSON Schema this document conforms to. Pinned to the versioned canonical URL so documents stay validatable against the exact contract they were emitted under, even after the schema evolves."
14+
},
1015
"meta": {
1116
"type": "object",
1217
"required": [

spec/fixtures/json/sample.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"$schema": "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v1.0.schema.json",
23
"meta": {
34
"schema_version": "1.0",
45
"simplecov_version": "0.22.0",

spec/fixtures/json/sample_groups.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"$schema": "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v1.0.schema.json",
23
"meta": {
34
"schema_version": "1.0",
45
"simplecov_version": "0.22.0",

0 commit comments

Comments
 (0)