AgentSkillsCN

testing-hashql

HashQL 测试策略,包括 compiletest(UI 测试)、单元测试以及快照测试。在为 HashQL 代码编写测试、使用 //~ 注解、运行 --bless 命令、调试测试失败问题,或选择合适的测试方案时,可优先考虑这些策略。

SKILL.md
--- frontmatter
name: testing-hashql
description: HashQL testing strategies including compiletest (UI tests), unit tests, and snapshot tests. Use when writing tests for HashQL code, using //~ annotations, running --bless, debugging test failures, or choosing the right testing approach.
license: AGPL-3.0
metadata:
  triggers:
    type: domain
    enforcement: suggest
    priority: high
    keywords:
      - hashql test
      - compiletest
      - ui test
      - snapshot test
      - insta test
      - //~ annotation
      - --bless
    intent-patterns:
      - "\\b(write|create|run|debug|add|fix)\\b.*?\\b(hashql|compiletest)\\b.*?\\btest\\b"
      - "\\b(test|verify)\\b.*?\\b(diagnostic|error message|mir|hir|ast)\\b"

HashQL Testing Strategies

HashQL uses three testing approaches. compiletest is the default for testing compiler behavior.

Quick Reference

ScenarioTest TypeLocation
Diagnostics/error messagescompiletesttests/ui/
Compiler pipeline phasescompiletesttests/ui/
MIR/HIR/AST pass integrationcompiletesttests/ui/
MIR/HIR/AST pass edge casesinstatests/ui/<category>/
MIR pass unit testsMIR buildersrc/**/tests.rs
Core crate (where needed)instasrc/**/snapshots/
Parser fragments (syntax-jexpr)instasrc/*/snapshots/
Internal functions/logicUnit testssrc/*.rs

compiletest (UI Tests)

Test parsing, type checking, and error reporting using J-Expr files with diagnostic annotations.

Structure:

text
package/tests/ui/
  category/
    .spec.toml        # Suite specification (required)
    test.jsonc        # Test input
    test.stdout       # Expected output (run: pass)
    test.stderr       # Expected errors (run: fail)
    test.aux.svg      # Auxiliary output (some suites)

Commands:

bash
cargo run -p hashql-compiletest run                           # Run all
cargo run -p hashql-compiletest run --filter "test(name)"     # Filter
cargo run -p hashql-compiletest run --bless                   # Update expected

Test file example:

jsonc
//@ run: fail
//@ description: Tests duplicate field detection
["type", "Bad", {"#struct": {"x": "Int", "x": "String"}}, "_"]
//~^ ERROR Field `x` first defined here

Directives (//@ at file start):

  • run: pass / run: fail (default) / run: skip
  • description: ... (encouraged)
  • name: custom_name

Annotations (//~ for expected diagnostics):

  • //~ ERROR msg - current line
  • //~^ ERROR msg - previous line
  • //~v ERROR msg - next line
  • //~| ERROR msg - same as previous annotation

📖 Full Guide: references/compiletest-guide.md

Unit Tests

Standard Rust #[test] functions for testing internal logic.

Location: #[cfg(test)] modules in source files

Example from hashql-syntax-jexpr/src/parser/state.rs:

rust
#[test]
fn peek_returns_token_without_consuming() {
    bind_context!(let context = "42");
    bind_state!(let mut state from context);

    let token = state.peek().expect("should not fail").expect("should have token");
    assert_eq!(token.kind, number("42"));
}

Commands:

bash
cargo nextest run --package hashql-<package>
cargo test --package hashql-<package> --doc    # Doc tests

insta Snapshot Tests

Use insta crate for snapshot-based output when compiletest (the preferred method) is infeasible. Three categories exist:

CategoryCratesSnapshot LocationRationale
Pipeline Cratesmir, hir, asttests/ui/<category>/*.snapColocate with compiletest tests
Corehashql-coreDefault insta (src/**/snapshots/)Separate from pipeline; prefer unit tests
Syntaxsyntax-jexprsrc/*/snapshots/Macro-based for parser fragments

Pipeline Crates (mir, hir, ast)

Snapshots colocate with compiletest UI tests. Test code lives in src/**/tests.rs, snapshots go in the appropriate tests/ui/<category>/ directory.

rust
// Example: hashql-mir/src/pass/transform/ssa_repair/tests.rs
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut settings = Settings::clone_current();
settings.set_snapshot_path(dir.join("tests/ui/pass/ssa_repair")); // matches test category
settings.set_prepend_module_to_snapshot(false);

let _drop = settings.bind_to_scope();
assert_snapshot!(name, value);

Categories vary: reify/, lower/, pass/ssa_repair/, etc.

Core

hashql-core is separate from the compilation pipeline, so it uses default insta directories. Prefer unit tests; only use snapshots where necessary.

Syntax (syntax-jexpr)

Syntax crates predate compiletest and use macro-based test harnesses for testing parser fragments directly.

rust
// hashql-syntax-jexpr/src/parser/string/test.rs
pub(crate) macro test_cases($parser:ident; $($name:ident($source:expr) => $description:expr,)*) {
    $(
        #[test]
        fn $name() {
            assert_parse!($parser, $source, $description);
        }
    )*
}

Snapshots: hashql-syntax-jexpr/src/parser/*/snapshots/*.snap

Commands

bash
cargo insta test --package hashql-<package>
cargo insta review     # Interactive review
cargo insta accept     # Accept all pending

MIR Builder Tests

For testing MIR transformation and analysis passes directly with programmatically constructed MIR bodies.

Location: hashql-mir/src/pass/**/tests.rs

When to use:

  • Testing MIR passes in isolation with precise CFG control
  • Edge cases requiring specific MIR structures hard to produce from source
  • Benchmarking pass performance

Key features:

  • Transform passes return Changed enum (Yes, No, Unknown) to indicate modifications
  • Test harness captures and includes Changed value in snapshots for verification
  • Snapshot format: before MIR → Changed: Yes/No/Unknown separator → after MIR

Important: Missing Macro Features

The body! macro does not support all MIR constructs. If you need a feature that is not supported, do not work around it manually - instead, stop and request that the feature be added to the macro.

Quick Example (using body! macro)

rust
use hashql_core::{heap::Heap, r#type::environment::Environment};
use hashql_mir::{builder::body, intern::Interner};

let heap = Heap::new();
let interner = Interner::new(&heap);
let env = Environment::new(&heap);

let body = body!(interner, env; fn@0/1 -> Int {
    decl x: Int, cond: Bool;

    bb0() {
        cond = load true;
        if cond then bb1() else bb2();
    },
    bb1() {
        goto bb3(1);
    },
    bb2() {
        goto bb3(2);
    },
    bb3(x) {
        return x;
    }
});

📖 Full Guide: references/mir-builder-guide.md

References