chore(deps): update dependency simplecov to v1 - #299
Open
renovate[bot] wants to merge 1 commit into
Open
Conversation
Contributor
Author
|
renovate
Bot
force-pushed
the
renovate/simplecov-1.x
branch
from
August 11, 2026 00:24
6e4c2bb to
c6b717e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
"~> 0.22.0"→"~> 1.1.0"Release Notes
simplecov-ruby/simplecov (simplecov)
v1.1.1Compare Source
==================
Enhancements
schemas/directory, dropping 32 KB unpacked from every install. Nothing read it at runtime: the JSON formatter points consumers at the canonical schema URL on GitHub, and the schema specs read from the repository.Bugfixes
at_exithooks run. rspec-conductor closes each worker's pipes as soon as the worker has sent its run summary, and emitting SimpleCov's status line probesColor.enabled?, whoseIO#tty?call raisedIOErroron the closed stream. That exception aborted the remaining exit tasks, so the HTML and JSON reports were written but the minimum and maximum coverage checks silently never ran, and neither.last_run.jsonnor the.report_stampdeferral marker was written. A closed stream now counts as not a tty, and the status line and the violation report are dropped rather than cancelling the checks that follow. rspec-conductor is also named in the parallel adapter documentation and covered end to end now, since it follows theTEST_ENV_NUMBERandPARALLEL_TEST_GROUPSconvention the generic adapter already recognizes. See #1156.SimpleCov.collateno longer depends on load order forCoverage.line_stub.SimulateCoverageandResultAdapterboth call it without requiring the coverage library. A normal run loads that library while starting tracking, but a collate-only process never starts tracking, so the constant resolved only by luck through a criterion-support check that happened to run first. Any reordering of that path would have turned into aNameErrorout ofSimpleCov.collate, so each file now requires what it uses.v1.1.0Compare Source
==================
Breaking Changes
simplecov report --jsonnow emits{"total": {...}, "groups": {...}}instead of flattening the overall"All Files"entry and configured groups into one object. The old shape silently overwrote the overall totals when a user group was also namedAll Files; the text report now labels that user sectionAll Files (group)as well.Ungroupedis now reserved for the implicit group of files that match no configured group. Defining an explicit group with that name previously caused SimpleCov to overwrite it during result processing and silently discard its matched files; rename such a group toOtheror another distinct label. Group names are also normalized when configured: aSymbolname (group :Models) now means the same group as itsStringspelling (sogroup :Ungroupedis rejected like the string form, and a symbol-named group can no longer produce a duplicate JSON key next to a string-named one), and a name that is neither aStringnor aSymbolraisesSimpleCov::ConfigurationError.index.html. The viewer's JavaScript and CSS are inlined into the compiled template at build time, and the coverage data is embedded at report time (with<escaped in the payload so embedded source text cannot terminate the surrounding<script>element), socoverage/contains justindex.htmlandcoverage.json. A single file can be mailed, uploaded as a non-zipped GitHub Actions run artifact (actions/upload-artifactwitharchive: false, viewable directly from the run page), or copied anywhere without sibling files, and the report can no longer be read mid-write in a torn state whereindex.html,coverage_data.js, andapplication.jscome from different runs — the whole report updates in one atomic rename. The sibling files the formatter previously wrote (coverage_data.js,application.js,application.css, and the three favicon PNGs) are gone; anything scripted against that layout should readcoverage.json(the sanctioned data artifact, unchanged) instead ofcoverage_data.js. Formatting also deletes those six names from the output directory when an earlier version left them there, so an upgraded project'scoverage/doesn't keep a stalecoverage_data.jsaround forsimplecov serveto serve. This restores single-file reports to the 1.0 line — the pre-1.0simplecov-htmlformatter offered them via theSIMPLECOV_INLINE_ASSETSenvironment variable, which the 1.0 client-side rendering rewrite dropped — and makes them the default and only mode, with no environment variable or configuration flag. See #1241.Enhancements
aria-pressed.simplecov servenow handles each connection on its own thread with a read timeout, so a stalled connection (browsers routinely open speculative sockets that send no bytes) no longer blocks every other request. It also works on JRuby and TruffleRuby, answers malformed request lines with a 400 instead of an empty response, and prints a bracketed URL for IPv6 hosts.docs/directory (Configuration, Parallelism, Formatters, CLI, Troubleshooting) alongside the changelogs, contributing guide, and code of conduct, with the issue template tucked into.github/. This changelog now lives atdocs/Changelog.mdand the gem'schangelog_urimetadata follows it. Nothing underdocs/ships in the gem, which also stops packaging the olddoc/*link lists. The alternate formatters catalog was rebuilt against RubyGems: twenty formatters join the twelve that were listed, organized by output type..resultset.jsonis now written as compact JSON instead of pretty-printed. It is a machine-read cache that every parallel worker rewrites wholesale, and pretty printing nearly doubled the bytes written, read back, and parsed on each store-merge round trip — on a 100,000-file project the file shrinks from 89MB to 51MB and serialization halves. Any JSON parser reads the compact form; pipe it throughjqif you need to inspect it by eye.SimpleCov.collatetakes a newprocesses:argument that fans the resultset merge out across that many forked worker processes. This addresses the wall clock of a large CI matrix's collate step, where the collating process reads, parses and folds hundreds of resultsets in sequence and nearly all the time goes into that fold: merging 160 resultsets covering 1,836 files on a 14-core machine took 4.53s at the defaultprocesses: 1and 1.35s across 8 workers. The report is identical either way, not merely equivalent — each worker folds a contiguous slice of the file list and the collating process folds the slices back in order, so the resultsets are visited in the same order a single-process merge visits them. The fan-out lives in a newSimpleCov::ParallelResultMerger, whoseabsorb_resultsmirrorsResultMerger.absorb_results, splitting that fold across workers and unioning the tracked paths each one saw.processesdefaults to theSIMPLECOV_CONCURRENCYenvironment variable (1 when unset), so one rake task can serve CI runners of different sizes without being edited, and an explicit argument wins over the variable. It never forks at 1, so existingcollatecalls are unaffected; it is deliberately not clamped to the core count nor gated on a minimum number of resultsets — only the caller knows what a collate job is allowed to use — asking for more processes than there are result files just gives one file per process, and anything below 1 is taken as 1. Merging falls back to the collating process, with the same report and no error, when the runtime cannot fork (JRuby, TruffleRuby, Windows), when there is only one resultset, or when a worker dies. Abenchmarks/collate.rbharness (PROCESSES=N) measures the phases against a saved baseline.Bugfixes
Two concurrent runners sharing a command name no longer lose the later writer's coverage for files both carried. A live result serializes its criterion tables under Ruby's Symbol keys while entries parsed back from
.resultset.jsoncarry Strings, and the combiners read only Strings, so the merge that exists to prevent an empty parent process from clobbering a subprocess's data (#581) silently contributed nothing from the incoming side. Criterion keys are now stringified at serialization time so the stored and live shapes always match.Merging or collating stored resultsets with method coverage enabled no longer crashes on singleton methods defined on instances.
def obj.greetrecords its receiver as the nested inspect form#<Class:#<Object:0x...>>, and the parser that turns JSON-stringified method keys back into tuples stopped at the first closing angle bracket, raisingArgumentErrorout of the merge. The quoting now handles nested segments.That same clobber-prevention backstop now stands down for failed child runs too. It keyed its freshness check on
.last_run.json, which only fully successful runs write, so a Rakefile parent overwrote the child's report exactly when the child's tests or coverage checks had failed and the report mattered most. Formatting now touches acoverage/.report_stampmarker no matter how the run ends, and the backstop accepts either file as evidence of a fresher report.A
# simplecov:disable lineblock around a method no longer silently removes that method from method-coverage totals. The method skip fell back to asking whether all of the method's lines were skipped, so the line-only directive (the README's own example) leaked into the method criterion. Deprecated# :nocov:chunks still exclude methods, now routed explicitly like every other criterion.A directive reason that merely starts with a category name no longer narrows the directive.
# simplecov:disable linear algebra reasonsparsed as categorylinewith the rest as reason, disabling only line coverage where the documented behavior for unrecognised text is to over-disable everything. The category list now requires a word boundary.Per-group minimums configured with a Symbol group name are enforced again.
group :Modelsnormalizes the name to a String butminimum_per_group 95, only: :Models(and the deprecatedminimum_coverage_by_group) stored the Symbol untouched, so the check-time lookup missed and warned that group "Models" doesn't exist while listing that very name as available.simplecov diffmatches its documentation:--threshold Nis inclusive (a file that moved exactly N% is listed), removed files no longer trip--fail-on-drop(deleting a covered file is not a regression), and sub-epsilon float noise no longer fails the gate on a row shown only for its gains.Tracked-but-unloaded files with multi-statement parenthesized conditions no longer synthesize phantom branches. CRuby folds
if (1; 2)by its last expression when the compiler can eliminate every leading statement, and the rules differ per version (parse.y eliminates only pure literals, 3.3 also eliminates side-effect-free reads and containers of them, 3.4+ narrows containers to fully static literals). The static extractor now mirrors each compiler exactly, verified against realCoverageoutput on every supported Ruby.require "simplecov"no longer raises whenHOMEis set but empty, as some container and CI images do. The global-config loader treats an emptyHOMElike an unset one.simplecov merge,report, andcoverageprint one-line errors instead of backtraces on more bad inputs: a directory or unreadable file passed tomerge, valid JSON whosetotalorgroupshas the wrong type, and a per-file entry that is not an object.An empty or non-numeric
PARALLEL_TEST_GROUPSno longer makes the reporting worker expect zero siblings and skip the wait for their results; unusable values now mean one worker, and non-positive values are rejected too.Simulating tracked files became tolerant of unreadable paths: a
track_filesglob that sweeps up a directory named like a Ruby file or a permission-denied entry now treats it as empty instead of crashing the merge or report step. Resultset files truncated to a single byte now warn like other corruption instead of reading as quietly empty, a hand-edited.last_run.jsonwith a non-numeric percentage no longer raises out of the at_exit hook, and the missing-group notice respectsprint_errorsand survives-W0like every other enforcement message.coverage :eval, minimum: 100now explains that thresholds are unsupported for:evalinstead of claiming the criterion itself is invalid, andsimplecov clean --dry-runcounts dotfiles such as.resultset.jsonin its entry count.Source files containing invalid UTF-8 bytes no longer crash report generation. A file with no encoding magic comment is read as UTF-8, and a stray high-bit byte (a Latin-1 comment, say) previously raised
ArgumentError: invalid byte sequence in UTF-8from the first regex that touched the line — the shebang check or the lines classifier — taking the whole report down. Invalid bytes are now replaced with the Unicode replacement character at load time, so every line leaves the source loader as valid UTF-8 and the rest of the pipeline (classification, JSON embedding, the HTML viewer payload) works from sanitized text.Generated coverage artifacts now share one collision-safe atomic writer. Concurrent threads no longer reuse the same process-ID temporary name, and the JSON formatter and
simplecov mergeno longer expose partially written documents to readers; existing Unix permission bits and each artifact's historical byte format are preserved.Configuration blocks no longer install temporary
method_missinghooks on their caller or copy caller instance variables into SimpleCov. Those hooks leaked DSL commands across threads, broke overlapping and nested evaluations, rejected frozen or immediate-value owners, changedrequire_relativeand binding behavior, and could mask an original exception during cleanup. See the parameterized-block migration under Breaking Changes.Non-final parallel workers now stop after storing their own result instead of reading and caching a partial merge. A single ownership predicate selects the adapter's final process for merging, formatting, threshold checks, and
.last_run.json; explicitSimpleCov.collateremains authoritative regardless of worker identity.simplecov servenow builds a missingindex.htmlfromcoverage.jsonand fails before binding when neither artifact exists or the JSON is invalid. An existing self-contained report remains usable even if its optional sidecar JSON was later removed or damaged.Coverage JSON consumers now reject malformed syntax, invalid UTF-8, and non-object roots through one shared parser.
HTMLFormatter#format_from_jsonalso validates the viewer's required metadata, coverage flags, enabled totals, groups, and source arrays before creating or replacing its output.HTML reports now render correctly when line coverage is disabled. Branch-only and method-only runs use their configured primary criterion for tabs, color bands, sorting, tables, filters, and source summaries instead of crashing while dereferencing absent line statistics.
HTML reports now disambiguate source files whose truncated SHA-1 identifiers collide. Existing fragments stay unchanged for non-colliding files, while colliding links receive deterministic suffixes and always open the intended source.
SimpleFormatternow prints each file's configured primary coverage percentage instead of always printing line coverage. Branch-, method-, and oneshot-primary reports now match the documented primary-criterion behavior; oneshot coverage correctly reads the normalized line statistics.Frontend builds now use the esbuild binary installed from
html_frontend/bun.lockinstead of whichever global version happens to be onPATH. CI recompiles the self-contained HTML template and fails on a diff, preventing dependency updates or source changes from leaving the checked-in report asset stale.Frontend asset compilation now fails when esbuild rejects the CSS. The rake helper previously ignored the minifier subprocess's exit status and continued with empty output, allowing a successful build to replace the checked-in report template with a stylesheet-free page.
Read-only CLI commands now handle unreadable, malformed, and structurally unusable
coverage.jsoninputs consistently.coverage,report,uncovered, and both inputs todiffreturn status 1 with one command-specific diagnostic instead of raising a JSON parser backtrace;uncoveredno longer mislabels its input errors assimplecov report.Sorting one HTML report group no longer corrupts the next group's first sort. Every table previously shared the same fallback sort-state key because the tables have no ids, so clicking a column already selected in another group reversed unsorted rows while displaying an ascending indicator; sort state is now scoped to each table element.
The HTML report now gives the overall file list and configured groups distinct typed identities, so a user-defined group named
All Filesno longer shares the overall section's DOM id and tab target. Both identically labelled tabs now remain present and open their own file lists.HTML group tabs now remain distinct when one group name contains punctuation and another contains that character's hexadecimal escape spelling (for example,
By/groupandBy_2f_group). Literal underscores are now escaped because underscores delimit encoded characters; previously both names produced the same DOM id and one tab opened the wrong file list.Enabling ordinary line coverage after oneshot-line coverage no longer passes both incompatible modes to Ruby's
Coverage.start, which raisedRuntimeError: cannot enable lines and oneshot_lines simultaneously. The two modes now replace each other in either direction, with the last request winning, and replacing the active primary criterion resets it to an enabled default.Branch and method tuples are no longer synthesized for code the compiler eliminates. 1.0.2 stopped synthesizing a branch for a constant-folded condition itself (
if false,if true, a ternary on a literal), but everything nested inside the dead arm was still visited, so anif false ... endblock containing conditionals or method definitions — a common way to disable code — gave a tracked-but-unloaded file tuples Ruby'sCoveragenever emits: phantom, permanently-missed branches and phantom uncovered methods, the same unmergeable-tuple failure mode as #1226 / #1233. The extractor now descends only into the arm the compiler keeps, so a dead arm's entire subtree (nested conditionals, loops, safe navigation, anddefs alike) emits nothing, while the live arm's contents — and the survivingelsifchain of a falsyif— are tracked exactly asCoveragetracks them. The folding table also gains the three literals it was missing:__LINE__,__ENCODING__, and a stabby lambda (->) fold as conditions too, while their lookalikes__FILE__and alambdacall do not and are still tracked. And the fold's paren transparency now matches the compiler's, which is not universal:if (1)folds likeif 1, but(nil),("x"), and(-> {})keep their real branch the moment parentheses wrap them (for the string, this mismatch predates these changes).A merged report no longer shows 100% branch and method coverage for a tracked file that no process ever loaded.
SourceFile::Statisticsreports 0% rather than a misleading 100% when a never-loaded file has no branch or method data at all (#902), but that rule keys off aloaded:flag that only the single-process path ever set:ResultMerger.create_resultbuilt itsResultwithoutnot_loaded_files, so every file in a merged report claimed to have been loaded and the rule could never fire there. The flag isn't serialized into.resultset.json(Result#to_hashwrites only coverage and a timestamp), so the merged result now re-derives it from the merged line counts, using the same "did any line execute" signalCombine::FilesCombineralready reconciles on. In practice this surfaced on files with no branches at all, such as a constants file picked up by acoverglob, since #1059's synthesized tuples already produce 0% for anything containing a conditional. Anything a process did load is unaffected, including under a branch-only or method-only configuration:Coveragereports no line data there, so a simulated file omits it too and the merged report flags nothing rather than mistaking every loaded file for an unloaded one. A file is judged only when it has at least one relevant line, so a loaded file with no executable lines at all (a comment-only constants stub, say) keeps its usual statistics rather than being mistaken for never-loaded — a simulated file carries a0on every relevant line, so genuinely unloaded files are still flagged. Reported with an exemplary diagnosis by @andriytyurnikov. See #1250.SimpleCov.command_nameis no longer decided by an incidental substring of the path to the Ruby interpreter.CommandGuessermatches its framework patterns against"#{$PROGRAM_NAME} #{ARGV.join(' ')}", and those patterns were bare substrings, so atest/anywhere in that string won. A Ruby installed under alatest/bindirectory (the layout mise creates alongside the versioned one) puttest/in the path of every binary run through it, and becausetest/is checked beforespec/, RSpec and Cucumber suites alike were labelledUnit Tests. The same flaw applied inside the arguments, whererspec spec/greatest/foo_spec.rbwas mislabelled for the same reason. The patterns now match only at a path segment boundary, solatest/,contest/, andgreatest/no longer read astest/. Because the command name is the resultset key under merging, a mislabelled suite was filed under the wrong key, letting two different suites merge into each other rather than failing loudly. Reported with an exemplary diagnosis by @andriytyurnikov. See #1249.The invoked executable is now consulted before the path patterns, so an
rspecorcucumberbinary names the framework regardless of what surrounds it on the command line. This is what keepsrspec featuresreporting asRSpecrather than as Cucumber: an RSpec suite whose examples live infeatures/is still an RSpec suite, and previously that case only worked by accident, because the old unanchoredspecpattern matched the letters inside the wordrspec. Generic runners are deliberately not in the table, soruby test/integration/foo_test.rband rake's test loader still fall through to the path patterns that draw the unit, functional, and integration distinction. The executable is read from$PROGRAM_NAME, which is now recorded separately from the flattened command asCommandGuesser.original_program_name, because the space that joins it toARGVmakes a program path containing one (/opt/My Ruby/bin/rspec) impossible to recover afterwards.SimpleCov.formatters = falsenow opts out of formatting, matchingformatter false. Since 1.0.1's input normalization,Array(false)smuggled thefalsethrough as a one-element formatter list, so every report printed a "Formatter false failed with NoMethodError" complaint instead of skipping formatting.nil,false, and[]now all mean the same explicit opt-out on both setters.Merging no longer discards branch and method data the resultsets carried just because the merging process does not measure that criterion itself. A merge runs on behalf of the processes that produced the resultsets and does not necessarily share their configuration:
simplecov mergeonly requires the library and never runsSimpleCov.start, and aSimpleCov.collateblock need not repeatenable_coverage :branch. Such a process dropped the branch table from every file that appeared in more than one resultset while passing through, untouched, the table of any file that appeared in only one, so the merged output was both lossy and internally inconsistent, andmerge_and_storewrote that state back to disk. A criterion is now carried when the merging process measures it or when the data carries it, so nothing measured is lost and a process that does measure a criterion still always gets a table, even an empty one.Performance
source_in_json falsenow builds metadata, groups, errors, and per-file statistics once, then derives the source-lesscoverage.jsonpayload from that result. It previously traversed the entire result and queried Git a second time solely to omit each file'ssourcefield.inject_unloaded_filesskipped only the files the current process had loaded, so every worker in a parallel run simulated nearly the whole project and the merge discarded all but one copy of each. The work now happens inResultMerger, against the union of what every contributing process loaded, which makes it O(1) in worker count rather than O(N). Over 400 tracked files with 40 of them loaded by no worker, a 16-worker run drops from 6,040 simulations taking 3.15s across the workers to 40 taking 0.024s once, and per-worker resultsets shrink from 13.4MB to 800KB because they no longer each carry a simulated copy of the project. A single process, and any run withmerging false, pays exactly what it did before. Each process records the paths it was told to track into its resultset so the merge can do this without needing that process'scover/track_filesconfiguration, which a standaloneSimpleCov.collatedoes not have; resultsets written by earlier versions carry the files their process injected and merge unchanged. Reported with measurements by @andriytyurnikov. See #1250.LinesClassifier#classifyran the:nocov:regex twice for every line — once to toggle skipping and once insidenot_relevant_line?. A marker is always a comment, so the cheaper whitespace-or-comment test now gates the token match, and a line of real code (most of a source file) no longer pays for it at all. Over SimpleCov's ownlib/(104 files, 8,845 lines) the whole simulation pass drops 11% on a line-coverage-only run and 7% with synthesis on.benchmarks/simulate_coverage.rbcovers this path, which had no benchmark before — onlycollateandResultdid, so nothing measured the per-process work at exit.collatespends its time on. Results were folded together pairwise, so every one of the N-1 steps rebuilt the whole accumulated structure — a fresh outer file hash, a fresh lines array for every file, and a fresh branch/method table for every file whose keys were re-interned from their tuples each time. A 160-worker run over ~1,800 files did ~290,000 whole-file rebuilds to produce ~1,800 files of output. Results are now absorbed into an accumulator that owns its state and updates it in place, and the interned branch/method tables become tuple-keyed hashes once, at the end. Resultsets are still read and absorbed one at a time, so the memory ceilingmerge_resultsis careful about is unchanged (peak RSS on the benchmark is identical). On the repository'sbenchmarks/collate.rbfixture — 160 resultsets, 1,836 files, 147,875 lines, branch coverage on — the merge phase drops from 5.85s to 3.38s and the whole collate from 6.65s to 3.87s.SimpleCov::Combine::FilesCombinerandSimpleCov::Combine.combineare gone, their roles taken by the newSimpleCov::Combine::CoverageAccumulator; both were internal API.v1.0.3Compare Source
==================
Bugfixes
#inspectwith an incompatible signature. Rendering a method coverage key's receiver callsto_s, and a singleton class'sto_srenders its attached object via#inspect— Liquid'sUtilsmodule definesinspect(value, max_depth = 2)as amodule_function, so any suite whose report included Liquid's files (typically a vendored bundle under the project root, which is why this surfaced only in CI) raisedArgumentErrorfrom the at_exit hook and lost its report. The exposure predates 1.0.2's key normalization, which only moved the call. Rendering now recovers by rebuilding the name fromModule#namevia bound methods, which user code cannot shadow, falling back to an address form that the existing normalization collapses. Theexternal_at_exitworkaround is no longer needed. Reported with an exemplary diagnosis by @bkuhlmann. See #1236.container.each_key { |key| define_method(key) { ... } }produces an entry per generated name, all at the block's location — and every name whose generated wrapper no test happened to call showed as an uncovered method on a line with full line and branch coverage. A source location is the unit a file-based report can express, and regulardefs map one location to one name, so they are unaffected. The same identity is used when merging resultsets across processes. This also covers methods copied into refinements viaimport_methods, which Ruby records once per importing refinement at the shared module's original location, so exercising the method through any refinement now marks the shared definition covered and theskipworkaround for shared refinement modules can be dropped. Reported with exemplary diagnoses by @bkuhlmann. See #1234 and #1237.SimpleCov.formatterandSimpleCov.formattersnow accept formatter instances in addition to formatter classes, so constructor options can actually be passed — most notablySimpleCov::Formatter::HTMLFormatter.new(silent: true)to suppress the "Coverage report generated" status line. Previously SimpleCov unconditionally called.newon whatever was configured, so passing an instance crashed withNoMethodErrorat report time. See #1240.Performance
1.0.0as a result of usingRipper#parsein a hot path) by adding parsed key memoisation toRubyDataParser.call.v1.0.2Compare Source
==================
Bugfixes
simplecovCLI's colorizing subcommands (report,uncovered,coverage,diff) no longer crash withNoMethodError: undefined method 'color'when run in a project without a.simplecovfile. The CLI deliberately loads onlysimplecov/clirather than the full library, soSimpleCov.colorwas undefined unless a dotfile load had incidentally defined it — and--no-colorwas the only workaround, since the documentedNO_COLORenv var was checked after the line that raised.Color.enabled?now treats missing configuration the same as its:autodefault and falls through toNO_COLOR/FORCE_COLOR/ TTY detection. Reported with an exemplary diagnosis by @hasghari. See #1231.Coveragefor a safe-navigation call that takes a block. Forx&.foo { ... }(and the second link of a chain likex&.foo&.bar { ... }) the extractor keyed the branch on the call node's full source range, which extends through the attached block, whileCoverageends the range at the call itself — so a simulated entry merging with a real one produced a phantom, permanently-missed branch, the same failure mode as theelsiffix in 1.0.1. Reported with an exemplary diagnosis and a suggested fix by @alexdeng-mp. See #1233.StaticCoverageExtractoragainst Ruby'sCoverage— a fuzzing harness that runs thousands of generated programs through both and diffs the branch tuples, now part of the spec suite (opt-in viaSIMPLECOV_FUZZ=1) — surfaced and fixed four more mismatches of the same phantom-branch class. Conditions that are compile-time literals (if true,if 1, a ternary on a literal) are folded away by Ruby's compiler and no longer produce synthesized branches (while truestill does — loops are not folded). On Ruby 3.3, three legacy conventions now match: the body range of a do-while (begin ... end while), the location of empty branch arms (which on 3.3 depends on whether the construct is in value or void position), and one-line pattern matching (x => pattern/x in pattern), which emits a:casebranch on 3.3 and nothing on 3.4+. The audit also caught a crash on Ruby 3.3's stdlib Prism (0.19), which still exposes the else clause ofUnlessNode/CaseNode/CaseMatchNodeunder its pre-1.3 nameconsequent: the extractor raised internally and silently dropped simulated branch and method data for any file containingunless/elseor acasewith an empty arm, unless a newer prism gem happened to be installed.define_method/define_singleton_methodblocks defined onto more than one receiver — e.g. a module'sincludedhook defining the same block on every including class. Ruby records one method entry per receiver, all pointing at the same source location, so any receiver whose copy was never called showed as an uncovered method on a line with 100% line coverage. Entries are now aggregated by (name, source location) with hit counts summed, and cross-process merging matches methods on the same source identity rather than on the receiver class. Reported with an exemplary diagnosis by @bkuhlmann. See #1234.enable_coverage :evalno longer inflates denominators or reports phantom missed branches for templates compiled more than once — e.g. hanami-view compiles each template once per view class, and everyERB.new(...).resultis a fresh compile. Ruby'sCoverageemits a fresh set of branch entries per compile of the same file (nondeterministically through Ruby 4.0, consistently on current ruby master — see https://bugs.ruby-lang.org/issues/22203), each counting only the renders that flowed through that compile, so a side exercised under one compile appeared as a permanently-missed branch in another compile's entry at the same location, andignore_branches :implicit_elseswung the report wildly by stripping only the synthetic-else halves of the duplicates. Duplicated conditions are now aggregated by source location with arm counts summed. Reported with an exemplary diagnosis by @bkuhlmann. See #1235.v1.0.1Compare Source
==================
Enhancements
sig/, covering the public API: the configuration DSL (including the criterion-scopedcoverageblock and the legacy deprecated verbs), theResult/FileList/SourceFile/CoverageStatisticsread API that formatter authors consume, the formatter and filter class hierarchies, exit codes, and theParallelAdapters::Basecontract. Internal classes carry repository-only skeleton signatures (sig/internal/, excluded from the gem package) so the entire codebase type-checks under Steep in strict mode, while the shipped signature payload stays small. Signatures are checked withrbs validateandsteep checkas part of the default rake task. RBS and Steep users no longer need the third-party signatures fromruby/gem_rbs_collection, which cover the 0.22 API and predate 1.0's configuration redesign.Bugfixes
Coverageexactly forelsifand forifarms with empty bodies.StaticCoverageExtractorattributed the outer else arm of anelsifto the clause's body rather than the whole clause, and an emptyifthen-body to the whole node rather than Coverage's zero-width point at the predicate's end. Since resultset merges combine branch arms by their exact location, a simulated entry merging with a real one for the same file (parent and worker under Minitest'sparallelize, or RSpec and Minitest suites collated together) produced phantom, permanently-missed branch arms. A new differential spec now pins every branch construct tuple-for-tuple against Ruby'sCoverage— which promptly caught that CRuby 3.4 changed several of these conventions, so the extractor now emits whichever shape the running Ruby'sCoverageuses (on 3.2/3.3: elsif clause ranges end at the chain's last content rather than the sharedend, emptyif/else/whenbodies fall back to enclosing ranges, and emptywhile/inbodies collapse to points). Reported with an exemplary diagnosis by @hasghari. See #1226.merge_subprocessesno longer silently drops all worker coverage under Minitest's fork-basedparallelize(workers: N)(the setup therailsprofile exists for). When Minitest's autorun was armed beforeSimpleCov.start— which is howrails testloads — SimpleCov deferred its report toMinitest.after_run, and forked workers inherited that deferral even though Minitest pins itsafter_runhook to the parent's pid, so no exit path in the worker ever stored its resultset. Workers now reset the inherited at_exit state on fork and re-arm their own hook, so their resultsets are stored and merged as documented. Reported with an exemplary diagnosis by @hasghari. See #1227.SimpleCov.formatters=raisingNoMethodErrorwhen given a single formatter instead of an Array — a regression from 0.22.x, whereMultiFormatter.newnormalized the value internally. This restores the long-documentedSimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([...])pattern, in whichMultiFormatter.newreturns a Class rather than an Array. The regression surfaced in ruby/ruby's CI through net-imap's test helper. Thanks @koic. See #1224.Kernel#warn. They still print to stderr, but they are program output rather than Ruby warnings, soWarning.warnhooks — warning trackers and raise-on-warning test setups — no longer intercept them as unaddressable noise, and threshold failure explanations now surviveruby -W0, which previously reduced a failing check to a bare exit code with no explanation. Genuine warnings (deprecations, dropped-file notices, parse failures) still usewarn. Suppression remains explicit:silent: truefor formatter status lines,print_errors falsefor enforcement output. Thanks @viralpraxis. See #1225.v1.0.0Compare Source
==================
First stable release of the 1.0 line. The entries below consolidate release candidates rc1 through rc5 and describe all changes since 0.22.1.
Breaking Changes
RUBY_VERSION3.4). Ruby 3.1 reached end of life in March 2025, and a recenti18nrelease callsFiber[], a Ruby 3.2 API, at load time, so suites that load Rails no longer run on 3.1. Raisingrequired_ruby_versionto>= 3.2also excludes JRuby 9.4, which reportsRUBY_VERSION3.1.x. See #1171.{ "covered_percent": 80.0 }to full stats shape{ "covered": 8, "missed": 2, "total": 10, "percent": 80.0, "strength": 0.0 }. The keycovered_percentis renamed topercent.simplecov_json_formattergem is now built in.require "simplecov_json_formatter"continues to work via a shim.StringFilternow matches at path-segment boundaries."lib"matches/lib/but no longer matches/library/. Use aRegexpfilter for substring matching.SourceFile#project_filenamenow returns a truly relative path with no leading separator (e.g.lib/foo.rbinstead of/lib/foo.rb). This also removes the leading/from file path keys incoverage.jsonand from the filename inminimum_coverage_by_fileerror messages. AnchoredRegexFilters that relied on a leading/(e.g.%r{^/lib/}) should be rewritten (e.g.%r{\Alib/}).docilegem dependency. TheSimpleCov.configureblock is now evaluated viainstance_execwith instance variable proxying.JSONFormatterwhen theCC_TEST_REPORTER_IDenvironment variable is set. The defaultHTMLFormatternow emitscoverage.jsonalongside the HTML report (usingJSONFormatter.build_hashto serialize the same payloadJSONFormatterwrites), so the env-var special case is no longer needed. Because of this, listingJSONFormatteralongsideHTMLFormatteris redundant and can be removed.SimpleCov.startnow loads thetest_frameworksprofile by default, which filters paths undertest/,spec/,features/, andautotest/. Running the suite always executes 100% of the test files themselves, which inflated the overall percentage and obscured application coverage. To opt back in (e.g. to surface dead test helpers), drop the filter withremove_filter %r{\A(test|features|spec|autotest)/}. See #816.rspec -f json. Suppress it entirely withsilent: trueon the formatter; redirect with2>&1if you want the old behavior. See #1060.parallel_tests, SimpleCov now waits in the first started process (viaParallelTests.first_process?) rather than the last. This matches the conventionparallel_tests's own README recommends for "do something once after all workers finish" hooks, so user code that has its ownParallelTests.wait_for_other_processes_to_finishin anRSpec.after(:suite)(or equivalent) no longer deadlocks against SimpleCov's wait when both pick the same process. As a side benefit, the previousPARALLEL_TEST_GROUPS=1workaround forlast_process?'s"" == "1"mismatch (#1066) is no longer needed —first_process?handles that case naturally. Migration: the rare project that wired its own wait viaParallelTests.last_process?now hits the symmetric deadlock and must switch tofirst_process?. See #922.SimpleCov.coverage_criterion. It was a reader/writer for a value nothing in SimpleCov ever consumed, so it duplicatedprimary_coveragewithout affecting any behavior. Useprimary_coverageto choose the report's leading criterion (or thecoverage :branch, primary: trueform).Deprecations
add_filter→skip(identical matcher grammar; no behavior change)add_group→group(identical matcher grammar; no behavior change)track_files→cover(coverincludes unloaded files liketrack_filesdid and restricts the report to the matching set; pass every directory you want reported, e.g.cover "lib/**/*.rb", "app/**/*.rb", to keep the old additive-only behavior)use_merging→merging(same value)enable_for_subprocesses→merge_subprocesses(same value)enable_coverage_for_eval→enable_coverage :eval(folds into the same call that enables:line/:branch/:method)print_error_status(reader) →print_errors(theprint_error_status=writer is unaffected for now)SimpleCov.startfrom.simplecovis deprecated. Coverage tracking still begins for backward compatibility, but a one-time deprecation warning fires pointing the user at moving the call intospec_helper.rb/test_helper.rb; a future release will require the explicitSimpleCov.startfrom a test helper. The migration goes hand-in-hand with the bugfix below: onceSimpleCov.startlives in the test helper, the parent process that auto-loads.simplecovnever starts tracking and the empty-report-overwrite scenario can't arise. See #581.# :nocov:toggle comments (and the configurableSimpleCov.nocov_token/SimpleCov.skip_token) are deprecated in favor of the new# simplecov:disable/# simplecov:enabledirectives. 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 toSimpleCov.nocov_tokenorSimpleCov.skip_token(getter or setter) likewise warns. The directive will be removed in a future release.SimpleCov::SourceFile#branches_coverage_percentand#methods_coverage_percentare deprecated in favor of the uniformcovered_percent(:branch)/covered_percent(:method).covered_percent(andcovered_strength) now take a criterion argument (defaulting to:line), so the same call reaches any criterion instead of line being the unprefixed default while branch and method had their own differently-named methods.coverage_statisticsalso now accepts a criterion (e.g.coverage_statistics(:branch)) to return that oneCoverageStatisticsrather than the whole Hash.minimum_coverage_by_fileandminimum_coverage_by_groupare deprecated in favor of thecoveragemethod'sminimum_per_file/minimum_per_groupverbs. The legacy methods overloaded a single hash to carry both per-criterion defaults and per-path / per-group overrides, withminimum_coverage_by_filefurther distinguishing Symbol keys (criterion defaults) from String / Regexp keys (path overrides) and accepting either a bare number or a per-criterion hash as the value. Thecoverageblock fixes the criterion so every threshold is a plain percentage with anonly:target. The setter form emits a deprecation warning naming the replacement; the no-arg getter (read internally) is unchanged. Replace e.g.minimum_coverage_by_file line: 70, 'app/x.rb' => 100withcoverage(:line) { minimum_per_file 70; minimum_per_file 100, only: 'app/x.rb' }. See the "Per-criterion thresholds withcoverage" README section.Enhancements
simplecov uncoveredgained--criterion line|branch|method(defaultline) so the lowest-coverage listing can rank by branch or method coverage, not just line.coverageconfiguration 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-linercoverage :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(withonly:String-path / Regexp overrides), andminimum_per_group. Options:primary:(the report's leading criterion),oneshot:(oneshot-lines mode for:line), and:eval. The flatminimum_coveragefamily remains as suite-wide sugar. Thresholds feed the same internal stores, so exit-code enforcement is unchanged. SeeConfiguration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.