feat: add stress test projects and testing documentation
- Add String.fromChar function to convert Char to String - Create four stress test projects demonstrating Lux features: - json-parser: recursive descent parsing with Char handling - markdown-converter: string manipulation and ADTs - todo-app: list operations and pattern matching - mini-interpreter: AST evaluation and environments - Add comprehensive testing documentation (docs/testing.md) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
272
docs/testing.md
Normal file
272
docs/testing.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Testing in Lux
|
||||
|
||||
This guide explains how to write and run tests for Lux programs.
|
||||
|
||||
## Test File Structure
|
||||
|
||||
Lux uses a simple test framework based on comparing program output against expected results. Tests are organized in the `tests/` directory.
|
||||
|
||||
### Expected Output Files
|
||||
|
||||
Each `.lux` file in `examples/` can have a corresponding `.expected` file in `tests/expected/` that contains the expected output:
|
||||
|
||||
```
|
||||
examples/
|
||||
hello.lux # The program
|
||||
tests/
|
||||
expected/
|
||||
hello.expected # Expected output
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Running All Tests
|
||||
|
||||
```bash
|
||||
# Run the full test suite
|
||||
cargo test
|
||||
|
||||
# Run with output
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
### Running a Single Program
|
||||
|
||||
```bash
|
||||
# Run a specific file
|
||||
./result/bin/lux examples/hello.lux
|
||||
|
||||
# Or with cargo
|
||||
cargo run -- examples/hello.lux
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Basic Program Test
|
||||
|
||||
Create a program in `examples/` and its expected output in `tests/expected/`:
|
||||
|
||||
```lux
|
||||
// examples/my_test.lux
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("Hello, Test!")
|
||||
Console.print("1 + 2 = " + toString(1 + 2))
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
```
|
||||
|
||||
Expected output (`tests/expected/my_test.expected`):
|
||||
```
|
||||
Hello, Test!
|
||||
1 + 2 = 3
|
||||
```
|
||||
|
||||
### Testing Pure Functions
|
||||
|
||||
For testing pure functions, create a test file that prints results:
|
||||
|
||||
```lux
|
||||
// examples/test_math.lux
|
||||
fn add(a: Int, b: Int): Int = a + b
|
||||
fn multiply(a: Int, b: Int): Int = a * b
|
||||
|
||||
fn assertEqual(expected: Int, actual: Int, name: String): Unit with {Console} =
|
||||
if expected == actual then Console.print(name + ": PASS")
|
||||
else Console.print(name + ": FAIL - expected " + toString(expected) + ", got " + toString(actual))
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
assertEqual(5, add(2, 3), "add(2, 3)")
|
||||
assertEqual(6, multiply(2, 3), "multiply(2, 3)")
|
||||
assertEqual(0, add(-1, 1), "add(-1, 1)")
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
```
|
||||
|
||||
### Testing with Effects
|
||||
|
||||
Test effect handlers by providing mock implementations:
|
||||
|
||||
```lux
|
||||
// Example: Testing with a mock Console
|
||||
effect Console {
|
||||
fn print(message: String): Unit
|
||||
}
|
||||
|
||||
// Real implementation would output to terminal
|
||||
// Test implementation captures output to a list
|
||||
handler testConsole(output: List<String>): Console {
|
||||
fn print(message) = {
|
||||
// In a real test, we'd capture this
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
|
||||
fn testableFunction(): Unit with {Console} = {
|
||||
Console.print("Test output")
|
||||
}
|
||||
|
||||
fn runTest(): Unit with {Console} = {
|
||||
// Run with test handler
|
||||
run testableFunction() with {
|
||||
Console = testConsole([])
|
||||
}
|
||||
Console.print("Test completed")
|
||||
}
|
||||
|
||||
let output = run runTest() with {}
|
||||
```
|
||||
|
||||
### Testing Result Types
|
||||
|
||||
Use pattern matching to test functions that return `Result`:
|
||||
|
||||
```lux
|
||||
fn testResults(): Unit with {Console} = {
|
||||
let success = Ok(42)
|
||||
let failure = Err("Something went wrong")
|
||||
|
||||
match success {
|
||||
Ok(n) => Console.print("Success with " + toString(n)),
|
||||
Err(e) => Console.print("Unexpected error: " + e)
|
||||
}
|
||||
|
||||
match failure {
|
||||
Ok(n) => Console.print("Unexpected success"),
|
||||
Err(e) => Console.print("Expected error: " + e)
|
||||
}
|
||||
}
|
||||
|
||||
let output = run testResults() with {}
|
||||
```
|
||||
|
||||
### Testing Option Types
|
||||
|
||||
```lux
|
||||
fn findEven(list: List<Int>): Option<Int> =
|
||||
List.find(list, fn(x: Int): Bool => x % 2 == 0)
|
||||
|
||||
fn testOptions(): Unit with {Console} = {
|
||||
match findEven([1, 3, 5]) {
|
||||
None => Console.print("No even number found: PASS"),
|
||||
Some(_) => Console.print("FAIL - found unexpected even")
|
||||
}
|
||||
|
||||
match findEven([1, 2, 3]) {
|
||||
None => Console.print("FAIL - should find 2"),
|
||||
Some(n) => Console.print("Found even: " + toString(n) + (if n == 2 then " PASS" else " FAIL"))
|
||||
}
|
||||
}
|
||||
|
||||
let output = run testOptions() with {}
|
||||
```
|
||||
|
||||
## Test Patterns
|
||||
|
||||
### Assertion Helpers
|
||||
|
||||
Create reusable assertion functions:
|
||||
|
||||
```lux
|
||||
fn assertEq<T>(expected: T, actual: T, desc: String): Unit with {Console} =
|
||||
Console.print(desc + ": " + (if expected == actual then "PASS" else "FAIL"))
|
||||
|
||||
fn assertTrue(condition: Bool, desc: String): Unit with {Console} =
|
||||
Console.print(desc + ": " + (if condition then "PASS" else "FAIL"))
|
||||
|
||||
fn assertFalse(condition: Bool, desc: String): Unit with {Console} =
|
||||
Console.print(desc + ": " + (if condition then "FAIL" else "PASS"))
|
||||
```
|
||||
|
||||
### Grouping Tests
|
||||
|
||||
Organize related tests into groups:
|
||||
|
||||
```lux
|
||||
fn testGroup(name: String): Unit with {Console} = {
|
||||
Console.print("")
|
||||
Console.print("=== " + name + " ===")
|
||||
}
|
||||
|
||||
fn runAllTests(): Unit with {Console} = {
|
||||
testGroup("List Operations")
|
||||
testListHead()
|
||||
testListTail()
|
||||
testListMap()
|
||||
|
||||
testGroup("String Operations")
|
||||
testStringLength()
|
||||
testStringSplit()
|
||||
|
||||
Console.print("")
|
||||
Console.print("All tests completed")
|
||||
}
|
||||
```
|
||||
|
||||
## Example Test Files
|
||||
|
||||
Look at the example projects for comprehensive test patterns:
|
||||
|
||||
- `projects/json-parser/main.lux` - Tests recursive parsing
|
||||
- `projects/todo-app/main.lux` - Tests ADTs and list operations
|
||||
- `projects/mini-interpreter/main.lux` - Tests complex pattern matching
|
||||
- `projects/markdown-converter/main.lux` - Tests string manipulation
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
The test suite runs automatically on every commit. To run locally:
|
||||
|
||||
```bash
|
||||
# Build and test
|
||||
nix build
|
||||
nix develop --command cargo test
|
||||
|
||||
# Test all example programs
|
||||
for f in examples/standard/*.lux; do
|
||||
echo "Testing $f"
|
||||
./result/bin/lux "$f" > /tmp/out.txt 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " OK"
|
||||
else
|
||||
echo " FAILED"
|
||||
cat /tmp/out.txt
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## Debugging Tests
|
||||
|
||||
### Verbose Output
|
||||
|
||||
Add debug prints to trace execution:
|
||||
|
||||
```lux
|
||||
fn debug(msg: String): Unit with {Console} =
|
||||
Console.print("[DEBUG] " + msg)
|
||||
|
||||
fn myFunction(x: Int): Int with {Console} = {
|
||||
debug("Input: " + toString(x))
|
||||
let result = x * 2
|
||||
debug("Output: " + toString(result))
|
||||
result
|
||||
}
|
||||
```
|
||||
|
||||
### Type Checking Only
|
||||
|
||||
Check types without running:
|
||||
|
||||
```bash
|
||||
# Type check only (when available)
|
||||
./result/bin/lux --check examples/myfile.lux
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **One concern per test** - Each test should verify one specific behavior
|
||||
2. **Descriptive names** - Use clear test function names that describe what's being tested
|
||||
3. **Expected output** - Create `.expected` files for reproducible testing
|
||||
4. **Test edge cases** - Include tests for empty lists, zero values, error conditions
|
||||
5. **Keep tests fast** - Avoid complex computations in tests
|
||||
6. **Test effects separately** - Mock effects to isolate behavior being tested
|
||||
Reference in New Issue
Block a user