Skip to content

Jesssullivan/quickchpl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

quickchpl

Simple Property-Based Testing for Chapel

Chapel License: MIT GitLab

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_test

Output:

âś“ addition is commutative passed 100 tests

Generators

Primitive Generators

// 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

Composite Generators

// 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);

Generator Combinators

// 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));

Properties

Basic Properties

var prop = property(
  "absolute value is non-negative",
  intGen(),
  proc(x: int) { return abs(x) >= 0; }
);

var result = check(prop);
assert(result.passed);

Conditional Properties (Implication)

// 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);
  }
);

Convenience Functions

// 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; });

Property Patterns

The Patterns module provides reusable predicate functions for testing common properties. All pattern predicates are available when you use quickchpl;.

Algebraic Patterns

// 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); }
);

Functional Patterns

// 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; } }
    );
  }
);

Shrinking

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)

Shrinking Strategies

  • 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

GitLab CI

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

GitHub Actions

- name: Run property tests
  run: |
    chpl tests/my_properties.chpl -M quickchpl/src -o /tmp/props
    /tmp/props --numTests=1000

Configuration

export 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);

API

Core Types

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

Functions

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

Todos & Future work:

  • 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-2025 done! one is good for now)

Notes:

  • module include statements are not yet stable <-- this is expected
  • list.parSafe is unstable <-- this is expected
  • use of routines as values is unstable <-- routines are necessary AFAICT to achieve this familiar PBT structure

About

Simple Property-Based Testing for Chapel Language

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published