Property Tests

SkillDocs & knowledge

Use when adding or changing a rule's detection or safe-fix behaviour, or editing any property-test suite (safe-fix / rule-checker / markdown-fix / config). Covers what each generator must be extended with, the ~1000x pre-commit run, and which rules intentionally have no safe `--fix`.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Property Tests skill

What this skill tells your AI

The instructions your AI receives, as published by owenlamont/ryl in .agents/skills/property-tests/SKILL.md and read by ahel’s review.

When implementing a new rule or changing an existing one, extend the relevant property-test generator(s) so the new/updated syntax is actually exercised (each suite below lists exactly what to extend and the deterministic guard to add), then do a one-off ~1000× thorough run before committing: e.g. PROPTEST_CASES=512000 cargo test --release --test property_check (the suites' in-CI default is 512 cases — tuned for speed, not exhaustiveness). Build --release and run it in the background; it routinely flushes rare interleavings the small count misses. Commit only once it is green, and keep any newly-persisted seeds in tests/proptest-regressions/.

Property Tests For Safe Fixes

tests/property_safe_fix.rs runs generated YAML through apply_safe_fixes and asserts three soundness invariants (a safe fix must never change meaning, but need not be complete — so it does not assert "no diagnostics remain"): idempotence, parse preservation (parses to an equal YamlOwned), and a leading # ryl disable making the fix a byte-for-byte no-op. It runs a matrix of named configs — five YAML (yamllint-default, best-practice, strict-single, strict-double, consistent) plus one TOML-backed (best-practice-toml, covering ryl-only options like allow-double-quotes-for-escaping). Deterministic siblings pin known-dirty / production-bug inputs through the same checks (and assert the fixer clears them) so the property can't pass vacuously.

When you add a new FixSafety::Safe rule:

  1. Add its rule id to SAFE_FIX_RULES and to COMMON_SAFE_FIX_RULES_YAML in tests/property_safe_fix.rs. If the new rule introduces meaningful config axes, add a variant to QUOTED_STRINGS_VARIANTS (or a peer constant for that rule) so the matrix exercises each regime; ryl-only options must go through the TOML slot rather than YAML.
  2. Extend the AST / renderer in that file so generated documents exercise the syntax the new fixer targets. Skipping this leaves the property tests green for the wrong reason — the fixer has nothing to do.
  3. Run cargo test --test property_safe_fix and resolve any failures before landing the rule.
  4. Add a focused CLI-level regression test in tests/cli_fix.rs (or the rule-specific file) for any production bug discovered along the way, so the property suite is backed by a deterministic guard.

Failing inputs are persisted at tests/proptest-regressions/property_safe_fix.txt and replayed first on every run. That file is committed to git so the regression follows the codebase, not the developer's machine.

Property Tests For Rule Checkers

tests/property_check.rs property-tests the detection path: it runs every rule's check() over generated YAML and asserts oracle-free invariants — check() never panics, every span is in-bounds and character-aligned (1 <= line <= line_count, 1 <= column <= chars_on_line + 1), a leading # ryl disable mutes every rule (only a syntax error survives), and block-disabling a firing rule removes its diagnostics. It targets ryl's fragile byte↔char offset arithmetic rather than semantic correctness (the fast complement to the slow yamllint_compat_* differential suite). property_check/strategy.rs generates documents biased to trigger every rule (truthy words, octal/float scalars, duplicate/unordered keys, flow spacing, anchors, long lines, odd indentation, trailing spaces, %YAML version directives) interleaved with multibyte chars, raw NEL/LS/PS, and mixed LF/CRLF/bare-CR (a bare \r is a YAML 1.2 line break everywhere, so the oracle line_char_lengths is CR-aware too). harness.rs holds the trigger-all config and the per-rule dispatch, which calls each check() directly (not lint_str, which drops rule spans on a parse error) so spans are bounds-checked even on input that fails to parse.

When you add a new rule, extend collect_spans in harness.rs to call its check() and add a (rule-id, triggering-input) row to RULE_TRIGGERS in property_check.rs. The deterministic each_rule_triggers_and_reports_in_bounds_spans test asserts each rule fires on its crafted input, so the property assertions cannot silently pass vacuously if the generator drifts. Failing inputs persist to the committed tests/proptest-regressions/property_check.txt. Run with cargo test --test property_check.

Property Tests For Markdown --fix

tests/property_markdown_fix.rs property-tests fix::fix_markdown_str (write-back into embedded YAML). It reuses the safe-fix generator via #[path] and wraps the documents into a Markdown host (property_markdown_fix/wrap.rs), asserting four oracle-free invariants across the config matrix: host bytes outside regions stay byte-identical (region count/kinds stable), each region's parsed value is preserved, each region is untouched or rewritten to exactly its apply_safe_fixes_filtered form, and it's idempotent. Deterministic siblings pin known-dirty / CRLF / ragged / fence-crossing-front-matter cases.

Extend this suite only when the Markdown extractor/wrapper grows new region shapes: add a wrap.rs variant and a deterministic sibling. Failing inputs persist to the committed tests/proptest-regressions/property_markdown_fix.txt; run with cargo test --test property_markdown_fix.

Property Tests For Config Parsing

tests/property_config.rs property-tests configuration robustness: property_config/strategy.rs generates randomized configs (random rule subsets, levels, and options, mixing valid with hostile values — invalid regexes, ill-typed/out-of-range scalars, bogus locales) rendered to both YAML and TOML. The oracle-free invariant: the pipeline errors or succeeds but never panics — YAML via YamlLintConfig::from_yaml_str (then linting samples, to drive the .expect()s in key-ordering/quoted-strings resolve()), TOML via parse_toml_config_str -> validate_toml_config -> normalize_toml_config. Deterministic siblings pin empty-config, invalid-regex, billion-laughs, and rich-valid cases. When a rule gains a config-compiled regex or typed option, add its key(s) to CATALOG in strategy.rs.

Rules Without A Safe --fix

These rules are intentionally not part of SAFE_FIX_RULES. Each entry is the one-sentence reason --fix cannot rewrite the rule without risking changed parsed values or unintended user-visible behaviour. Revisit this list when considering a partial safe fix — if you can satisfy the property-test invariants for some subset, move the rule into SAFE_FIX_RULES and document the unsafe-trigger subset in that rule's module-level doc comment instead.

  • anchors — Fixing requires choosing which anchor an undeclared alias should point at, which duplicate to keep, or whether an "unused" anchor is actually referenced from a template the linter cannot see.
  • block-scalar-chomping — YAML has no explicit clip indicator (only - strip and + keep exist), so a bare |/> cannot be annotated without switching it to strip or keep, which changes the scalar's trailing newlines and resolved value; the choice is the author's intent.
  • colons — Collapsing extra space around colons safely needs precise parser context tracking (plain scalars, alias keys, explicit ?/: mappings) equivalent to re-implementing the YAML mapping scanner.
  • empty-values — The rule's intent is to force the user to choose between ~, null, or restructuring; auto-inserting a literal contradicts the rule's purpose and would silently change downstream behaviour.
  • float-values — Rewrites such as 0.5 → .5, .5 → 0.5, expanding 1e3 → 1000, or replacing .nan/.inf all change the scalar's string representation and, in tagged or string-typed consumers, its semantic value.
  • hyphens — Collapsing trailing spaces after - in a block sequence changes the indent of any nested block mapping/sequence that follows on subsequent lines and so can change the parsed structure; the dash-on-own-line option is likewise no-fix, since breaking the - onto its own line re-indents the mapping body.
  • indentation — Re-indenting alters the block-structure boundaries the YAML grammar uses to delimit mappings, sequences, and scalars; any non-trivial fix risks changing the parsed value.
  • key-duplicates — Resolving a duplicate requires deciding which key (and value) to keep; both choices alter the parsed mapping and need user intent.
  • key-ordering — Reordering a mapping silently disassociates any comment the user placed above or beside a key from that key, losing information the YAML grammar does not carry.
  • line-length — Splitting an over-long line requires line-folding decisions that depend on whether the scalar is plain, quoted, or block-styled, and on whether folding is semantically allowed; no single rewrite is universally safe.
  • merge-keys — Removing a << merge requires inlining the merged mapping's resolved keys/values (which the source text alone does not carry) and would change the document's structure; quoting the << silently drops the merge, so no rewrite is universally safe.
  • octal-values — Resolving 010 requires knowing whether the user meant the integer 8, the integer 10, or the string "010"; the YAML source alone cannot disambiguate.
  • tags — Rewriting or removing a flagged tag changes the node's resolved type (!!omap to a plain mapping, !env to a string, …) or requires guessing the intended value, so no rewrite is universally safe.
  • truthy — Rewriting Yes/No/On/Off requires choosing between quoting them (preserves the string), normalising to true/false (changes type), or rewording — all of which depend on the user's intent.
  • unicode-line-breaks — The \N/\L/\P escape is valid only inside a double-quoted scalar; rewriting a raw NEL/LS/PS in a plain or single-quoted scalar, a comment, or a block scalar would require changing the quoting style or guessing intent, so no rewrite is universally safe.

Signals

GitHub stars
72
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
property-tests
Source
github.com/owenlamont/ryl