Skip to content

snake-eyes: test mapping pipeline and test_mapping method (O1) #6

Description

@jflowers

How to use this issue with /opsx-propose

/opsx-propose test-mapping

Use this entire issue body as the change description. Do not ask clarifying questions. Every decision is already made below. If something is still unspecified, apply these defaults in order:

  1. Match test_mapping protocol v1.1.0 field names exactly.
  2. Lift gaze-py pairing + assertion detection; write a new pipeline (do not lift quality/pipeline.py).
  3. Do not compute CRAP, GazeCRAP, quadrants, or contract-coverage percentages — Gaze does that from the mappings.
  4. Astroid strategy 3 is optional: if astroid fails, skip it; still return strategies 1–2.

OpenSpec change name: test-mapping

Depends on: #4 (analysis-methods) merged.

Independent of: classify-signals. May be implemented in parallel. Do not modify signal extractors here.


Context

test_mapping is the optional method that lets Gaze compute GazeCRAP (contract coverage). snake-eyes pairs tests to production functions, finds assertions, and guesses which side-effect type each assertion is checking.

Lift (mpeter/gaze-py, permission granted, keep copyright):

  • src/gaze_py/quality/pairing.pysrc/snake_eyes/quality/pairing.py
  • assertion helpers from gaze-py quality package (whatever file detects assert / pytest.raises / unittest asserts) → src/snake_eyes/quality/assertions.py

Do not lift quality/pipeline.py. Write src/snake_eyes/quality/pipeline.py against snake-eyes models and the protocol mapping record.

astroid may already be a dependency from classify-signals; if this issue lands first, add astroid>=3.0,<5 here.


Protocol

Request:

{"root_path": "/abs/path", "patterns": ["./..."]}

Result:

{
  "mappings": [
    {
      "test_function": "test_multiply",
      "test_file": "tests/test_ops.py",
      "assertion_location": "tests/test_ops.py:10",
      "assertion_type": "equality",
      "target_function": "multiply",
      "target_package": "math_utils",
      "side_effect_type": "ReturnValue",
      "confidence": 80
    }
  ]
}

confidence is int 0–100 (not 0.0–1.0). Convert lifted 0.9 → 90.

assertion_location format: path:line (no column required). Path relative to root_path.

Allowed assertion_type strings:

equality | error_check | membership | identity | comparison | generic

Flip capabilities.test_mapping to true.

If no tests or no pairs: {"mappings": []}, not an error.


What to build

1. Pairing (lift + adapt)

Three strategies, first match wins, do not emit duplicate pairs for the same (test_function, test_file, target_function):

Priority Strategy confidence
1 Name convention: test_foo / testFoo / TestFoofoo. Strip test_ / Test prefix. Case-insensitive fallback confidence 70 if only case differs 90 / 70
2 Direct call: test function AST contains a Call to the target name 80
3 Astroid transitive call graph, BFS depth_limit=5 75

Unpaired tests produce no mapping rows (do not emit confidence 0 rows).

target_package from the production file's dotted module path (same helper as analyze).

2. Assertion detection (lift + adapt)

For each paired test function, collect assertions:

AST / pattern assertion_type
assert x == y, assertEqual, assertEquals equality
assert x != y, assertNotEqual comparison
assert x < y / > / <= / >=, assertLess* comparison
assert x is y, assertIs, assertIsNone identity
assert x in y, assertIn, assertNotIn membership
pytest.raises, unittest.assertRaises, raises( error_check
other assert / assertTrue / assertFalse generic

One mapping row per assertion, not per pair. A test with 3 asserts → 3 rows (same target, different assertion_location).

3. Effect-type guess (new, mapping.py)

For each assertion, set side_effect_type as:

assertion_type side_effect_type
error_check ErrorReturn if the target has that effect, else ErrorSignal if present, else ErrorReturn anyway
equality, comparison, identity, membership ReturnValue if the target has it, else the first P0 effect on the target, else ReturnValue
generic first effect on the target if any, else ReturnValue

Use the detector output for the target function. Do not re-parse ad hoc.

4. Pipeline (new)

run_test_mapping(root_path, patterns) -> list[dict]:

  1. discover()
  2. analyze_path() on the same root/patterns (need effects on targets)
  3. Parse test files, collect FunctionDef/AsyncFunctionDef whose names start with test_ or are methods of classes subclassing unittest.TestCase whose names start with test
  4. Pair
  5. Assertions
  6. Effect guess
  7. Serialize protocol dicts

Do not run pytest. Do not read coverage here.

5. Fixture project

tests/fixtures/sample_project/:

src/sample/__init__.py
src/sample/calculator.py    # add(a,b)->a+b; divide(a,b) raises ZeroDivisionError; Counter.inc mutates self
tests/test_calculator.py    # test_add (name strategy), a test that calls divide without matching name,
                            # pytest.raises for divide

Include pyproject.toml only if needed for imports; tests should set root_path to this fixture dir and patterns to ["./..."].


Tests required

  • Name strategy: test_addadd, confidence 90
  • Case-insensitive: test_Addadd, confidence 70
  • Direct call: test named test_it_works that calls dividedivide, confidence 80
  • No match: test that calls nothing and name-matches nothing → no row
  • pytest.raisesassertion_type=error_check, side_effect_type ErrorReturn or ErrorSignal
  • assert result == 3equality, ReturnValue
  • Pipeline on sample_project → at least two mapping rows with all required keys present
  • JSON-RPC e2e; initialize has test_mapping: true
  • Missing root → -32602

Do not require strategy 3 to fire in CI if astroid cannot import the fixture; still unit-test the BFS helper with a mocked graph or a tiny on-disk package that astroid can parse.


Coverage strategy (Constitution IV)

Layer Target
pairing.py 90%+
assertions.py 90%+
mapping.py 95%+
pipeline.py 85%+
Project gate 85%

Out of scope

  • gaze-py quality/pipeline.py
  • Running coverage/pytest for the analyzed project
  • CRAP formulas, quadrants, fix strategies
  • classify_signals changes
  • Streaming

Done when

  • test_mapping RPC returns protocol-shaped mappings
  • capability flag true
  • three pairing strategies exist in code (3rd degrades without failing the method)
  • copyright retained on lifted pairing/assertion files
  • snake-eyes still does not print CRAP scores

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions