Skip to content

Commit aeaf820

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 ec15cd7 commit aeaf820

10 files changed

Lines changed: 304 additions & 17 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
@@ -1150,17 +1150,23 @@ SimpleCov.formatter = SimpleCov::Formatter::JSONFormatter
11501150
11511151
### `coverage.json` schema
11521152

1153-
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.
1153+
The `coverage.json` payload is treated as a public contract. Every emitted file carries:
1154+
1155+
- 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),
1156+
- a `meta.schema_version` (`"major.minor"`) for human-readable inspection.
1157+
1158+
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.
11541159

11551160
The schema version is independent of the gem version:
11561161

11571162
- Additive changes (new fields) bump the **minor** segment. Existing consumers keep working.
1158-
- Removals or shape changes bump the **major** segment.
1163+
- 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.
11591164

11601165
The current version is **1.0**. Top-level structure:
11611166

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

lib/simplecov/formatter/json_formatter/result_hash_formatter.rb

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,26 @@ def format_maximum_coverage_drop_errors
103103
end
104104

105105
def formatted_result
106-
@formatted_result ||= {meta: format_meta, total: {}, coverage: {}, groups: {}, errors: {}}
106+
@formatted_result ||= {
107+
:$schema => SCHEMA_URL,
108+
:meta => format_meta,
109+
:total => {},
110+
:coverage => {},
111+
:groups => {},
112+
:errors => {}
113+
}
107114
end
108115

109-
# Bump SCHEMA_VERSION when the JSON shape changes. Additive
110-
# changes bump the minor segment; removals or shape changes bump
111-
# major. See schemas/coverage.schema.json for the contract this
112-
# version describes.
116+
# Bump SCHEMA_VERSION (and SCHEMA_URL) when the JSON shape
117+
# changes. Additive changes bump the minor segment, removals or
118+
# shape changes bump the major segment. The versioned file at
119+
# schemas/coverage-vX.Y.schema.json is the canonical artifact
120+
# consumers should pin to, schemas/coverage.schema.json is a
121+
# convenience alias that always tracks the latest. See the
122+
# `coverage.json` schema section of the README for the rationale.
113123
SCHEMA_VERSION = "1.0"
114-
private_constant :SCHEMA_VERSION
124+
SCHEMA_URL = "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v#{SCHEMA_VERSION}.schema.json".freeze
125+
private_constant :SCHEMA_VERSION, :SCHEMA_URL
115126

116127
def format_meta
117128
{

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",

spec/fixtures/json/sample_with_branch.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_with_method.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)