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:
192
stdlib/testing.lux
Normal file
192
stdlib/testing.lux
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user