Skip to content

spec: opt-in line coverage for .lic scripts - #7547

Open
simtel12 wants to merge 3 commits into
elanthia-online:mainfrom
simtel12:coverage-in-spec-helper
Open

spec: opt-in line coverage for .lic scripts#7547
simtel12 wants to merge 3 commits into
elanthia-online:mainfrom
simtel12:coverage-in-spec-helper

Conversation

@simtel12

Copy link
Copy Markdown
Contributor

Adds opt-in line/branch coverage for the .lic scripts:

COVERAGE=1 bundle exec rspec     # report in coverage/ (already gitignored)

Default runs are untouched — without COVERAGE set, nothing here loads simplecov or does any extra work.

Current picture across the 39 specced scripts: 6391 / 16401 lines (39.0%), branch 27.3%. The other 183 scripts have no spec at all.

Why this needed more than SimpleCov.start

I tried the obvious thing first and got a report claiming 0 covered lines. Three separate problems, each of which silently produces a wrong number rather than an error:

1. The specs never require the scripts. load_lic_class evals a line-slice of the .lic, and Ruby only measures eval'd source when Coverage is set up with eval: true. Without enable_coverage :eval the report is empty. Starting simplecov at the top of spec_helper.rb is early enough — .rspec's --require spec_helper runs before any *_spec.rb loads, so every extractor call still lies ahead of it.

2. The extractors built the path as <root>/spec/../foo.lic. Its project-relative form is spec/../foo.lic, which simplecov's default test-directory filter discards — so the scripts were being thrown out of their own coverage report. The new lic_path helper expands the path, after which the default filters are correct and no filter configuration is needed. It also means backtraces from eval'd code point at the file's real path.

3. Ruby sizes a file's eval-coverage line array on the first eval attributed to that filename and never grows it. A multi-class .lic is eval'd once per class, so any class starting past the end of the first-eval'd one was dropped — not counted as missed, just absent. combat-trainer.lic was reporting 2 of its 11 classes. prime_lic_coverage sizes the array once per file up front; it is a no-op unless Coverage.running?.

Un-eval'd code is now counted as missed

Only the class/module bodies a spec extracts ever run, so the rest of a script — the before_dying block, the Klass.new entry point, a top-level def — had no coverage entry at all. That is not the same as being uncovered: those lines were dropped from the denominator, which flattered every percentage. They are now backfilled from simplecov's static line classifier (the same one it uses for files that were never loaded) at ~390 lines across the suite.

The backfill is deliberately restricted to lines outside every range an extractor eval'd, recorded in LIC_EVAL_RANGES. Inside an eval'd range Ruby is authoritative and the static classifier is not: it calls each element line of a multi-line literal relevant, while Ruby attributes the whole literal to one line. Without the restriction, astrology.lic's OBSERVE_SUCCESS_PATTERNS had its opening line reported missed while the next line showed a hit — 23 invented missed lines across the suite.

Extractor consolidation

automap_spec and moonwatch_spec each defined their own top-level load_lic_module, and dependency_spec built its own path. Two identical top-level defs in different spec files is exactly the load-order hazard spec_helper.rb's own header warns about, so load_lic_module moves into spec_helper and the three specs use the shared extractors.

That was not only tidiness — it was also why automap.lic and dependency.lic were missing from the report entirely and moonwatch.lic was losing its five modules.

Two smaller fixes while in here:

  • load_lic_constant passes the constant's real line number to eval instead of defaulting to line 1.
  • load_lic_constant uses index rather than find, so the line number is available.

Note on the dependency

simplecov is added to the :development group. It is only loaded when COVERAGE is set, but it does become a bundle dependency. If that is unwelcome I am happy to move the whole thing behind an optional group instead — say the word.

Test plan

  • bundle exec rspec — 2977 examples, 0 failures (unchanged from main)
  • COVERAGE=1 bundle exec rspec — same 2977 examples, plus coverage/index.html
  • bundle exec rubocop spec/ — clean
  • Spot-check coverage/index.html: combat-trainer.lic shows all 11 classes, and its before_dying block reads as missed rather than being absent

`COVERAGE=1 bundle exec rspec` now produces a real coverage report in
coverage/ (already gitignored). Three things were in the way:

1. The specs never require the .lic files -- load_lic_class evals a slice
   of them -- and Ruby only measures eval'd source when Coverage is set up
   with eval: true. Without that the report is 0% covered, which is what
   made it look like coverage simply did not work here.

2. The extractors built the path as "<root>/spec/../foo.lic", whose
   project-relative form is "spec/../foo.lic" -- which SimpleCov's default
   test-directory filter discards. Expanding the path via the new lic_path
   helper makes the default filters correct, so no filter surgery is
   needed. It also means backtraces point at the file's real path.

3. Ruby allocates a file's eval-coverage line array on the first eval
   attributed to that filename and never grows it. A multi-class .lic is
   eval'd once per class, so any class starting past the end of the
   first-eval'd one was silently dropped -- not counted as missed, just
   absent. combat-trainer.lic reported 2 of its 11 classes (791 relevant
   lines); it now reports all 11 (3724). prime_lic_coverage sizes the array
   once per file, and is a no-op unless Coverage is running.

While wiring this up: automap_spec and moonwatch_spec each defined their
own top-level load_lic_module, and dependency_spec built its own path.
Two identical top-level defs in different spec files is the load-order
hazard spec_helper's own header warns about, so load_lic_module moves to
spec_helper and the three specs use the shared extractors. That also
stopped automap.lic and dependency.lic being dropped from the report and
moonwatch.lic losing its five modules.

load_lic_constant now passes the constant's real line number to eval
instead of defaulting to line 1.

Suite is unchanged at 2922 examples with and without COVERAGE set.
Only the class/module bodies a spec extracts are ever eval'd, so the rest
of a script -- the before_dying block, the `Klass.new` entry point, a
top-level def, a multi-line constant -- had no coverage entry at all. That
is not the same as being uncovered: those lines were dropped from the
denominator entirely, which flattered every percentage.

Backfill them at exit from SimpleCov's own static line classifier, the
same one it uses for files that were never loaded. Real measurements
always win; this only fills nil holes with 0.

Across the 39 specced scripts this moves the denominator 16007 -> 16424
relevant lines with the covered count unchanged at 6336, so the reported
figure goes 39.6% -> 38.6%. combat-trainer.lic now shows its before_dying
cleanup and ct_safe_eval as missed rather than omitting them.
The static-classifier backfill was inventing missed lines inside code that
demonstrably ran. SimpleCov's line classifier calls every element line of a
multi-line literal relevant, while Ruby attributes the whole literal to a
single line, so a constant like astrology.lic's OBSERVE_SUCCESS_PATTERNS
came out with its opening line reported as missed even though the array is
built (Ruby records the hit on the following line).

Inside a range that was eval'd, Ruby is authoritative and the static
classifier is not, so restrict the backfill to lines outside every range an
extractor touched. The extractors now record those ranges in
LIC_EVAL_RANGES.

Drops 23 bogus missed lines: 16424 -> 16401 relevant, covered unchanged at
6336. astrology.lic keeps its before_dying block and `Astrology.new.run`
marked missed, which is the point, but no longer claims its constants are.
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c093f882-bbec-4080-b1c4-7c88ec552ffa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant