Add deprecations boot to capture load-time deprecations - #194
Conversation
Capture deprecation warnings emitted while the app loads and eager-loads, the surface the per-example test tracker structurally misses (association / scope / callback declaration warnings that fire when a class body is evaluated). It does not reimplement capture: a gem-shipped runner script drives the existing DeprecationTracker (init_tracker installs the version-correct hooks across Rails 2-8.1 -- Rails.application.deprecators on 7.1+, the ActiveSupport::Deprecation singleton before that, plus KernelWarnTracker), sets a bucket, eager-loads so declaration-time warnings fire while it listens, then after_run writes an ordinary shitlist. BootCapture only builds the boot command and resolves the shitlist path (spec/support convention, ".boot" marker, ".next" for the next bundle).
`deprecations boot` (and `--next boot`) boots the app, runs the BootCapture runner, and reuses print_info to summarize the resulting shitlist. Adds the --output option and help text. A boot failure is reported honestly: the result file is cleared first, and a non-zero exit or missing output prints "Boot did not complete ... NOT a clean result" and exits 1, so a crashing boot is never mistaken for "no deprecations." Selecting the next bundle uses BUNDLE_GEMFILE=Gemfile.next rather than bin/next, which need not exist in every project.
Covers the boot command shape (RAILS_ENV=test, output env, BUNDLE_GEMFILE for --next, no bin/next), the spec/support default paths, and that the shipped runner is valid Ruby that reuses DeprecationTracker and always eager-loads.
Adds a Boot-time deprecations section (what it captures, the two commands, output path) plus a note on when --next applies, and lists the mode in the Features and command sections.
References PR #194 (the predicted next number). Update the link if the PR is assigned a different number when opened.
rails runner fully boots the app before this script loads, so an env with config.eager_load = true has already eager-loaded and the declaration-time warnings fired before the tracker could attach. Re-running eager_load! is blocked by Zeitwerk's @eager_loaded guard, so the capture silently reported "clean". Pass CI= so the stock Rails 7.1+ test env (eager_load = ENV["CI"].present?) does not eager-load, and refuse with a distinct exit code for apps that hardcode it, which the CLI surfaces instead of its generic boot-failure guess.
rails runner finishes initialize! before the runner script loads, and init_tracker appends to the deprecators registered at that moment, so warnings from gem requires and initializers are already gone. The docs and the success message claimed "load-time" coverage the command does not have; the strongest case was "no deprecation warnings at load time" printed on an empty result. Narrow the wording to eager-load and state the out-of-scope surface where each claim is made. Those warnings still print to stderr during this boot, so they are visible, just not collected.
A silenced deprecator (config.active_support.report_deprecations = false, or ActiveSupport::Deprecation.silenced = true) returns early in Reporting#warn before behavior is consulted, so capture recorded nothing and reported a false "clean". A :raise-configured env aborts on the first eager-load warning before after_run. Both defeat capture. Before attaching the collector, un-silence and force :stderr / disallowed :stderr via the collection-level setters, which update the collection's stored options so deprecators a gem/engine registers during eager-load inherit the non-fatal setup instead of the app's :raise. Mirrors init_tracker's deprecators-vs-singleton version fork.
boot_command interpolated the operator-supplied --output path and the gem's RUNNER_PATH into one string passed to system, so a path containing spaces broke the run and --output 'x.json; rm -rf foo' executed. Both surfaced as the misleading "app failed to load / check database.yml". Return [env_hash, *argv] and call system(*command), which bypasses the shell entirely. Add command_display for the logged line, which is rendered but never executed. CI is now unset rather than set to an empty string, so apps testing ENV["CI"] for truthiness also see it absent.
run_boot deleted the output file before running, so a boot that failed or refused left the user with no capture at all, including the good one from last time. Write to a sibling .partial file and rename it into place only after the runner succeeds; clear the partial on both failure paths. Same-directory rename, so the replacement is atomic.
The boot capture writes to spec/support/deprecation_warning.boot.shitlist.json, but info/run/merge hardcoded the test-run path, so the README's claim that you could read it with info was false. --path selects the file to read (overriding --next); --output still selects where boot writes.
Pass the Rails.root-stripping transform_message so the boot shitlist stores project-relative, committable paths (matching the RSpec/Minitest setups), and require tempfile/fileutils in deprecation_tracker so save/diff work under `rails runner` in a slim app where those stdlib files aren't already loaded — previously a NameError that run_boot misreported as a boot failure. The stdlib requirement is covered by a subprocess spec (bare Ruby, no RSpec preloading) that reproduces and guards the crash.
boot no longer silently ignores --pattern (rejects it, pointing to `info --path`). print_info derives the bucket label from the data, so a boot shitlist shows "Source" instead of mislabeling the synthetic "boot" bucket a test file — on both the post-capture summary and a later `info --path`. `run --path` is rejected (its buckets may not be spec files) and a missing shitlist aborts with a message instead of a raw Errno::ENOENT. boot_command declares output_path as an optional kwarg + guard rather than a required kwarg, so the file parses on Ruby 2.0 — the gemspec's floor, which the rest of the lib keeps to (it was the only required kwarg present).
boot_command set only BUNDLE_GEMFILE=Gemfile.next for --next, so the next boot resolved against the current bundle's vendored cache. `next` and gem-next-diff always pair Gemfile.next with BUNDLE_CACHE_PATH=vendor/cache.next; do the same. The current bundle keeps Bundler's defaults (Gemfile, vendor/cache).
run_boot's branching lived inline in exe/deprecations, which no spec loads, so the partial-promotion and cleanup logic was unverified. Move the partial path and the success/failure/refusal classification onto BootCapture and cover them directly. Also switch the runner syntax check to `ruby -c`, since RubyVM::InstructionSequence is MRI-only.
exe/deprecations required rainbow, which the gemspec never declares as a runtime dependency, so the shipped executable raised LoadError on a clean install and only worked where rainbow happened to be present for another reason. The gem already has a dependency-free ANSI wrapper used throughout lib/. Add the underline style it was missing and switch the CLI to it.
`deprecations run` called DeprecationTracker.sanitize_mode, which is defined in deprecation_tracker.rb, but the CLI only required valid_modes/shard_merger/boot_capture. Every invocation of run mode raised NoMethodError before reaching any of its own logic.
No spec loaded the CLI, which is how an undeclared rainbow require and a NoMethodError in run mode both shipped. Execute the real script in a subprocess and cover every mode: info's summary, filtering and labelling, merge, the flag guards, and unknown/missing modes. Drive boot's three outcomes with a stub `bundle` on PATH, so the partial promotion, the preserved previous capture on failure, and the refusal exit code are verified end to end rather than only through boot_result.
- boot only captures (save mode); compare stays test-run-only - describe the real failure modes (load failure exit 1, eager-load refusal exit 3) instead of a vague "no deprecations" - list `info --path` in the deprecations command reference
exe/deprecations pushed the gem's lib onto $LOAD_PATH so that `require "next_rails/tint"` inside deprecation_tracker.rb would resolve when the script ran out of a checkout rather than an installed gem. Resolving both the tracker requires and the tint require relatively removes the need for that: the paths are anchored to the files themselves, so the script works the same installed or from a checkout, and no global load path is mutated. boot_capture_runner.rb keeps its by-name requires; it runs via `bundle exec rails runner` in the target app, where the gem is a bundle dependency and already on the load path.
|
Closing in favor of #199, which solves the same problem from a better attach point. This PR runs the capture inside Measured on freshly generated Two smaller things #199 also resolves: the eager-load refusal path disappears, since attaching before The two CLI bugfixes that were bundled into this branch shipped separately as #197 (undeclared |
What
The
DeprecationTrackeronly sees what a test actually exercises, and it only attaches once the suite is running. Deprecations that fire while the app itself loads (association, scope, and callback declarations evaluated as the class bodies load) either happen before the tracker exists or never get exercised at all, so they don't make it into a shitlist.This adds a way to get those boot-time deprecations.
deprecations bootkeeps the app from eager-loading on its own, attaches the tracker first, and then eager-loads, so the declaration-time warnings get recorded.It writes an ordinary shitlist to
spec/support/deprecation_warning.boot.shitlist.jsonand summarizes it the same wayinfodoes.These are deprecations, so they flag the APIs Rails is going to remove. Capturing them up front, even on an app with a thin suite, lets us clear them on our own schedule instead of scrambling during the bump.
Scope
This is eager-load deprecation capture, not boot smoke testing. It tells you which deprecated APIs the app's own code declares. It does not tell you whether the app boots or serves a request under the next bundle.
Notes
DeprecationTrackerrather than adding new capture logic (init_tracker, eager-load,after_run). That also means it talks to whichever deprecator the Rails version exposes:Rails.application.deprecatorson 7.1+, and theActiveSupport::Deprecationsingleton before that. So it works from Rails 2 through 8.1.--nextonly gives you the full set once the next bundle already boots (dual boot is green). Before that, eager-load stops at the first breaking change, one at a time. When a boot fails or is refused, the command exits non-zero and says why, instead of pretending the app is clean.rainbow, which is no longer a dependency, so it wouldn't even load on a clean install; anddeprecations runblew up with aNoMethodErrorbefore doing any work.This is a starting point proposal, happy to hear other approaches.