Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Unreleased
## Enhancements
* `simplecov uncovered` gained `--criterion line|branch|method` (default `line`) so the lowest-coverage listing can rank by branch or method coverage, not just line.
* Added the criterion-first `coverage` configuration method — a uniform way to configure each coverage criterion (`:line`, `:branch`, `:method`) in one place. `coverage :line do minimum 90; minimum_per_file 80; maximum_drop 5 end` (or the one-liner `coverage :branch, minimum: 80`) enables the criterion and declares its thresholds with identical syntax regardless of criterion, because the criterion is fixed by the enclosing call rather than smuggled into the argument as the historical "a bare number means line coverage, every other criterion needs a Hash" special case. Verbs: `minimum`, `maximum`, `exact`, `maximum_drop`, `minimum_per_file` (with `only:` String-path / Regexp overrides), and `minimum_per_group`. Options: `primary:` (the report's leading criterion), `oneshot:` (oneshot-lines mode for `:line`), and `:eval`. The flat `minimum_coverage` family remains as suite-wide sugar. Thresholds feed the same internal stores, so exit-code enforcement is unchanged. See the "Per-criterion thresholds with `coverage`" section in the README.
* 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. `meta.commit` carries the git commit SHA the report was generated against (or null outside a git checkout), so tools can recover the exact source from history even when `source_in_json false` omits the per-file source arrays.
* 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.
* 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.
* 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.
Expand All @@ -45,6 +46,7 @@ Unreleased
* Terminal output is now colorized when stderr is a TTY: coverage percentages in the formatter summary line and threshold-violation messages are rendered green (>= 90%), yellow (>= 75%), or red (< 75%) — matching the HTML report's thresholds. The "SimpleCov failed with exit N…" line is red and the "Stopped processing SimpleCov…" line is yellow. Respects `NO_COLOR` (force off, per no-color.org) and `FORCE_COLOR` (force on); `NO_COLOR` wins if both are set. See #1157.
* CLI subcommands `coverage`, `report`, `uncovered`, and `diff` now colorize their printed percentages by the same threshold (and `diff` colors regressions red, improvements green). Auto-detect based on whether stdout is a TTY; the same `NO_COLOR` / `FORCE_COLOR` env vars apply. Each subcommand also accepts a `--no-color` flag as a per-invocation override.
* Added `# simplecov:disable` / `# simplecov:enable` directive comments for selectively skipping `line`, `branch`, and `method` coverage. Block form (own line) opens a region until the matching `# simplecov:enable`; inline form (trailing a code line) skips just that line. Categories may be combined (`# simplecov:disable line, branch`); omitting categories targets all three. Any trailing text is treated as a free-form reason and discarded (e.g. `# simplecov:disable line legacy adapter`). Directive markers inside string literals or heredocs are ignored.
* Added `SimpleCov.source_in_json` (default true) to make the per-file `source` array in `coverage.json` opt-out. Tools that read the project's source files from disk don't need the embedded copy, and on larger projects it dominates the JSON payload. The HTML report's `coverage_data.js` still embeds source unconditionally because the client-side viewer renders source from there. See #1143.
* JSON formatter: `meta.timestamp` is now emitted with millisecond precision (`iso8601(3)`) so the concurrent-overwrite warning can distinguish writes within the same wall-clock second
* JSON formatter: added `total` section with aggregate coverage statistics (covered, missed, total, percent, strength) for line, branch, and method coverage. Line stats additionally include `omitted` (count of blank/comment lines, i.e. lines that cannot be covered)
* JSON formatter: per-file output now includes `total_lines`, `lines_covered_percent`, and when enabled: `branches_covered_percent`, `methods` array, and `methods_covered_percent`
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ group :development do
gem "capybara"
gem "cucumber"
gem "cuprite"
gem "json_schemer"
gem "nokogiri"
gem "rackup"
gem "rake"
Expand Down
8 changes: 8 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ GEM
websocket-driver (~> 0.7)
ffi (1.17.4)
ffi (1.17.4-java)
hana (1.3.7)
io-console (0.8.2)
io-console (0.8.2-java)
irb (1.18.0)
Expand All @@ -83,6 +84,11 @@ GEM
jar-dependencies (0.5.7)
json (2.19.5)
json (2.19.5-java)
json_schemer (2.5.0)
bigdecimal
hana (~> 1.3)
regexp_parser (~> 2.0)
simpleidn (~> 0.2)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
Expand Down Expand Up @@ -169,6 +175,7 @@ GEM
lint_roller (~> 1.1)
rubocop (~> 1.81)
ruby-progressbar (1.13.0)
simpleidn (0.2.3)
stringio (3.2.0)
sys-uname (1.5.1)
ffi (~> 1.1)
Expand Down Expand Up @@ -203,6 +210,7 @@ DEPENDENCIES
capybara
cucumber
cuprite
json_schemer
nokogiri
rackup
rake
Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1014,10 +1014,60 @@ SimpleCov.formatters = [
SimpleCov.formatter = SimpleCov::Formatter::JSONFormatter
```

By default `coverage.json` carries the full source-text array for every file, which makes the payload self-contained
but dominates the file size on larger projects. Tools that read the project's source files directly from disk can opt
out of that field with:

```ruby
SimpleCov.start do
source_in_json false
end
```

The HTML report's `coverage_data.js` always retains the source array — the client-side viewer renders source from
there. The setting only affects the side-file `coverage.json`. When the source is omitted, `meta.commit` (the git
commit SHA the report was generated against) lets tools recover the exact source lines from repository history.

> The JSON formatter was originally a separate gem,
> [simplecov_json_formatter](https://github.com/codeclimate-community/simplecov_json_formatter). It is now built in and
> loaded by default; existing code that does `require "simplecov_json_formatter"` will continue to work.

### JSON Schema for `coverage.json`

`coverage.json` is a public contract, described by a JSON Schema (2020-12) so downstream tools can validate it,
generate types, or pin to a known shape. Every emitted document carries a top-level `$schema` URL pointing at the
versioned canonical, plus a human-readable `meta.schema_version` (`"major.minor"`).

The **versioned canonical** lives at [`schemas/coverage-v1.0.schema.json`](schemas/coverage-v1.0.schema.json) and
long-lived integrations should pin to it. Once a SimpleCov release ships with a given versioned schema file, that file
is immutable: bug fixes, additions, or shape changes ship as a new versioned file (a minor or major bump), never as a
silent rewrite of an already-released one. Schemas may still be corrected in-place between gem releases — i.e., the
schema file as it currently exists on `main` may change before the next gem release, but the schema for any published
gem version stays frozen. 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.

The schema version is independent of the gem version:

- Additive changes (new fields) bump the **minor** segment. Existing consumers keep working.
- 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.

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

```jsonc
{
"$schema": "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v1.0.schema.json",
"meta": { /* schema_version, simplecov_version, command_name, project_name, timestamp, root, commit, line_coverage, branch_coverage, method_coverage */ },
"total": { /* aggregate stats for lines (and branches / methods when enabled) */ },
"coverage": { "<project-relative path>": { /* per-file lines, source, branches, methods, etc. */ } },
"groups": { "<group name>": { /* per-group stats + files */ } },
"errors": { /* minimum_coverage, minimum_coverage_by_file, minimum_coverage_by_group, maximum_coverage, maximum_coverage_drop violations */ }
}
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to get full example for some minimal 'Hello World' script (that covers all features).


The `.resultset.json` file is **not** schema'd — it's SimpleCov-internal and may change shape across releases. Build
integrations on top of `coverage.json`.

### More formatters, editor integrations, and hosted services

* [Open Source formatter and integration plugins for SimpleCov](doc/alternate-formatters.md)
Expand Down
22 changes: 22 additions & 0 deletions lib/simplecov/configuration/formatting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,28 @@ def print_errors(value = :__no_arg__)
@print_error_status = value
end

#
# Get or set whether `coverage.json` includes the full source-text
# array for every file. Defaults to true. Set to false when a
# downstream tool reads the project's source files directly and
# only needs the coverage metrics, so `coverage.json` doesn't carry
# a copy of the source tree (which dominates the payload on larger
# projects).
#
# The HTML viewer's `coverage_data.js` always includes source —
# the client-side renderer needs it. Only `coverage.json` honors
# this setting.
#
# SimpleCov.start do
# source_in_json false
# end
#
def source_in_json(value = :__no_arg__)
return defined?(@source_in_json) ? @source_in_json : true if value == :__no_arg__

@source_in_json = value
end

# DEPRECATED: alias for `print_errors`. Same value, same behavior.
def print_error_status
warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov.print_error_status` is deprecated. " \
Expand Down
16 changes: 12 additions & 4 deletions lib/simplecov/formatter/html_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@ class HTMLFormatter < Base
DATA_FILENAME = "coverage_data.js"

def format(result)
json = JSON.pretty_generate(JSONFormatter.build_hash(result))

# `coverage_data.js` feeds the client-side viewer, which renders
# source from the embedded array — it always needs `source`,
# regardless of `SimpleCov.source_in_json`. The side-file
# `coverage.json` honors the setting so downstream tools that
# read source from disk can opt into a smaller payload. When
# the setting is at its default (true), the two files share a
# single serialization.
FileUtils.mkdir_p(output_path)
atomic_write(File.join(output_path, JSONFormatter::FILENAME), json)
atomic_write(File.join(output_path, DATA_FILENAME), "window.SIMPLECOV_DATA = #{json};\n")
viewer_json = JSON.pretty_generate(JSONFormatter.build_hash(result, include_source: true))
coverage_json = SimpleCov.source_in_json ? viewer_json : JSON.pretty_generate(JSONFormatter.build_hash(result))

atomic_write(File.join(output_path, JSONFormatter::FILENAME), coverage_json)
atomic_write(File.join(output_path, DATA_FILENAME), "window.SIMPLECOV_DATA = #{viewer_json};\n")

copy_static_assets
# stderr, not stdout: this is a status message, not the program's
Expand Down
9 changes: 7 additions & 2 deletions lib/simplecov/formatter/json_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ module Formatter
class JSONFormatter < Base
FILENAME = "coverage.json"

def self.build_hash(result)
ResultHashFormatter.new(result).format
# `include_source:` defaults to `SimpleCov.source_in_json` (true
# by default) so the historical payload shape is unchanged.
# Callers that need the source array regardless of the global
# setting (the HTML formatter, which feeds the client-side
# viewer) pass `include_source: true` explicitly.
def self.build_hash(result, include_source: SimpleCov.source_in_json)
ResultHashFormatter.new(result, include_source: include_source).format
end

def format(result)
Expand Down
4 changes: 2 additions & 2 deletions lib/simplecov/formatter/json_formatter/errors_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ def format_minimum_by_file
end

def record_by_file(violation)
criterion_bucket = bucket(:minimum_coverage_by_file)[key_for(violation)] ||= {}
criterion_bucket[violation.fetch(:project_filename)] = expected_actual(violation)
file_bucket = bucket(:minimum_coverage_by_file)[violation.fetch(:project_filename)] ||= {}
file_bucket[key_for(violation)] = expected_actual(violation)
end

def format_minimum_by_group
Expand Down
58 changes: 51 additions & 7 deletions lib/simplecov/formatter/json_formatter/result_hash_formatter.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require "open3"
require "time"
require_relative "errors_formatter"
require_relative "source_file_formatter"
Expand All @@ -10,24 +11,39 @@ class JSONFormatter
# Builds the hash that JSONFormatter serializes to coverage.json:
# meta, per-file coverage data, group totals, and aggregate stats.
class ResultHashFormatter
def initialize(result)
# Bump SCHEMA_VERSION (and SCHEMA_URL) when the JSON shape
# changes. Additive changes bump the minor segment, removals or
# shape changes bump the major segment. The versioned file at
# schemas/coverage-vX.Y.schema.json is the canonical artifact
# consumers should pin to, schemas/coverage.schema.json is a
# convenience alias that always tracks the latest. See the
# `coverage.json` schema section of the README for the rationale.
SCHEMA_VERSION = "1.0"
SCHEMA_URL = "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v#{SCHEMA_VERSION}.schema.json".freeze
private_constant :SCHEMA_VERSION, :SCHEMA_URL

def initialize(result, include_source: true)
@result = result
@include_source = include_source
end

def format
{
meta: format_meta,
total: format_coverage_statistics(@result.coverage_statistics),
coverage: format_files,
groups: format_groups,
errors: ErrorsFormatter.new(@result).call
:$schema => SCHEMA_URL,
:meta => format_meta,
:total => format_coverage_statistics(@result.coverage_statistics),
:coverage => format_files,
:groups => format_groups,
:errors => ErrorsFormatter.new(@result).call
}
end

private

def format_files
@result.files.to_h { |source_file| [source_file.project_filename, SourceFileFormatter.new(source_file).call] }
@result.files.to_h do |source_file|
[source_file.project_filename, SourceFileFormatter.new(source_file, include_source: @include_source).call]
end
end

def format_groups
Expand All @@ -39,16 +55,44 @@ def format_groups

def format_meta
{
schema_version: SCHEMA_VERSION,
simplecov_version: SimpleCov::VERSION,
command_name: @result.command_name,
project_name: SimpleCov.project_name,
timestamp: @result.created_at.iso8601(3),
root: SimpleCov.root,
commit: git_commit
}.merge!(coverage_flags)
end

# Full git commit SHA of `SimpleCov.root`'s HEAD, or nil when the
# project isn't a git checkout or git isn't on PATH. Recorded so tools
# can recover the exact source a report was generated against, which
# matters most when `source_in_json false` drops the source text from
# coverage.json. stderr is captured (not forwarded) so a non-git project
# doesn't print git's diagnostics to the build.
def git_commit
output, status = Open3.capture2e("git", "-C", SimpleCov.root.to_s, "rev-parse", "HEAD")
status.success? ? output.strip : nil
rescue StandardError
nil
end

def coverage_flags
{
line_coverage: line_coverage_enabled?,
branch_coverage: SimpleCov.branch_coverage?,
method_coverage: SimpleCov.method_coverage?
}
end

# Mirrors SourceFileFormatter's predicate so meta.line_coverage
# tracks exactly which configurations cause the formatter to
# emit line stats.
def line_coverage_enabled?
SimpleCov.coverage_criterion_enabled?(:line) || SimpleCov.coverage_criterion_enabled?(:oneshot_line)
end

def format_coverage_statistics(statistics)
result = {}
result[:lines] = format_line_statistic(statistics[:line]) if statistics[:line]
Expand Down
Loading
Loading