feat: add property-based testing framework

Implements property-based testing infrastructure:

stdlib/testing.lux:
- Generators: genInt, genIntList, genString, genBool, etc.
- Shrinking helpers: shrinkInt, shrinkList, shrinkString
- Property helpers: isSorted, sameElements

examples/property_testing.lux:
- 10 property tests demonstrating the framework
- Tests for: involution, commutativity, associativity, identity
- 100 iterations per property with random inputs

docs/guide/14-property-testing.md:
- Complete guide to property-based testing
- Generator patterns and common properties
- Best practices and examples

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 04:39:50 -05:00
parent 87c1fb1bbd
commit b02807ebf4
4 changed files with 748 additions and 0 deletions

192
stdlib/testing.lux Normal file
View File

@@ -0,0 +1,192 @@
// Property-Based Testing Library for Lux
//
// This module provides generators and helper functions for property-based testing.
//
// Usage:
// Use the generator functions (genInt, genIntList, genString, etc.) in your
// property tests to generate random test data.
//
// Example:
// fn testReverseInvolutive(n: Int): Unit with {Console, Test, Random} = {
// if n <= 0 then
// Console.print(" PASS reverse(reverse(xs)) == xs")
// else {
// let xs = genIntList(0, 100, 20)
// if List.reverse(List.reverse(xs)) == xs then
// testReverseInvolutive(n - 1)
// else
// Test.fail("Property failed")
// }
// }
// ============================================================
// Generator Module
// ============================================================
// Character set for string generation
let GEN_CHARS = "abcdefghijklmnopqrstuvwxyz"
let GEN_ALPHANUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
// Generate a random integer in a range
fn genInt(min: Int, max: Int): Int with {Random} =
Random.int(min, max)
// Generate a random integer from 0 to max
fn genIntUpTo(max: Int): Int with {Random} =
Random.int(0, max)
// Generate a random positive integer
fn genPositiveInt(max: Int): Int with {Random} =
Random.int(1, max)
// Generate a random boolean
fn genBool(): Bool with {Random} =
Random.bool()
// Generate a random character (lowercase letter)
fn genChar(): String with {Random} = {
let idx = Random.int(0, 25)
String.substring(GEN_CHARS, idx, idx + 1)
}
// Generate a random alphanumeric character
fn genAlphaNum(): String with {Random} = {
let idx = Random.int(0, 61)
String.substring(GEN_ALPHANUM, idx, idx + 1)
}
// Generate a random string of given max length
fn genString(maxLen: Int): String with {Random} = {
let len = Random.int(0, maxLen)
genStringOfLength(len)
}
// Generate a random string of exact length
fn genStringOfLength(len: Int): String with {Random} = {
if len <= 0 then
""
else
genChar() + genStringOfLength(len - 1)
}
// Generate a random list of integers
fn genIntList(min: Int, max: Int, maxLen: Int): List<Int> with {Random} = {
let len = Random.int(0, maxLen)
genIntListOfLength(min, max, len)
}
// Generate a random list of integers with exact length
fn genIntListOfLength(min: Int, max: Int, len: Int): List<Int> with {Random} = {
if len <= 0 then
[]
else
List.concat([Random.int(min, max)], genIntListOfLength(min, max, len - 1))
}
// Generate a random list of booleans
fn genBoolList(maxLen: Int): List<Bool> with {Random} = {
let len = Random.int(0, maxLen)
genBoolListOfLength(len)
}
fn genBoolListOfLength(len: Int): List<Bool> with {Random} = {
if len <= 0 then
[]
else
List.concat([Random.bool()], genBoolListOfLength(len - 1))
}
// Generate a random list of strings
fn genStringList(maxStrLen: Int, maxLen: Int): List<String> with {Random} = {
let len = Random.int(0, maxLen)
genStringListOfLength(maxStrLen, len)
}
fn genStringListOfLength(maxStrLen: Int, len: Int): List<String> with {Random} = {
if len <= 0 then
[]
else
List.concat([genString(maxStrLen)], genStringListOfLength(maxStrLen, len - 1))
}
// ============================================================
// Shrinking Module
// ============================================================
// Shrink an integer towards zero
fn shrinkInt(n: Int): List<Int> = {
if n == 0 then
[]
else if n > 0 then
[0, n / 2, n - 1]
else
[0, n / 2, n + 1]
}
// Shrink a list by removing elements
fn shrinkList<T>(xs: List<T>): List<List<T>> = {
let len = List.length(xs)
if len == 0 then
[]
else if len == 1 then
[[]]
else {
// Return: empty list, first half, second half
let half = len / 2
let firstHalf = List.take(xs, half)
let secondHalf = List.drop(xs, half)
[[], firstHalf, secondHalf]
}
}
// Shrink a string by removing characters
fn shrinkString(s: String): List<String> = {
let len = String.length(s)
if len == 0 then
[]
else if len == 1 then
[""]
else {
let half = len / 2
["", String.substring(s, 0, half), String.substring(s, half, len)]
}
}
// ============================================================
// Common Properties
// ============================================================
// Check that a list is sorted
fn isSorted(xs: List<Int>): Bool = {
match List.head(xs) {
None => true,
Some(first) => {
match List.tail(xs) {
None => true,
Some(rest) => isSortedHelper(first, rest)
}
}
}
}
fn isSortedHelper(prev: Int, xs: List<Int>): Bool = {
match List.head(xs) {
None => true,
Some(curr) => {
if prev <= curr then {
match List.tail(xs) {
None => true,
Some(rest) => isSortedHelper(curr, rest)
}
} else
false
}
}
}
// Check that two lists have the same elements (ignoring order)
fn sameElements(xs: List<Int>, ys: List<Int>): Bool = {
List.length(xs) == List.length(ys) &&
List.all(xs, fn(x) => List.contains(ys, x)) &&
List.all(ys, fn(y) => List.contains(xs, y))
}