Skip to content

Add multi-line test support to SpecValidator #313

@andreasronge

Description

@andreasronge

Summary

Add support for extracting and validating multi-line PTC-Lisp examples from the specification markdown, enabling comprehensive testing of complex examples like let bindings with multiple lines.

Context

Architecture reference: lib/ptc_runner/lisp/spec_validator.ex - current single-line extraction logic
Dependencies: None
Related issues: None

Current State

The SpecValidator extracts examples line-by-line using extract_example_from_line/1 (lines 290-311). It looks for the pattern code ; => expected on a single line.

Multi-line examples in the spec like this are not tested:

(let [x 10
      y (+ x 5)]    ; y can use x
  (* x y))          ; => 150

Only the last line is seen, detected as a "fragment" via fragment?/1, and skipped. The fragment?/1 function (lines 317-337) is a workaround for this limitation.

Verified: Current extraction yields 129 examples. Examples like (* x y)) ; => 150 (the multi-line let) and name) ; => "Dan" (nested destructuring) are confirmed missing - fragments are correctly filtered but the full expressions are never assembled.

Acceptance Criteria

  • Multi-line examples in ```clojure blocks are extracted and tested
  • Single-line examples continue to work as before
  • The fragment?/1 workaround can be removed or simplified
  • extract_examples/0 captures more examples than before (current: 129, target: 140+)
  • Existing tests in spec_validator_test.exs pass
  • New tests cover multi-line extraction scenarios
  • Skip mechanism exists for temporarily disabling failing tests

Implementation Hints

Approach: Two-pass parsing

Separate concerns into two passes for cleaner implementation:

Pass 1: Extract code blocks from markdown

  • Scan for clojure ... blocks
  • Track current section header (## N. Title)
  • Output: List of %{section: String.t(), content: String.t()} maps

Pass 2: Extract tests from each block

  • Within each block's content, find lines ending with ; => expected
  • For each match, scan backward to find the complete balanced expression
  • Use parenthesis/bracket counting to find expression start
  • Handle strings (don't count parens inside quotes)

Skip mechanism for known-failing tests:

  • Add ; => SKIP or ; => TODO(#issue) marker that SpecValidator recognizes
  • When encountered, skip the test but track it in results as skipped
  • Example: (some-buggy-code) ; => SKIP or (unimplemented) ; => TODO(#314)
  • This allows CI to pass while documenting what needs fixing

Files to modify:

  • lib/ptc_runner/lisp/spec_validator.ex:
    • Add extract_code_blocks/1 for Pass 1
    • Modify extract_examples_from_content/1 to use two-pass approach
    • Add extract_tests_from_block/2 for Pass 2
    • Add find_expression_start/2 for backward scanning with paren balancing
    • Add skip detection in parse_expected/1 for SKIP and TODO(#N) markers
    • Consider removing or simplifying fragment?/1

Patterns to follow:

  • Current extract_section_header/1 for section tracking
  • Current has_unbalanced_parens?/1 for paren counting (extend for backward scan)

Edge cases to consider:

  • Parentheses inside string literals: "(not a paren)"
  • Comments before ; =>: ; comment here vs ; => result
  • Multiple ; => in same block (each is a separate test)
  • Nested expressions with multiple closing parens
  • Empty code blocks
  • Code blocks with only comments (no executable code)

Handling Discovered Failures

When multi-line tests are extracted, some may fail due to:

  1. Spec typos/errors - Fix directly in this PR
  2. Implementation bugs - Fix in this PR if small (< 30 min), otherwise:
    • Create separate issue for the bug
    • Mark example with ; => TODO(#issue) to skip temporarily
    • Reference the issue number so it's tracked
  3. Unimplemented features - Create separate issue, mark with ; => TODO(#issue)

Guideline: CI must pass. Use skip markers for anything that can't be fixed in this PR.

Test Plan

Unit tests:

  • extract_code_blocks/1 correctly extracts blocks with section info
  • find_expression_start/2 handles:
    • Simple single-line: (+ 1 2)
    • Multi-line let: (let [x 1\n y 2]\n (+ x y))
    • Nested parens: (map (fn [x] (inc x)) [1 2 3])
    • Strings with parens: (str "hello (world)")
  • Skip markers (SKIP, TODO(#N)) are recognized and counted as skipped

Integration tests:

  • extract_examples/0 returns more examples than before (target: 140+)
  • All extracted examples pass validation (no regressions)
  • Specific multi-line examples from spec are captured:
    • (let [x 10 y (+ x 5)] (* x y)) ; => 150
    • Complex destructuring examples
    • Threading macro examples

E2E test:

  • Run mix ptc.validate_spec - should show increased pass count and any skipped tests

Out of Scope

  • Changing the ; => marker format in the specification
  • Adding ; => TYPE ERROR for negative tests (future enhancement)
  • Extracting examples from non-clojure code blocks
  • Performance optimization (current approach is fast enough)

Documentation Updates

None - this is an internal testing infrastructure change. The specification format remains unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-approvedMaintainer-approved for Claude automationenhancementNew feature or requestready-for-implementationIssue is approved and ready to implement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions