Technical Documentation & API Reference
Complete guide to building robust software with hegeltest and hegeltest_flutter. From primitive property checks to automated binary shrinking, accessibility monkey fuzzing, and distribution coverage contracts.
Architecture & Overview
hegeltest is a state-of-the-art property-based testing and fuzzing engine for Dart and Flutter. Unlike traditional randomized testing that sprays arbitrary values, Hegel is built upon a high-performance native Rust core (libhegel) communicating via zero-copy C-FFI.
| Component | Package | Responsibility |
|---|---|---|
| Core Engine | hegeltest |
Native FFI bindings, PRNG byte streams, deterministic shrinking, standalone runner, and standard generator library. |
| Flutter Bridge | hegeltest_flutter |
flutter_test harness integration, widget tree sweeps, accessibility tree monkey fuzzing, and layout constraint invariant checks. |
| Native Core | libhegel |
Cross-platform pre-compiled binaries (macOS arm64, Linux x64/arm64, Windows x64/arm64) with SHA-256 integrity verification. |
Quickstart: Dart Core
Add hegeltest to your Dart project's dev_dependencies:
dart pub add dev:hegeltest
Write your first property test in test/math_test.dart:
import 'package:test/test.dart';
import 'package:hegeltest/hegeltest.dart';
void main() {
hegelTest('addition is commutative across integers', (tc) {
final a = tc.draw(integers(), label: 'a');
final b = tc.draw(integers(), label: 'b');
expect(a + b, equals(b + a));
}, testCases: 200);
}
Run standard Dart tests as usual. Hegel's Dart build hook dynamically unpacks and links the native library:
dart test
Quickstart: Flutter
Flutter projects use flutter_test rather than package:test. Install hegeltest_flutter to seamlessly bind the property runner inside Flutter's widget testing lifecycle:
flutter pub add dev:hegeltest_flutter
Create a test in test/widget_property_test.dart:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hegeltest_flutter/hegeltest_flutter.dart';
void main() {
hegelFlutterWidgetTest('text renders correctly without throwing', (tc, tester) async {
final title = tc.draw(text(minSize: 1, maxSize: 80), label: 'title');
await tester.pumpWidget(MaterialApp(
home: Scaffold(body: Center(child: Text(title))),
));
expect(find.text(title), findsOneWidget);
});
}
Property Testing vs Unit Testing
Traditional unit tests verify software by asserting hand-picked examples: "When given [1, 2, 3], return [3, 2, 1]". This leaves massive voids where edge cases hide: empty collections, Unicode zero-width spaces, integer overflows, NaN, and complex state sequences.
Property-based testing inverts this paradigm: you define universal invariants (e.g. "Reversing a list twice returns the identical list" or "Encoding then decoding JSON produces an equivalent object"), and Hegel automatically constructs hundreds of adversarial inputs to break your assertions.
Deterministic Binary Shrinking
When random fuzzing discovers a defect, the failing input is usually bloated with dozens of irrelevant fields and noise. Hegel's native Rust engine executes deterministic byte-level shrinking:
- Input generation is driven by a canonical byte choice buffer consumed through FFI.
- Upon failure, the engine applies binary search, bit deletion, zero-byte minimization, and token reductions directly to the input choice buffer.
- The test iteration is repeatedly re-executed until the minimal reproducing counterexample is isolated.
[-1], saving hours of debugging time.
Stateful Invariant Verification
Stateful systems (caches, state management stores, database drivers, UI navigation stacks) fail due to subtle sequences of actions rather than a single input. With StateMachine, you declare actions and invariants:
class StackMachine extends StateMachine {
final List<int> actual = [];
int modelCount = 0;
@override
List<StateRule> get rules => [
StateRule('push', execute: (tc) {
final val = tc.draw(integers());
actual.add(val);
modelCount++;
}),
StateRule('pop', precondition: () => actual.isNotEmpty, execute: (tc) {
actual.removeLast();
modelCount--;
}),
];
@override
List<StateInvariant> get invariants => [
StateInvariant('actual length equals model count', check: (tc) {
expect(actual.length, equals(modelCount));
}),
];
}
void main() {
hegelTest('stack preserves model invariant', () => StackMachine());
}
Persistent Regression Database (.hegel/)
When Hegel discovers a failing input, it persists the minimal reproduction seed to .hegel/examples/ on disk. On every subsequent test execution:
- Phase 1 (Reuse): Prior failures are loaded from disk and replayed on iteration 1. If your bug was not fixed, the test fails in sub-milliseconds without wasting cycles generating new random values.
- Phase 2 (Generate): If all known historical failures pass, fresh property exploration continues.
- Phase 3 (Shrink): If a new failure is encountered, it is minimized and saved back to the database.
.gitignore inside .hegel/. However, caching the .hegel/ directory in your CI workflow prevents regression slips across pull requests.
Generators Cheat Sheet
Import either package:hegeltest/hegeltest.dart or package:hegeltest/generators.dart to access the comprehensive generator catalog.
1. Primitives & Numbers
| Generator | Type | Parameters & Constraints | Edge Cases Covered |
|---|---|---|---|
integers() |
int |
min, max (supports full 64-bit range) |
0, 1, -1, min, max |
doubles() |
double |
min, max, includeSpecialValues: true |
0.0, -0.0, NaN, +Infinity, -Infinity |
booleans() |
bool |
None | true, false |
bigIntegers() |
BigInt |
min, max, bitLength |
Arbitrary-precision integer boundaries |
2. Strings & Text
| Generator | Parameters | Description |
|---|---|---|
text() |
alphabet, minSize, maxSize |
Full Unicode strings including surrogate pairs, zero-width joiners, and emoji. |
fromRegex(pattern) |
String pattern |
Synthesizes valid strings strictly adhering to regular expression grammar. |
emails() |
None | Valid RFC 5322 email addresses. |
urls() |
schemes |
Valid absolute URI strings with paths, query params, and fragments. |
uuids() |
None | Valid v4 UUID formatted strings (8-4-4-4-12). |
3. Collections
| Generator | Type | Parameters |
|---|---|---|
lists(elementGen) |
List<T> |
minSize, maxSize, unique: bool |
sets(elementGen) |
Set<T> |
minSize, maxSize |
maps(keyGen, valueGen) |
Map<K, V> |
minSize, maxSize |
4. Combinators & Weighted Sampling
| Combinator | Signature | Description |
|---|---|---|
oneOf() |
oneOf([genA, genB]) |
Uniform random selection between multiple generators. |
sampled() |
sampled([v1, v2, v3]) |
Uniform random selection from a fixed literal list. |
sampledWeighted() |
sampledWeighted([(weight, value), ...]) |
Picks a value based on relative integer frequency weights. |
oneOfWeighted() |
oneOfWeighted([(weight, gen), ...]) |
Picks a generator based on relative integer frequency weights. |
frequency() |
frequency([(weight, gen), ...]) |
Alias for weighted generator selection (Hypothesis / QuickCheck convention). |
nullable() |
nullable(gen, {double nullProbability: 0.1}) |
Yields either null or a value drawn from the inner generator. |
5. Temporal & Network
| Generator | Output Type | Details |
|---|---|---|
dates() |
DateTime |
Generates UTC date-only timestamps (hours/mins/seconds set to 0). |
dateTimes() |
DateTime |
Full microsecond-precision timestamps with boundary constraints. |
ipv4Addresses() |
String |
Valid dotted-decimal IPv4 addresses (0.0.0.0 – 255.255.255.255). |
ipv6Addresses() |
String |
Valid hex-colon IPv6 network addresses. |
bytes() |
Uint8List |
Raw binary payload buffers with length constraints. |
Distribution Contracts & Coverage
A hidden weakness in randomized testing is test distribution drift. If your generator accidentally produces empty lists 99% of the time, complex business logic for populated lists never executes.
hegeltest solves this with executable Coverage Contracts:
hegelTest('payment routing satisfies SLA distribution contracts', (tc) {
final tier = tc.draw(
sampledWeighted([
(70, 'standard'),
(25, 'pro'),
(5, 'enterprise'),
]),
label: 'tier',
);
final cart = tc.draw(lists(integers(min: 1, max: 500), minSize: 0, maxSize: 50));
// 1. Classify observations for distribution quality telemetry
tc.classify(cart.isEmpty, 'empty cart', label: 'cart_state');
tc.classify(cart.length >= 20, 'bulk items', label: 'cart_state');
// 2. Enforce minimum coverage contracts
tc.cover(2.0, tier == 'enterprise', 'enterprise tier exercised');
tc.cover(10.0, cart.isEmpty, 'empty cart edge case exercised');
final invoice = processOrder(tier, cart);
expect(invoice.total, greaterThanOrEqualTo(0));
}, testCases: 300);
Telemetry Output
When running tests with verbosity: Verbosity.verbose or via the standalone runner, Hegel prints the exact percentage breakdown of every classification and coverage target:
Collected distribution statistics:
cart_state:
68.7% normal (1-19 items)
19.3% bulk items
12.0% empty cart
tier:
70.3% standard
24.7% pro
5.0% enterprise [Target: >= 2.0% - PASSED]
Flutter Testing Deep Dive
hegeltest_flutter provides five purpose-built testing harnesses targeting different levels of Flutter application architecture:
1. Accessibility Monkey Fuzzing (hegelFlutterMonkeyTest)
Traverses Flutter's active SemanticsOwner tree, discovers actionable accessibility elements (buttons, inputs, sliders, scrollables), and executes randomized interactive action sequences. When a crash occurs, Hegel shrinks the action sequence to the minimal steps to reproduce:
hegelFlutterMonkeyTest(
'fuzzes checkout workflow without crashing',
createWidget: (tc) => const CheckoutApp(),
steps: 30,
allowedActions: [
SemanticsAction.tap,
SemanticsAction.setText,
SemanticsAction.scrollDown,
],
invariant: (tc, tester) async {
expect(find.byType(CheckoutApp), findsOneWidget);
},
);
2. Layout & Screen Size Sweeps (hegelFlutterLayoutSweepTest)
Sweeps viewport constraints, device pixel ratios, right-to-left localizations, and dynamic user font scaling factors (TextScaler) to detect layout overflows (A RenderFlex overflowed by ... pixels):
hegelFlutterLayoutSweepTest(
'user card never overflows across screen sizes and text scales',
sweepConfig: const LayoutSweepConfig(
minWidth: 320,
maxWidth: 1024,
minHeight: 480,
maxHeight: 1200,
minTextScale: 0.8,
maxTextScale: 2.5,
),
builder: (tc, sample) => const UserProfileCard(),
);
Performance & Benchmark Metrics
Hegel is architected to eliminate the traditional performance bottlenecks of property-based testing in Dart. By pairing a high-throughput native Rust PRNG choice stream with zero-copy C-FFI pinned memory buffers, test suites run at production scale without stalling developer feedback loops.
Empirical Benchmark Telemetry
Measurements executed using benchmark/run_benchmarks.dart with JIT warmup cycles and statistical outlier elimination:
| Category | Benchmark Operation | Throughput | Median Latency | P95 Latency | Notes |
|---|---|---|---|---|---|
| Throughput | empty_property_iteration |
90M+ ops/s | 0.053 ms / 10k | 0.102 ms / 10k | Pure native PRNG choice stream & iteration loop dispatch overhead. |
| Throughput | integers_single_draw |
250k+ ops/s | 39.07 ms / 10k | 41.05 ms / 10k | Full property iteration with 64-bit integer drawing and identity checks. |
| Throughput | sampled_weighted_choice |
19M+ ops/s | 0.465 ms / 10k | 0.714 ms / 10k | Fast alias-method weighted discrete distribution sampling. |
| Throughput | coverage_contracts_telemetry |
9.5M+ ops/s | 0.510 ms / 5k | 0.580 ms / 5k | Live distribution telemetry tracking with tc.classify() and tc.cover(). |
| Throughput | composite_domain_object |
14.2k ops/s | 166.85 ms / 2.5k | 208.20 ms / 2.5k | Complex nested model: ID, text name, email regex, boolean, and list of tags. |
| Shrinking | shrink_integer_boundary |
18/s | 53.14 ms | 65.78 ms | Binary searches arbitrary 64-bit integer space to exact minimal failing boundary. |
| Shrinking | shrink_string_delimiter |
170/s | 5.00 ms | 10.36 ms | Minimizes arbitrary multi-character string to exact 1-character failure trigger. |
| Database | database_replay_cached_failure |
3,400+ ops/s | 0.280 ms | 0.356 ms | Sub-millisecond disk cache lookup & replay from .hegel/ on iteration 1. |
Running Benchmarks Locally
The benchmark runner is built into hegel-dart with zero third-party dependencies:
# Run full benchmark suite and print ASCII table:
dart run benchmark/run_benchmarks.dart
# Output GitHub-flavored Markdown for CI summaries:
dart run benchmark/run_benchmarks.dart --format=markdown
# Output structured JSON for telemetry dashboards:
dart run benchmark/run_benchmarks.dart --format=json
CI/CD Caching Recipes
Persist the .hegel/ regression database across CI runs using GitHub Actions cache. This ensures tests fail immediately on iteration 1 if an old regression reappears:
- name: Cache Hegel counterexamples
uses: actions/cache@v4
with:
path: .hegel/
key: hegel-${{ runner.os }}-${{ github.ref_name }}
restore-keys: |
hegel-${{ runner.os }}-main
hegel-${{ runner.os }}-
Configuration & Environment Flags
| Flag / Variable | Default | Purpose |
|---|---|---|
HEGEL_SEED |
Unset | Fixes the deterministic PRNG seed for all test cases. Crucial for pinpoint CI bug isolation. |
HEGEL_DATABASE |
1 (Enabled) |
Set to 0 or false to disable reading/writing counterexamples to .hegel/. |
HEGEL_LIBHEGEL_PATH |
Bundled Native | Explicit filesystem override path for custom compiled libhegel_c.dylib, .so, or .dll. |
testCases |
100 |
Number of randomized iterations executed per test run. |
reproduce |
null |
Hex reproduction blob printed during failure. Paste into test options to immediately replay the minimal failure. |