Skip to content

Simplify formatter architecture#1165

Open
sferik wants to merge 5 commits intomainfrom
simplify-formatter-architecture
Open

Simplify formatter architecture#1165
sferik wants to merge 5 commits intomainfrom
simplify-formatter-architecture

Conversation

@sferik
Copy link
Copy Markdown
Collaborator

@sferik sferik commented Apr 5, 2026

This PR builds on #1164 to separate data from presentation in the coverage report by making the HTML formatter a client-side JavaScript app that reads structured JSON data, rather than a server-side Ruby ERB renderer.

Both formatters share a single entry point for data serialization: JSONFormatter.build_hash(result), a class method with no side effects that converts a SimpleCov::Result into a Ruby hash. The JSON formatter calls build_hash and writes the result to coverage.json. The HTML formatter calls the same method, wraps the result as window.SIMPLECOV_DATA = {...}; in a JavaScript file, and copies pre-compiled static assets (index.html, application.js, application.css, and favicons) into the output directory. There is no ERB and no Ruby-side rendering.

The JavaScript app reads window.SIMPLECOV_DATA on page load and renders everything client-side. Source files are rendered on demand when the user clicks into them.

The ERB templates, Ruby view and coverage helpers, ResultExporter, and SourceFileFormatter have been deleted, removing about 2,700 lines of code. In their place, a static index.html and client-side rendering logic in TypeScript add about 1,500 lines, for a net reduction of roughly 1,200 lines.

The JSON output has been enriched with source code, coverage counts, branch display metadata, group file membership, and result metadata. The formatter class hierarchy has been flattened from 5 classes to 3: HTMLFormatter, JSONFormatter, and ResultHashFormatter.

This gives a clean separation of data and presentation. The HTML report can be regenerated from a saved coverage.json without re-running tests via HTMLFormatter#format_from_json. The coverage.json format becomes a rich, self-contained data API that other tools and third-party formatters can consume directly. The static HTML, JS, and CSS assets are pre-compiled once via rake assets:compile and ship with the gem. No runtime template rendering is needed.

This comment was marked as resolved.

@sferik sferik force-pushed the simplify-formatter-architecture branch 4 times, most recently from 1478ee1 to 85a78d1 Compare April 7, 2026 02:36
Comment thread spec/fixtures/json/sample_with_branch.json
Comment thread lib/simplecov/formatter/json_formatter.rb Outdated
Replace server-side ERB template rendering with a static single-page
JavaScript app. The HTML formatter now writes coverage_data.js
(a thin JS wrapper around the JSON coverage data) and copies
pre-compiled static assets. All rendering — file lists, source code
views, coverage bars, group tabs — happens in the browser.

This cleanly separates data (JSON) from presentation (JS/HTML/CSS):

* Delete ERB templates, Ruby view helpers, and coverage helpers
* Move all rendering logic into TypeScript (app.ts)
* Add static index.html as a pre-compiled asset via rake assets:compile
* Flatten formatter class hierarchy (5 classes → 3)
* Introduce JSONFormatter.build_hash for shared, side-effect-free
  serialization used by both formatters
* Eliminate double JSONFormatter execution when both formatters
  are configured
@sferik sferik force-pushed the simplify-formatter-architecture branch from 85a78d1 to 827afd3 Compare April 17, 2026 00:56
sferik added 2 commits April 16, 2026 18:29
Per-file coverage keys, group file lists, and minimum_coverage_by_file
error keys now use `project_filename` (root-relative) instead of
`filename` (absolute). Consumers already have `meta.root` if they
need to reconstruct an absolute path.

The frontend's `shortenFilename` simplifies to stripping the leading
`/` — no more root parameter threaded through the render pipeline.
Record the time each JSONFormatter process loads as `PROCESS_START_TIME`
and, before writing `coverage.json`, compare the existing file's
`meta.timestamp` against it. If the file on disk was written *after*
this process started, another test run likely produced it — warn that
the overwrite will lose their data and point at `SimpleCov::ResultMerger`.

The check is zero-cost for the common case (no existing file, or an
older one from a prior sequential run) and has no state beyond the
existing `meta.timestamp` field.

Sequential re-runs do not warn: the prior file's timestamp predates
the new process's start. parallel_tests-style setups, where workers
share `PROCESS_START_TIME` but finish at different times, do warn
once the later writers reach format time.
@sferik sferik force-pushed the simplify-formatter-architecture branch from 7b7f797 to f953a80 Compare April 17, 2026 01:33
@sferik sferik requested a review from brynary April 17, 2026 14:22
@brynary
Copy link
Copy Markdown
Contributor

brynary commented Apr 17, 2026

I had left two comments but with the new GH UI I can no longer find them:

  1. I'm unsure if the shortenFilename function may need to account for backslashes on Windows. (Not my jam, just caught my eye.)

  2. I would strongly recommend that the relative paths not be prefixed with a forward slash /. Consuming tools will likely interpret a forward-slash prefixed path as absolute and do the wrong thing.

The paths returned by `SourceFile#project_filename` are described as
"relative to the projects directory," but they began with a `/` because
stripping `SimpleCov.root` left the separator behind. Consumers of
`coverage.json` (and the `minimum_coverage_by_file` error line) can
reasonably interpret a `/`-prefixed path as absolute and resolve it
incorrectly.

Strip the leading `/` or `\` in `project_filename`, adjust the built-in
regex profiles (`hidden_filter`, `test_frameworks`, `rails`) and
`StringFilter#segment_pattern` to match segment boundaries without
requiring a leading slash, refresh the JSON fixtures and specs, and
remove the now-unnecessary `shortenFilename` helper from the HTML
frontend (which also sidesteps any Windows backslash concern).

Anchored user-supplied RegexFilters that relied on a leading `/`, like
`%r{^/lib/}` need to be rewritten as `%r{\Alib/}`. Noted in CHANGELOG.
@sferik
Copy link
Copy Markdown
Collaborator Author

sferik commented Apr 17, 2026

@brynary thanks for the second round for feedback.

SourceFile#project_filename now strips the leading separator so it returns a truly relative path (lib/foo.rb instead of /lib/foo.rb). That flows through to the coverage.json keys, the group file lists, the errors.minimum_coverage_by_file.* keys, and the CLI "coverage by file … is below … in " message.

Since paths are now genuinely relative, I removed the shortenFilename helper in the frontend entirely. This sidesteps the Windows backslash concern you flagged. The regex in project_filename strips / or \, so the frontend just uses the JSON values as-is.

Users with their own anchored RegexFilters like %r{^/lib/} will need to rewrite them as %r{\Alib/}. I called this out in the CHANGELOG. This could break a lot of apps, but it’s an easy fix and I agree that it’s more correct and now (ahead of the 1.0 release) is the time to make that change.

The concurrent-overwrite warning compares the timestamp in an existing
`coverage.json` against `PROCESS_START_TIME`. `Time.now` has sub-second
precision, but `iso8601` (no arg) truncates to whole seconds on the way
out, so a sibling writer landing inside the same wall-clock second as
when we started could slip past the check.

Switch `meta.timestamp` to `iso8601(3)` so the round-trip keeps
millisecond fidelity, and update the JSON fixtures and the
`json_formatter_spec` assertion to match.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants