Simple Property-Based Testing for Chapel
Inspired by QuickCheck and my father ^w^
use quickchpl;
proc main() {
// Test that addition is commutative
var gen = tupleGen(intGen(-100, 100), intGen(-100, 100));
var prop = property(
"addition is commutative",
gen,
proc(args: (int, int)) {
const (a, b) = args;
return a + b == b + a;
}
);
var result = check(prop);
if result.passed {
writeln("âś“ ", prop.name, " passed ", result.numTests, " tests");
} else {
writeln("âś— ", prop.name, " FAILED");
writeln(" Counterexample: ", result.failureInfo);
}
}Compile and run:
chpl my_test.chpl -M path/to/quickchpl/src
./my_testOutput:
âś“ addition is commutative passed 100 tests
// Integers
var intG = intGen(-100, 100); // Range [-100, 100]
var natG = natGen(1000); // Natural numbers [0, 1000]
var posG = positiveIntGen(1000); // Positive [1, 1000]
// Real numbers
var realG = realGen(0.0, 1.0); // Uniform [0, 1)
var normalG = realGen(0.0, 1.0, Distribution.Normal);
// Booleans
var boolG = boolGen(0.5); // 50% true probability
// Strings
var strG = stringGen(0, 50); // Length 0-50, alphanumeric
var alphaG = alphaGen(5, 10); // Alphabetic only// Tuples
var pairG = tupleGen(intGen(), stringGen());
var tripleG = tupleGen(intGen(), intGen(), intGen());
// Lists
var listG = listGen(intGen(), 0, 10); // List of 0-10 integers
// Fixed values
var constG = constantGen(42);// Transform output
var doubledG = map(intGen(), proc(x: int) { return x * 2; });
// Filter values
var evenG = filter(intGen(), proc(x: int) { return x % 2 == 0; });
// Combine generators
var zippedG = zip(intGen(), stringGen());
// Random choice
var choiceG = oneOf(intGen(0, 10), intGen(100, 200));
// Weighted choice
var weightedG = frequency(9, intGen(0, 10), 1, intGen(1000, 2000));var prop = property(
"absolute value is non-negative",
intGen(),
proc(x: int) { return abs(x) >= 0; }
);
var result = check(prop);
assert(result.passed);// Property only checked when condition is true
// (implies is available from quickchpl module)
var prop = property(
"division by non-zero",
tupleGen(intGen(), intGen()),
proc(args: (int, int)) {
const (a, b) = args;
return implies(b != 0, a / b * b == a - a % b);
}
);// Quick one-liner check
assert(quickCheck(intGen(), proc(x: int) { return x + 0 == x; }));
// forAll syntax
var result = forAll(intGen(), proc(x: int) { return x * 1 == x; });The Patterns module provides reusable predicate functions for testing common properties.
All pattern predicates are available when you use quickchpl;.
// Test that addition is commutative
var prop = property(
"addition is commutative",
tupleGen(intGen(-100, 100), intGen(-100, 100)),
proc(args: (int, int)) {
const (a, b) = args;
return isCommutative(a, b, proc(x: int, y: int) { return x + y; });
}
);
// Or use ready-made predicates for integers
var commProp = property(
"integer addition commutes",
tupleGen(intGen(), intGen()),
proc(args: (int, int)) {
const (a, b) = args;
return intAddCommutative(a, b);
}
);
// Test associativity
var assocProp = property(
"addition is associative",
tupleGen(intGen(), intGen(), intGen()),
proc(args: (int, int, int)) {
const (a, b, c) = args;
return intAddAssociative(a, b, c);
}
);
// Test identity element
var idProp = property(
"zero is additive identity",
intGen(),
proc(a: int) { return intAddIdentity(a); }
);// Idempotence: f(f(x)) = f(x)
var idempProp = property(
"abs is idempotent",
intGen(),
proc(x: int) {
return isIdempotent(x, proc(n: int) { return abs(n); });
}
);
// Involution: f(f(x)) = x
var invProp = property(
"negation is involution",
intGen(),
proc(x: int) {
return isInvolution(x, proc(n: int) { return -n; });
}
);
// Round-trip: decode(encode(x)) = x
var roundTripProp = property(
"int<->string round-trip",
intGen(),
proc(x: int) {
return isRoundTrip(x,
proc(n: int) { return n:string; },
proc(s: string): int { return try! s:int; catch { return 0; } }
);
}
);When a property fails, quickchpl automatically shrinks the counterexample to find the minimal failing case:
// Property fails for x >= 50
var prop = property(
"x is small",
intGen(0, 1000),
proc(x: int) { return x < 50; }
);
var result = check(prop);
// result.failureInfo might be "847"
// result.shrunkInfo will be "50" (minimal failing case)- Integers: Binary search towards 0
- Reals: Try 0, truncated, rounded values
- Strings: Try empty, remove chars, simplify to 'a'
- Lists: Try empty, remove elements, shrink elements
- Tuples: Shrink each component
include:
- remote: 'https://gitlab.com/tinyland/projects/quickchpl/-/raw/main/ci/.gitlab-ci.yml'
property_tests:
extends: .property_test_template
script:
- chpl tests/my_properties.chpl -M quickchpl/src -o /tmp/props
- /tmp/props --numTests=1000- name: Run property tests
run: |
chpl tests/my_properties.chpl -M quickchpl/src -o /tmp/props
/tmp/props --numTests=1000export CHPL_MODULE_PATH=$CHPL_MODULE_PATH:$PWD/quickchpl/src./my_tests --numTests=1000 --maxShrinkSteps=500 --verbose=true...Or in code:
var runner = new PropertyRunner(
numTests = 1000,
maxShrinkSteps = 500,
verboseMode = true
);
var result = runner.check(prop);| Type | Description |
|---|---|
IntGenerator |
Generates random integers |
RealGenerator |
Generates random real numbers |
BoolGenerator |
Generates random booleans |
StringGenerator |
Generates random strings |
Property |
Defines a property to test |
PropertyRunner |
Executes property tests |
TestResult |
Contains test results |
| Function | Description |
|---|---|
intGen(min, max) |
Create integer generator |
property(name, gen, pred) |
Define a property |
check(prop) |
Run property test |
quickCheck(gen, pred) |
One-liner property check |
shrink(value) |
Generate shrink candidates |
- Integrate with Chapel Mason package repo (in progress)
- Integrate Outbot Harness
- Integrate with (and
[ ]publish) sister projects,chapel-k8s-mail,chapel-git,tinymachines,mariolex) - Add IDE and LLM friendly text and code completions (docs in the works)
- Provide public demo (
aoc-2025done! one is good for now)
module include statements are not yet stable<-- this is expectedlist.parSafe is unstable<-- this is expecteduse of routines as values is unstable<-- routines are necessary AFAICT to achieve this familiar PBT structure