|
| 1 | +<!-- SPDX-License-Identifier: MPL-2.0 --> |
| 2 | +<!-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> --> |
| 3 | + |
| 4 | +# Testing & Coverage Roadmap |
| 5 | + |
| 6 | +This document records the current state of automated testing in the `my-lang` |
| 7 | +workspace and the prioritised plan for improving it. It is a living document: |
| 8 | +update it as gaps are closed. |
| 9 | + |
| 10 | +## How to run the tests |
| 11 | + |
| 12 | +```sh |
| 13 | +# Whole workspace |
| 14 | +cargo test --workspace |
| 15 | + |
| 16 | +# Just the compiler crate |
| 17 | +cargo test -p my-lang |
| 18 | + |
| 19 | +# Conformance + example fixtures (parse-level) |
| 20 | +cargo test -p my-lang --test conformance |
| 21 | + |
| 22 | +# Coverage (requires llvm-tools-preview + cargo-llvm-cov) |
| 23 | +rustup component add llvm-tools-preview |
| 24 | +cargo install cargo-llvm-cov --locked |
| 25 | +cargo llvm-cov --workspace --exclude my-llvm --summary-only |
| 26 | +``` |
| 27 | + |
| 28 | +CI runs coverage on every push/PR via `.github/workflows/coverage.yml` and |
| 29 | +enforces a conservative line-coverage **floor** (`COVERAGE_FLOOR`). The floor is |
| 30 | +a ratchet: raise it as coverage improves; never lower it. |
| 31 | + |
| 32 | +### Baseline (at introduction) |
| 33 | + |
| 34 | +Whole-workspace coverage (excluding `my-llvm`, which needs a system LLVM |
| 35 | +toolchain) was **~46.7% lines / 45.3% regions / 48.0% functions**. Notable |
| 36 | +per-area gaps: |
| 37 | + |
| 38 | +| Area | Line coverage | |
| 39 | +|------|--------------:| |
| 40 | +| `my-lang/src/lexer.rs` | ~95% | |
| 41 | +| `my-lang/src/parser.rs` | ~79% | |
| 42 | +| `my-lang/src/checker.rs` | ~61% | |
| 43 | +| `my-lang/src/interpreter.rs` | ~58% | |
| 44 | +| `my-lang/src/stdlib.rs` | ~42% | |
| 45 | +| `my-lang/src/types.rs` | ~31% | |
| 46 | +| `my-lang/src/token.rs` | ~21% | |
| 47 | +| `my-mir/src/lib.rs` | ~21% | |
| 48 | +| `my-pkg/src/lib.rs` | ~10% | |
| 49 | +| `my-lsp/src/lib.rs` | ~16% | |
| 50 | +| `my-lang/src/main.rs`, `src/visitor.rs`, `my-test`, `my-lsp/main.rs` | 0% | |
| 51 | + |
| 52 | +## Current state (snapshot) |
| 53 | + |
| 54 | +The active build is the `crates/*` Cargo workspace. Test density is very uneven: |
| 55 | +the compiler front end (lexer/parser/checker/interpreter) is reasonably covered, |
| 56 | +while the middle/back end, the CLI, and most support binaries are barely tested. |
| 57 | + |
| 58 | +| Crate | ~LOC | Inline tests | Integration files | Status | |
| 59 | +|-------|-----:|-------------:|------------------:|--------| |
| 60 | +| `my-lang` (compiler) | 14,156 | 140+ | 3 | Front end good; `stdlib.rs`, `ast.rs`, `types.rs`, `token.rs`, `visitor.rs` have **no** unit tests | |
| 61 | +| `my-cli` | 764 | 0 | 0 | The `my` binary — **untested** | |
| 62 | +| `my-mir` | 1,596 | 1 | 0 | MIR lowering nearly untested | |
| 63 | +| `my-llvm` | 1,077 | 1 | 0 | Codegen nearly untested | |
| 64 | +| `my-lsp` | 648 | 1 | 0 | Language server nearly untested | |
| 65 | +| `my-ai` | 778 | 5 | 0 | Light; no async end-to-end tests | |
| 66 | +| `my-hir` | 666 | 1 | 0 | Thin | |
| 67 | +| `my-fmt` | 529 | 2 | 0 | Thin | |
| 68 | +| `my-lint` | 444 | 1 | 0 | Thin | |
| 69 | +| `my-pkg` | 377 | 1 | 0 | Thin | |
| 70 | +| `my-test` | 344 | 0 | 0 | **Untested** | |
| 71 | +| `my-debug` | 266 | 0 | 0 | **Untested** | |
| 72 | +| `my-dap` | 173 | 0 | 0 | **Untested** | |
| 73 | +| `my-parser` | 41 | 1 | 1 | Stub (all methods `Ok(())` / TODO) | |
| 74 | + |
| 75 | +### Structural issues found |
| 76 | + |
| 77 | +1. **Partly-orphaned duplicate source tree.** The repository root contains a |
| 78 | + second `src/`, `lib/`, `tests/`, and `fuzz/` tree alongside the active |
| 79 | + `crates/*` workspace. The picture is mixed: |
| 80 | + - `tests/integration_test.rs` (20 tests) **is** run — `crates/my-lang/Cargo.toml` |
| 81 | + wires it in via `[[test]] path = "../../tests/integration_test.rs"`. |
| 82 | + - `tests/property_tests.rs` (34 tests) is **not referenced anywhere** and |
| 83 | + never runs. |
| 84 | + - The root `src/` and `lib/` are **not referenced** by any crate; the root |
| 85 | + `src/lib.rs` also references modules that do not exist there, so it cannot |
| 86 | + compile as a package. |
| 87 | + → **Action:** wire in (or port) `property_tests.rs`, and delete the orphaned |
| 88 | + root `src/`/`lib/` duplicates so the layout has a single source of truth. |
| 89 | + |
| 90 | +2. **Conformance suite was shell-only and broken.** `conformance/run_conformance.sh` |
| 91 | + drove `my-cli` through a `--parse-only` flag that no longer exists (the CLI |
| 92 | + uses subcommands such as `parse`/`check`). It was therefore easy to skip and |
| 93 | + silently non-functional. → **Done:** added `crates/my-lang/tests/conformance.rs`, |
| 94 | + which exercises every fixture against the library API under `cargo test` (and |
| 95 | + thus under coverage). See "Known parse gaps" below. |
| 96 | + |
| 97 | +3. **No coverage measurement.** → **Done:** added `.github/workflows/coverage.yml` |
| 98 | + (`cargo-llvm-cov`) with an LCOV + HTML artifact and a ratcheting floor. |
| 99 | + |
| 100 | +4. **Several crates did not compile.** Enabling a workspace-wide build revealed |
| 101 | + that `my-fmt`, `my-lsp`, and `my-lint` did not compile at all (a borrow-after-move, |
| 102 | + a non-exhaustive `match` missing the `ExpressionTooDeep` checker variant, and |
| 103 | + invalid multi-codepoint `char` literals, respectively). This strongly implies |
| 104 | + current CI only builds `my-lang`. → **Done:** all three are fixed in this |
| 105 | + change, and the new coverage job now builds and tests the whole workspace, so |
| 106 | + such breakage cannot recur silently. `my-lint`'s only unit test had also never |
| 107 | + run and asserted the wrong count; it now matches actual behaviour. |
| 108 | + |
| 109 | +5. **Thin negative-path testing.** Outside `checker.rs`, very few tests assert |
| 110 | + on *which* error/span is produced. The `conformance/invalid/*.my` fixtures |
| 111 | + now assert rejection, but unit tests should also pin specific |
| 112 | + `ParseError`/`CheckError` variants. |
| 113 | + |
| 114 | +## Known parse gaps (surfaced by the new conformance test) |
| 115 | + |
| 116 | +Wiring up the conformance fixtures immediately found real defects/feature gaps. |
| 117 | +One was a genuine bug and is **fixed**; the rest are tracked as a fail-closed |
| 118 | +allowlist in `crates/my-lang/tests/conformance.rs` (`KNOWN_PARSE_GAPS`). When a |
| 119 | +gap is closed, the test forces removal of its allowlist entry. |
| 120 | + |
| 121 | +| Fixture | Gap | Status | |
| 122 | +|---------|-----|--------| |
| 123 | +| `v03_ai_model.my` | `ai_model` blocks rejected comma-separated / trailing-comma attributes (inconsistent with `struct`) | **Fixed** in `parser.rs` (`parse_ai_model_attr`) + regression test | |
| 124 | +| `v04_let.my` | assignment statements (`y = y + x;`) not parsed | Tracked (allowlisted) | |
| 125 | +| `v06_match.my` | `match` not accepted as a function tail expression | Tracked (allowlisted) | |
| 126 | +| `v07_agent.my` | unit type `()` in effect op signatures not parsed | Tracked (allowlisted) | |
| 127 | +| `v08_effect.my` | lambda syntax (`\|n\| => expr`) not parsed | Tracked (allowlisted) | |
| 128 | + |
| 129 | +## Prioritised improvement plan |
| 130 | + |
| 131 | +1. **Resolve the duplicate tree** (item 1 above). Highest leverage — until then |
| 132 | + coverage numbers are misleading. |
| 133 | +2. **Close the known parse gaps**, removing each `KNOWN_PARSE_GAPS` entry as its |
| 134 | + feature lands (assignment statements, match tail expressions, unit type in |
| 135 | + effect ops, lambdas). |
| 136 | +3. **Test the middle/back end:** golden tests for `my-mir` lowering and `my-llvm` |
| 137 | + codegen (snapshot IR for small programs). |
| 138 | +4. **Test `my-cli`:** drive the built `my` binary (or `assert_cmd`) for |
| 139 | + argument parsing, exit codes, and each subcommand (`parse`, `check`, `lex`, |
| 140 | + `run`). |
| 141 | +5. **Cover `stdlib.rs`** — the largest untested unit (~1,900 LOC) and likely |
| 142 | + high-churn (JSON, maps, strings, fs, dates). |
| 143 | +6. **Add async tests for the AI paths** (`my-ai`, `library::mylang::*`) using the |
| 144 | + existing `MockAiClient` to exercise streaming and tool-call flows end-to-end. |
| 145 | +7. **Strengthen negative-path unit tests** — assert specific error variants and |
| 146 | + spans, especially for the `conformance/invalid` cases. |
| 147 | +8. **Cover the remaining untested binaries:** `my-test`, `my-debug`, `my-dap`. |
| 148 | +9. **Ratchet `COVERAGE_FLOOR`** upward as the above lands. |
0 commit comments