Skip to content

Commit ed731ff

Browse files
authored
Merge pull request #95 from ombulabs/feature/validate-patterns-self-test
Add --self-test flag to bin/validate-patterns (closes #71)
2 parents 60aa902 + 3d77c4e commit ed731ff

4 files changed

Lines changed: 90 additions & 1 deletion

File tree

.github/workflows/validate-patterns.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ jobs:
2525
with:
2626
ruby-version: "3.3"
2727
- run: ruby bin/validate-patterns
28+
- run: ruby bin/validate-patterns --self-test

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This file captures project-specific conventions Claude should follow when workin
77
- `bin/validate-patterns` validates every detection pattern YAML file under `rails-upgrade/detection-scripts/patterns/`. Run it before committing any change to a pattern file. Pure-stdlib Ruby, no Bundler or Gemfile required.
88
- `bin/validate-patterns` validates every file
99
- `bin/validate-patterns path/to/file.yml` validates one or more specific files
10+
- `bin/validate-patterns --self-test` runs built-in fixture assertions covering the four positive `kind:` values and the four rejection paths (missing top-level key, missing required pattern key, broken regex, unknown `kind:` value). CI runs this alongside the file-validation step
1011
- Checks: YAML parses, required top-level keys present, the eight required pattern keys present on each entry, every `pattern` / `exclude` regex compiles, and the `kind:` value is one of the allowed enum values (`breaking`, `deprecation`, `migration`, `optional`)
1112
- Exits 0 on success, 1 on any failure with a per-file error report
1213

bin/validate-patterns

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
# Usage:
77
# bin/validate-patterns # validate every patterns file
88
# bin/validate-patterns path/to/file # validate a specific file (one or more)
9+
# bin/validate-patterns --self-test # run built-in fixture assertions
910
#
10-
# Exits 0 if all files pass, 1 otherwise.
11+
# Exits 0 if all files / assertions pass, 1 otherwise.
1112

1213
require "yaml"
14+
require "tmpdir"
1315

1416
PATTERNS_DIR = File.expand_path("../rails-upgrade/detection-scripts/patterns", __dir__)
1517
TOP_LEVEL_KEYS = %w[version description upgrade_findings].freeze
@@ -72,6 +74,90 @@ def validate(path)
7274
errors
7375
end
7476

77+
def self_test
78+
base = {
79+
"version" => "test",
80+
"description" => "self-test fixture",
81+
"upgrade_findings" => {
82+
"high_priority" => [
83+
{
84+
"name" => "Test entry",
85+
"kind" => "breaking",
86+
"pattern" => "foo",
87+
"exclude" => "",
88+
"search_paths" => ["app/"],
89+
"explanation" => "test",
90+
"fix" => "test",
91+
"variable_name" => "TEST"
92+
}
93+
]
94+
}
95+
}
96+
97+
cases = []
98+
99+
KIND_VALUES.each do |kind|
100+
doc = Marshal.load(Marshal.dump(base))
101+
doc["upgrade_findings"]["high_priority"][0]["kind"] = kind
102+
cases << { name: "valid kind=#{kind}", doc: doc, expect: :pass }
103+
end
104+
105+
missing_top = Marshal.load(Marshal.dump(base))
106+
missing_top.delete("upgrade_findings")
107+
cases << { name: "missing upgrade_findings", doc: missing_top, expect: :fail, match: /missing top-level key: upgrade_findings/ }
108+
109+
missing_kind = Marshal.load(Marshal.dump(base))
110+
missing_kind["upgrade_findings"]["high_priority"][0].delete("kind")
111+
cases << { name: "missing pattern key kind", doc: missing_kind, expect: :fail, match: /missing key: kind/ }
112+
113+
bad_regex = Marshal.load(Marshal.dump(base))
114+
bad_regex["upgrade_findings"]["high_priority"][0]["pattern"] = "[unclosed"
115+
cases << { name: "broken regex", doc: bad_regex, expect: :fail, match: /pattern regex does not compile/ }
116+
117+
bad_kind = Marshal.load(Marshal.dump(base))
118+
bad_kind["upgrade_findings"]["high_priority"][0]["kind"] = "bogus"
119+
cases << { name: "unknown kind value", doc: bad_kind, expect: :fail, match: /invalid kind: "bogus"/ }
120+
121+
passed = 0
122+
failed = []
123+
124+
Dir.mktmpdir do |dir|
125+
cases.each_with_index do |c, i|
126+
path = File.join(dir, "fixture_#{i}.yml")
127+
File.write(path, c[:doc].to_yaml)
128+
errors = validate(path)
129+
130+
ok =
131+
case c[:expect]
132+
when :pass then errors.empty?
133+
when :fail then errors.any? { |e| e =~ c[:match] }
134+
end
135+
136+
if ok
137+
passed += 1
138+
else
139+
failed << { name: c[:name], expected: c[:expect], errors: errors }
140+
end
141+
end
142+
end
143+
144+
total = cases.size
145+
if failed.empty?
146+
puts "OK self-test (#{passed}/#{total} passed)"
147+
0
148+
else
149+
puts "FAIL self-test (#{passed}/#{total} passed)"
150+
failed.each do |f|
151+
puts " - #{f[:name]} (expected #{f[:expected]}, got errors: #{f[:errors].inspect})"
152+
end
153+
1
154+
end
155+
end
156+
157+
if ARGV.include?("--self-test")
158+
exit self_test
159+
end
160+
75161
paths =
76162
if ARGV.empty?
77163
Dir.glob(File.join(PATTERNS_DIR, "*.yml")).sort

rails-upgrade/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- `bin/validate-patterns` now **requires** the `kind:` field on every pattern entry (issue #53 rollout, sub-issue #67). All 12 pattern files have been classified across the preceding 12 sub-issues. Updated `CLAUDE.md` to list `kind:` among the required per-pattern fields and to remove the rollout caveat.
66
- Renamed the top-level pattern-file key `breaking_changes:``upgrade_findings:` (issue #53 rollout, sub-issue #68). The old name was a misnomer — most entries are deprecations, migrations, or opt-in features rather than hard-breaking changes. Touched all 12 `rails-*-patterns.yml` files, `bin/validate-patterns` (the `TOP_LEVEL_KEYS` constant and the lookup variable in `validate`), and `rails-upgrade/workflows/direct-detection-workflow.md` (5 references). The validator now rejects files that still use the old key.
77
- Updated `workflows/direct-detection-workflow.md` to surface the new `kind:` field in detection output (issue #53 rollout, sub-issue #69). Findings are now grouped into two top-level buckets: **fix-before-bump** (`kind: breaking` and `deprecation`) and **fix-when-ready** (`kind: migration` and `optional`). Within each bucket, `priority` (HIGH / MEDIUM / LOW) drives sub-ordering. Putting `breaking` and `deprecation` together reflects the practical reality that deprecations warn in production logs at this hop and typically become hard breaks at the next — addressing them in the same upgrade campaign is cheaper than splitting the work across two upgrades. `migration` and `optional` are silent at this hop and don't compete for the user's attention during the upgrade itself.
8+
- Added a `--self-test` flag to `bin/validate-patterns` (closes #71). Running `bin/validate-patterns --self-test` writes valid + invalid fixture YAMLs to a tmpdir and asserts the validator's behavior end-to-end: four positive cases (one per `kind:` value) and four rejection paths (missing top-level key, missing required pattern key, broken regex, unknown `kind:` value). Wired into the GitHub Actions workflow. Closes the test-coverage gap surfaced during PR #70 review where the new enum guard had no automated coverage. Single-file, stdlib-only — no Bundler, no test framework.
89

910
## v3.3.0, 28 April 2026
1011
- Added `upgrade-cleanup` companion plugin for finishing a Rails upgrade campaign. Removes dual-boot scaffolding, drops `NextRails.next?` / `NextRails.current?` branches, retires stale monkey-patches and version-conditional code, and aligns CI matrix / Dockerfile / Ruby pin to the new version baseline. Lives as a sibling plugin in this repo (`upgrade-cleanup/.claude-plugin/plugin.json` + `upgrade-cleanup/upgrade-cleanup/SKILL.md` + `upgrade-cleanup/upgrade-cleanup/workflows/upgrade-cleanup-workflow.md`). `load_defaults` alignment and deprecation triage stay with the rails-upgrade skill, cleanup deliberately does not duplicate them. Based on FastRuby.io's "Finishing an Upgrade" methodology. (Closes #1)

0 commit comments

Comments
 (0)