v0.9.0 Stable Dart ≥ 3.10 Flutter ≥ 3.38 Rust FFI Core

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.

Industrial Design Principle: Pure-Dart property testing packages suffer from slow shrinking cycles and lack of inter-run memory. Hegel pairs high-level idiomatic Dart APIs with native deterministic PRNG streams, byte-level test case shrinking, and persistent regression databases on disk.
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:

terminal
dart pub add dev:hegeltest

Write your first property test in test/math_test.dart:

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:

terminal
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:

terminal
flutter pub add dev:hegeltest_flutter

Create a test in test/widget_property_test.dart:

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.
Shrinking Output Example: A 500-element list with random negative integers that failed on negative numbers is automatically reduced to exactly [-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:

dart
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:

  1. 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.
  2. Phase 2 (Generate): If all known historical failures pass, fresh property exploration continues.
  3. Phase 3 (Shrink): If a new failure is encountered, it is minimized and saved back to the database.
Git Continuity: Hegel automatically places a .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:

dart
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:

telemetry
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:

dart
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):

dart
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:

terminal
# 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:

yaml
- 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.