Skip to content

Latest commit

 

History

History
630 lines (484 loc) · 11 KB

File metadata and controls

630 lines (484 loc) · 11 KB

Testing Guide

Comprehensive guide to testing AffineScript programs and the compiler itself.

Table of Contents

  1. Testing Framework
  2. Writing Tests
  3. Test Organization
  4. Property-Based Testing
  5. Fuzzing
  6. Compiler Testing
  7. Integration Tests
  8. Benchmarking

Testing Framework

AffineScript uses a built-in testing framework:

Basic Test Structure

#[test]
fn test_addition() {
  assert_eq(2 + 2, 4)
}

#[test]
fn test_string_concat() {
  let s = "hello" ++ " " ++ "world"
  assert_eq(s, "hello world")
}

Running Tests

# Run all tests
aspm test

# Run specific test file
aspm test tests/math_test.affine

# Run tests matching pattern
aspm test --filter "test_add"

# Run with verbose output
aspm test --verbose

# Run tests in parallel
aspm test --jobs 4

Writing Tests

Assertions

// Equality
assert_eq(actual, expected)
assert_ne(actual, unexpected)

// Boolean
assert(condition)
assert(condition, "custom message")

// Comparisons
assert_lt(a, b)   // a < b
assert_le(a, b)   // a <= b
assert_gt(a, b)   // a > b
assert_ge(a, b)   // a >= b

// Option/Result
assert_some(opt)
assert_none(opt)
assert_ok(result)
assert_err(result)

// Collections
assert_contains(collection, element)
assert_empty(collection)
assert_len(collection, expected_len)

Test Attributes

// Basic test
#[test]
fn test_basic() { ... }

// Test expected to panic
#[test]
#[should_panic]
fn test_panic() {
  panic("expected panic")
}

// Test expected to panic with specific message
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_bounds() {
  let arr = [1, 2, 3]
  arr[10]  // Panics
}

// Ignore test (skip)
#[test]
#[ignore]
fn test_slow() { ... }

// Ignore with reason
#[test]
#[ignore = "requires network"]
fn test_network() { ... }

// Test with timeout
#[test]
#[timeout(1000)]  // 1000ms
fn test_fast() { ... }

Test Fixtures

mod tests {
  use super::*

  // Setup function
  fn setup() -> TestContext {
    TestContext::new()
  }

  // Teardown function
  fn teardown(ctx: TestContext) {
    ctx.cleanup()
  }

  #[test]
  fn test_with_fixture() {
    let ctx = setup()

    // Test code...

    teardown(ctx)
  }
}

Test Modules

// In lib.affine
pub fn add(x: Int, y: Int) -> Int {
  x + y
}

#[cfg(test)]
mod tests {
  use super::*

  #[test]
  fn test_add_positive() {
    assert_eq(add(2, 3), 5)
  }

  #[test]
  fn test_add_negative() {
    assert_eq(add(-1, 1), 0)
  }

  #[test]
  fn test_add_zero() {
    assert_eq(add(0, 0), 0)
  }
}

Test Organization

Directory Structure

project/
├── src/
│   ├── lib.affine
│   └── math/
│       └── mod.affine
├── tests/
│   ├── integration_test.affine    # Integration tests
│   ├── math_test.affine           # Module tests
│   └── fixtures/
│       └── test_data.json
└── benches/
    └── performance_bench.affine   # Benchmarks

Unit vs Integration Tests

// tests/unit_test.affine - Unit tests for internal modules
use mylib::internal::parse_number

#[test]
fn test_parse_number() {
  assert_eq(parse_number("42"), Ok(42))
}

// tests/integration_test.affine - Integration tests for public API
use mylib

#[test]
fn test_full_workflow() {
  let config = mylib::Config::default()
  let result = mylib::process(config, "input data")
  assert_ok(result)
}

Property-Based Testing

Inspired by QuickCheck and Echidna's fuzzing approach.

Basic Properties

use test::property::*

#[property]
fn prop_addition_commutative(x: Int, y: Int) -> Bool {
  x + y == y + x
}

#[property]
fn prop_reverse_reverse(xs: Vec[Int]) -> Bool {
  xs.reverse().reverse() == xs
}

#[property]
fn prop_sort_preserves_length[T: Ord](xs: Vec[T]) -> Bool {
  xs.len() == xs.sorted().len()
}

Custom Generators

use test::property::*

// Custom generator for positive integers
fn gen_positive() -> Gen[Int] {
  Gen::int_range(1, 1000)
}

// Custom generator for non-empty strings
fn gen_non_empty_string() -> Gen[String] {
  Gen::string()
    .filter(|s| s.len() > 0)
}

#[property]
fn prop_positive_sqrt(#[gen(gen_positive)] n: Int) -> Bool {
  let root = (n as Float64).sqrt()
  root >= 0.0
}

// Generator for custom types
struct Point { x: Int, y: Int }

impl Arbitrary for Point {
  fn arbitrary() -> Gen[Point] {
    Gen::map2(Gen::int(), Gen::int(), |x, y| Point { x, y })
  }

  fn shrink(self) -> Vec[Point] {
    // Shrink towards simpler cases
    vec![
      Point { x: 0, y: 0 },
      Point { x: self.x, y: 0 },
      Point { x: 0, y: self.y },
    ]
  }
}

Configuration

#[property]
#[tests(1000)]        // Run 1000 test cases
#[max_shrinks(100)]   // Try up to 100 shrinks on failure
fn prop_many_tests(x: Int) -> Bool {
  x * 0 == 0
}

Invariants (Echidna-Style)

Define invariants that should always hold:

struct BankAccount {
  balance: Int,
  withdrawals: Vec[Int],
}

impl BankAccount {
  fn withdraw(self: &mut Self, amount: Int) -> Result[(), Error] {
    if amount > self.balance {
      return Err(InsufficientFunds)
    }
    self.balance -= amount
    self.withdrawals.push(amount)
    Ok(())
  }

  // Invariant: balance should never be negative
  #[invariant]
  fn balance_non_negative(self: &Self) -> Bool {
    self.balance >= 0
  }

  // Invariant: sum of withdrawals + balance = initial balance
  #[invariant]
  fn accounting_correct(self: &Self) -> Bool {
    let total_withdrawn: Int = self.withdrawals.iter().sum()
    // This requires tracking initial balance...
    true
  }
}

#[test]
fn fuzz_bank_account() {
  test::fuzz::run_invariant_tests::<BankAccount>(
    1000,  // iterations
    BankAccount { balance: 1000, withdrawals: vec![] }
  )
}

Fuzzing

Fuzz Testing

use test::fuzz::*

// Fuzz test for parser
#[fuzz]
fn fuzz_parser(data: &[Byte]) {
  // Should not panic on any input
  let _ = Parser::parse_str(String::from_utf8_lossy(data))
}

// Fuzz test with structured input
#[fuzz]
fn fuzz_json_roundtrip(value: JsonValue) {
  let serialized = value.to_string()
  let parsed = JsonValue::parse(&serialized)
  assert_eq(parsed, Ok(value))
}

Coverage-Guided Fuzzing

#[fuzz]
#[coverage_guided]
fn fuzz_lexer(input: String) {
  let tokens = lexer::lex(&input)
  // Should handle all inputs gracefully
  for token in tokens {
    let _ = token.to_string()
  }
}

Running Fuzz Tests

# Run all fuzz tests
aspm fuzz

# Run specific fuzz test
aspm fuzz --target fuzz_parser

# Run for specific duration
aspm fuzz --duration 3600  # 1 hour

# Use specific corpus
aspm fuzz --corpus ./corpus/

Compiler Testing

Lexer Tests

(* test/test_lexer.ml *)
open Alcotest
open Affinescript

let test_keywords () =
  let tokens = Lexer.lex "fn let mut own ref" in
  check (list token_eq) "keywords"
    [FN; LET; MUT; OWN; REF; EOF]
    tokens

let test_string_escapes () =
  let tokens = Lexer.lex {|"hello\n\tworld"|} in
  check (list token_eq) "escapes"
    [STRING_LIT "hello\n\tworld"; EOF]
    tokens

let tests = [
  "keywords", `Quick, test_keywords;
  "string escapes", `Quick, test_string_escapes;
]

Parser Tests

let test_parse_expr () =
  let ast = Parser.parse_expr "1 + 2 * 3" in
  check ast_eq "precedence"
    (Binary (Lit 1, Add, Binary (Lit 2, Mul, Lit 3)))
    ast

let test_parse_function () =
  let ast = Parser.parse "fn add(x: Int, y: Int) -> Int { x + y }" in
  match ast with
  | [FnDecl { name = "add"; params; ret_ty; body }] ->
      check int "param count" 2 (List.length params)
  | _ -> fail "expected function"

Type Checker Tests

(* Positive test - should type check *)
let test_infer_literal () =
  let expr = parse_expr "42" in
  let (ty, _) = Type_check.infer empty_ctx expr in
  check type_eq "int literal" T_Int ty

(* Negative test - should produce error *)
let test_type_mismatch () =
  let program = parse "let x: Int = \"hello\"" in
  match Type_check.check_program program with
  | Error (Type_mismatch _) -> ()
  | _ -> fail "expected type mismatch error"

Golden Tests

Compare output against expected files:

let test_golden name =
  let input = read_file (sprintf "tests/golden/%s.affine" name) in
  let expected = read_file (sprintf "tests/golden/%s.expected" name) in
  let actual = compile_and_run input in
  check string name expected actual

let golden_tests = [
  "hello", `Quick, test_golden "hello";
  "factorial", `Quick, test_golden "factorial";
  "fibonacci", `Quick, test_golden "fibonacci";
]

Integration Tests

End-to-End Tests

// tests/e2e_test.affine
use std::process::Command

#[test]
fn test_compile_and_run() {
  // Compile
  let compile_result = Command::new("aspm")
    .args(["build", "examples/hello.affine"])
    .output()

  assert!(compile_result.status.success())

  // Run
  let run_result = Command::new("./target/hello.wasm")
    .output()

  assert_eq(run_result.stdout, "Hello, World!\n")
}

Testing with External Dependencies

#[test]
#[requires(database)]
fn test_database_integration() {
  let db = test::fixtures::setup_test_db()

  db.execute("INSERT INTO users (name) VALUES ('Alice')")

  let users = db.query("SELECT * FROM users")
  assert_len(users, 1)

  test::fixtures::teardown_test_db(db)
}

Benchmarking

Basic Benchmarks

use test::bench::*

#[bench]
fn bench_vector_push(b: &mut Bencher) {
  b.iter(|| {
    let mut v = Vec::new()
    for i in 0..1000 {
      v.push(i)
    }
    v
  })
}

#[bench]
fn bench_hashmap_insert(b: &mut Bencher) {
  b.iter(|| {
    let mut m = HashMap::new()
    for i in 0..1000 {
      m.insert(i, i * 2)
    }
    m
  })
}

Parameterized Benchmarks

#[bench]
#[params(size = [10, 100, 1000, 10000])]
fn bench_sort(b: &mut Bencher, size: Int) {
  let data = gen_random_vec(size)

  b.iter(|| {
    let mut copy = data.clone()
    copy.sort()
    copy
  })
}

Running Benchmarks

# Run all benchmarks
aspm bench

# Run specific benchmark
aspm bench --filter "bench_sort"

# Compare against baseline
aspm bench --baseline main

# Output to file
aspm bench --output results.json

Test Coverage

Generating Coverage Reports

# Run tests with coverage
aspm test --coverage

# Generate HTML report
aspm coverage --format html --output coverage/

# Generate lcov format
aspm coverage --format lcov --output coverage.lcov

Coverage in CI

# .github/workflows/ci.yml
- name: Run tests with coverage
  run: aspm test --coverage

- name: Upload coverage
  uses: codecov/codecov-action@v3
  with:
    file: coverage.lcov

See Also