From e120c997a55ef6dcf9a7218aafa56a7bc11fe0bf Mon Sep 17 00:00:00 2001 From: Esau Rodriguez Date: Fri, 28 Aug 2026 11:47:30 +0100 Subject: [PATCH 1/2] feat: Add hegel-jna: a JNA-backed artifact supporting Java 17+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the build into two published artifacts that share one API and one source tree: - dev.hegel:hegel — the existing FFM binding, unchanged for users (Java 22+, Automatic-Module-Name dev.hegel). - dev.hegel:hegel-jna — a new JNA binding (Java 17+), with net.java.dev.jna:jna as its only dependency. To make the binding seam backend-neutral, opaque engine handles now cross the Libhegel interface as raw addresses (long, 0 = NULL) instead of MemorySegment; RealLibhegel wraps/unwraps at the FFI boundary and FakeLibhegel no longer touches java.lang.foreign. Each module supplies a package-private LibhegelBackend.open(Path) that Engine calls without knowing which backend it got. The backend-neutral sources and tests move to shared/, compiled into both jars via build-helper add-source, so the whole behaviour suite runs against both bindings (242 tests on FFM, 241 on JNA, 100% instruction+branch coverage per module). JnaLibhegel maps C bool as byte and size_t as long (the bundled natives are all 64-bit), passes date/time structs by value, and holds each run's output callback strongly until runFree. Build and tooling follow the multi-module layout: hegel-parent pom, per-module natives fetch and jacoco gates, a test-jna CI job proving the artifact runs on JDK 17 and 21, release.py bumping all three poms and verifying both artifacts on Central, and README/CLAUDE.md docs. Surefire now passes --enable-native-access=ALL-UNNAMED in both modules (JNA triggers the JDK 24+ JEP 472 warning too). The shared suite's one Java-21 construct (record patterns in ConformanceTest) is rewritten for 17, and the Nd-category assertion tolerates codepoints newer than the running JVM's Unicode tables. Co-Authored-By: Claude Fable 5 --- .claude/CLAUDE.md | 67 +- .github/scripts/release.py | 55 +- .github/workflows/ci.yml | 32 +- .vscode/settings.json | 3 + README.md | 22 +- RELEASE.md | 22 + hegel-jna/pom.xml | 81 ++ .../src/main/java/dev/hegel/JnaLibhegel.java | 872 ++++++++++++++++++ .../main/java/dev/hegel/LibhegelBackend.java | 11 + .../test/java/dev/hegel/JnaLibhegelTest.java | 167 ++++ hegel/pom.xml | 83 ++ .../it/module-consumer/invoker.properties | 0 {src => hegel/src}/it/module-consumer/pom.xml | 0 .../src/main/java/module-info.java | 0 .../java/dev/hegel/consumer/ConsumerTest.java | 0 .../main/java/dev/hegel/LibhegelBackend.java | 11 + .../main/java/dev/hegel/RealLibhegel.java | 242 ++--- .../test/java/dev/hegel/FfmCoverageTest.java | 32 + .../test/java/dev/hegel/RealLibhegelTest.java | 34 +- justfile | 8 +- pom.xml | 389 ++++---- .../java-templates/dev/hegel/BuildInfo.java | 0 .../src}/main/java/dev/hegel/Abi.java | 0 .../main/java/dev/hegel/AssumeRejected.java | 0 .../src}/main/java/dev/hegel/Backend.java | 0 .../src}/main/java/dev/hegel/DataSource.java | 0 .../src}/main/java/dev/hegel/Database.java | 0 .../src}/main/java/dev/hegel/Engine.java | 2 +- .../src}/main/java/dev/hegel/Generated.java | 0 .../src}/main/java/dev/hegel/Generator.java | 0 .../src}/main/java/dev/hegel/Generators.java | 0 .../src}/main/java/dev/hegel/HealthCheck.java | 0 .../java/dev/hegel/HealthCheckFailure.java | 0 .../src}/main/java/dev/hegel/Hegel.java | 0 .../main/java/dev/hegel/HegelException.java | 0 .../src}/main/java/dev/hegel/HegelTest.java | 0 .../java/dev/hegel/HegelTestExtension.java | 0 .../src}/main/java/dev/hegel/Invariant.java | 0 .../src}/main/java/dev/hegel/Libhegel.java | 119 +-- .../main/java/dev/hegel/LibraryLoader.java | 0 .../main/java/dev/hegel/LiveDataSource.java | 19 +- .../src}/main/java/dev/hegel/Mode.java | 0 .../src}/main/java/dev/hegel/OptBoolean.java | 0 .../src}/main/java/dev/hegel/Phase.java | 0 .../src}/main/java/dev/hegel/Pool.java | 0 .../src}/main/java/dev/hegel/Rule.java | 0 .../src}/main/java/dev/hegel/Runner.java | 42 +- .../src}/main/java/dev/hegel/Settings.java | 0 .../src}/main/java/dev/hegel/Stateful.java | 0 .../src}/main/java/dev/hegel/StopTest.java | 0 .../java/dev/hegel/StringGeneratorHandle.java | 13 +- .../src}/main/java/dev/hegel/TestCase.java | 0 .../src}/main/java/dev/hegel/Tuple2.java | 0 .../src}/main/java/dev/hegel/Tuple3.java | 0 .../src}/main/java/dev/hegel/Tuple4.java | 0 .../src}/main/java/dev/hegel/Tuple5.java | 0 .../src}/main/java/dev/hegel/Tuple6.java | 0 .../src}/main/java/dev/hegel/Tuple7.java | 0 .../src}/main/java/dev/hegel/Tuple8.java | 0 .../src}/main/java/dev/hegel/Verbosity.java | 0 .../dev/hegel/generators/BinaryGenerator.java | 0 .../hegel/generators/BooleanGenerator.java | 0 .../hegel/generators/CompositeGenerator.java | 0 .../hegel/generators/ConstantGenerator.java | 0 .../dev/hegel/generators/DateGenerator.java | 0 .../hegel/generators/DateTimeGenerator.java | 0 .../java/dev/hegel/generators/Deferred.java | 0 .../java/dev/hegel/generators/Derive.java | 0 .../dev/hegel/generators/DomainGenerator.java | 0 .../dev/hegel/generators/DoubleGenerator.java | 0 .../hegel/generators/DurationGenerator.java | 0 .../dev/hegel/generators/EmailGenerator.java | 0 .../hegel/generators/FilteredGenerator.java | 0 .../hegel/generators/FlatMappedGenerator.java | 0 .../dev/hegel/generators/FloatGenerator.java | 0 .../java/dev/hegel/generators/Floats.java | 0 .../dev/hegel/generators/HandleCache.java | 0 .../hegel/generators/IntegerGenerator.java | 0 .../hegel/generators/IpAddressGenerator.java | 0 .../dev/hegel/generators/ListGenerator.java | 0 .../dev/hegel/generators/LongGenerator.java | 0 .../dev/hegel/generators/MapGenerator.java | 0 .../dev/hegel/generators/MappedGenerator.java | 0 .../dev/hegel/generators/OneOfGenerator.java | 0 .../dev/hegel/generators/RecordGenerator.java | 0 .../dev/hegel/generators/RegexGenerator.java | 0 .../generators/SampledFromGenerator.java | 0 .../dev/hegel/generators/SetGenerator.java | 0 .../main/java/dev/hegel/generators/Sizes.java | 0 .../dev/hegel/generators/TextGenerator.java | 0 .../dev/hegel/generators/TimeGenerator.java | 0 .../dev/hegel/generators/TupleGenerator.java | 0 .../dev/hegel/generators/UrlGenerator.java | 0 .../dev/hegel/generators/UuidGenerator.java | 0 .../hegel/generators/ZoneOffsetGenerator.java | 0 .../main/java/dev/hegel/package-info.java | 0 .../java/dev/hegel/BindingErrorPathsTest.java | 0 .../test/java/dev/hegel/ConformanceTest.java | 14 +- .../test/java/dev/hegel/CoverageTest.java | 26 +- .../test/java/dev/hegel/DerivationTest.java | 0 .../test/java/dev/hegel/EndToEndTest.java | 0 .../test/java/dev/hegel/EngineEdgeTest.java | 0 .../src}/test/java/dev/hegel/EngineTest.java | 0 .../test/java/dev/hegel/FakeLibhegel.java | 119 ++- .../java/dev/hegel/FloatVsDoubleTest.java | 0 .../java/dev/hegel/FluentBuilderTest.java | 0 .../java/dev/hegel/GenerationQualityTest.java | 0 .../java/dev/hegel/GeneratorSmokeTest.java | 0 .../dev/hegel/GeneratorValidationTest.java | 0 .../dev/hegel/HegelTestAnnotationTest.java | 0 .../java/dev/hegel/LibraryLoaderTest.java | 0 .../src}/test/java/dev/hegel/OutputTest.java | 0 .../java/dev/hegel/ReproduceFailureTest.java | 0 .../src}/test/java/dev/hegel/RunnerTest.java | 5 - .../dev/hegel/SettingsHealthDatabaseTest.java | 0 .../java/dev/hegel/ShrinkQualityTest.java | 0 .../src}/test/java/dev/hegel/SmokeTest.java | 0 .../test/java/dev/hegel/StatefulTest.java | 0 .../test/java/dev/hegel/TestCaseTest.java | 0 .../src}/test/java/dev/hegel/TupleTest.java | 0 .../src}/test/java/dev/hegel/Utils.java | 0 .../java/dev/hegel/UuidGeneratorTest.java | 0 122 files changed, 1931 insertions(+), 559 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 RELEASE.md create mode 100644 hegel-jna/pom.xml create mode 100644 hegel-jna/src/main/java/dev/hegel/JnaLibhegel.java create mode 100644 hegel-jna/src/main/java/dev/hegel/LibhegelBackend.java create mode 100644 hegel-jna/src/test/java/dev/hegel/JnaLibhegelTest.java create mode 100644 hegel/pom.xml rename {src => hegel/src}/it/module-consumer/invoker.properties (100%) rename {src => hegel/src}/it/module-consumer/pom.xml (100%) rename {src => hegel/src}/it/module-consumer/src/main/java/module-info.java (100%) rename {src => hegel/src}/it/module-consumer/src/test/java/dev/hegel/consumer/ConsumerTest.java (100%) create mode 100644 hegel/src/main/java/dev/hegel/LibhegelBackend.java rename {src => hegel/src}/main/java/dev/hegel/RealLibhegel.java (83%) create mode 100644 hegel/src/test/java/dev/hegel/FfmCoverageTest.java rename {src => hegel/src}/test/java/dev/hegel/RealLibhegelTest.java (81%) rename {src => shared/src}/main/java-templates/dev/hegel/BuildInfo.java (100%) rename {src => shared/src}/main/java/dev/hegel/Abi.java (100%) rename {src => shared/src}/main/java/dev/hegel/AssumeRejected.java (100%) rename {src => shared/src}/main/java/dev/hegel/Backend.java (100%) rename {src => shared/src}/main/java/dev/hegel/DataSource.java (100%) rename {src => shared/src}/main/java/dev/hegel/Database.java (100%) rename {src => shared/src}/main/java/dev/hegel/Engine.java (96%) rename {src => shared/src}/main/java/dev/hegel/Generated.java (100%) rename {src => shared/src}/main/java/dev/hegel/Generator.java (100%) rename {src => shared/src}/main/java/dev/hegel/Generators.java (100%) rename {src => shared/src}/main/java/dev/hegel/HealthCheck.java (100%) rename {src => shared/src}/main/java/dev/hegel/HealthCheckFailure.java (100%) rename {src => shared/src}/main/java/dev/hegel/Hegel.java (100%) rename {src => shared/src}/main/java/dev/hegel/HegelException.java (100%) rename {src => shared/src}/main/java/dev/hegel/HegelTest.java (100%) rename {src => shared/src}/main/java/dev/hegel/HegelTestExtension.java (100%) rename {src => shared/src}/main/java/dev/hegel/Invariant.java (100%) rename {src => shared/src}/main/java/dev/hegel/Libhegel.java (53%) rename {src => shared/src}/main/java/dev/hegel/LibraryLoader.java (100%) rename {src => shared/src}/main/java/dev/hegel/LiveDataSource.java (94%) rename {src => shared/src}/main/java/dev/hegel/Mode.java (100%) rename {src => shared/src}/main/java/dev/hegel/OptBoolean.java (100%) rename {src => shared/src}/main/java/dev/hegel/Phase.java (100%) rename {src => shared/src}/main/java/dev/hegel/Pool.java (100%) rename {src => shared/src}/main/java/dev/hegel/Rule.java (100%) rename {src => shared/src}/main/java/dev/hegel/Runner.java (89%) rename {src => shared/src}/main/java/dev/hegel/Settings.java (100%) rename {src => shared/src}/main/java/dev/hegel/Stateful.java (100%) rename {src => shared/src}/main/java/dev/hegel/StopTest.java (100%) rename {src => shared/src}/main/java/dev/hegel/StringGeneratorHandle.java (75%) rename {src => shared/src}/main/java/dev/hegel/TestCase.java (100%) rename {src => shared/src}/main/java/dev/hegel/Tuple2.java (100%) rename {src => shared/src}/main/java/dev/hegel/Tuple3.java (100%) rename {src => shared/src}/main/java/dev/hegel/Tuple4.java (100%) rename {src => shared/src}/main/java/dev/hegel/Tuple5.java (100%) rename {src => shared/src}/main/java/dev/hegel/Tuple6.java (100%) rename {src => shared/src}/main/java/dev/hegel/Tuple7.java (100%) rename {src => shared/src}/main/java/dev/hegel/Tuple8.java (100%) rename {src => shared/src}/main/java/dev/hegel/Verbosity.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/BinaryGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/BooleanGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/CompositeGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/ConstantGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/DateGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/DateTimeGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/Deferred.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/Derive.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/DomainGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/DoubleGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/DurationGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/EmailGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/FilteredGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/FlatMappedGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/FloatGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/Floats.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/HandleCache.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/IntegerGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/IpAddressGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/ListGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/LongGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/MapGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/MappedGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/OneOfGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/RecordGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/RegexGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/SampledFromGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/SetGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/Sizes.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/TextGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/TimeGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/TupleGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/UrlGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/UuidGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/generators/ZoneOffsetGenerator.java (100%) rename {src => shared/src}/main/java/dev/hegel/package-info.java (100%) rename {src => shared/src}/test/java/dev/hegel/BindingErrorPathsTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/ConformanceTest.java (95%) rename {src => shared/src}/test/java/dev/hegel/CoverageTest.java (92%) rename {src => shared/src}/test/java/dev/hegel/DerivationTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/EndToEndTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/EngineEdgeTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/EngineTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/FakeLibhegel.java (72%) rename {src => shared/src}/test/java/dev/hegel/FloatVsDoubleTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/FluentBuilderTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/GenerationQualityTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/GeneratorSmokeTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/GeneratorValidationTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/HegelTestAnnotationTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/LibraryLoaderTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/OutputTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/ReproduceFailureTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/RunnerTest.java (99%) rename {src => shared/src}/test/java/dev/hegel/SettingsHealthDatabaseTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/ShrinkQualityTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/SmokeTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/StatefulTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/TestCaseTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/TupleTest.java (100%) rename {src => shared/src}/test/java/dev/hegel/Utils.java (100%) rename {src => shared/src}/test/java/dev/hegel/UuidGeneratorTest.java (100%) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index c0fb802..9d85ab3 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,17 +1,40 @@ # hegel-java — developer notes -Property-based testing for Java, binding the native Hegel engine (`libhegel`) in-process over the -Java Foreign Function & Memory API (FFM, `java.lang.foreign`). No JNI, no cgo-equivalent. +Property-based testing for Java, binding the native Hegel engine (`libhegel`) in-process. Two +published artifacts share one API and one source tree: `dev.hegel:hegel` binds over the Java +Foreign Function & Memory API (FFM, `java.lang.foreign`, Java 22+) and `dev.hegel:hegel-jna` binds +over JNA (Java 17+). Each jar is self-contained (users depend on exactly one); there is no shared +`hegel-core` artifact. This implementation was generated by Claude following the `hegel-implementation-guide` skills. The authoritative references are hegel-rust (the engine and canonical client) and its C ABI header `hegel-c/include/hegel.h`. +## Layout + +Multi-module Maven build (`dev.hegel:hegel-parent`, not itself useful to consumers): + +- `shared/` — **not a Maven module**: the backend-neutral source tree (`src/main/java`, + `src/main/java-templates`, `src/test/java`) compiled into *both* jars via + build-helper-maven-plugin `add-source`/`add-test-source`. Everything except the concrete + binding lives here, including `FakeLibhegel` and the whole behaviour suite, which therefore + runs against both backends. +- `hegel/` — the FFM artifact: `RealLibhegel` + `LibhegelBackend` (main), `RealLibhegelTest` + + `FfmCoverageTest` (test), and the JPMS `module-consumer` invoker IT under `src/it`. +- `hegel-jna/` — the JNA artifact: `JnaLibhegel` + `LibhegelBackend` (main), `JnaLibhegelTest`. + +Each module defines the same package-private `dev.hegel.LibhegelBackend.open(Path)`; the shared +`Engine` calls it without knowing which backend it got. IDE caveat: IntelliJ attaches a shared +source root to only one module at a time (duplicate content roots are unsupported); the Maven CLI +is authoritative. + ## Build & test - `just coverage` / `mvn verify` — runs all tests and enforces **100% instruction + branch - coverage** (JaCoCo). This is a hard gate. -- `just test` / `mvn test` — full suite, no coverage gate. + coverage** (JaCoCo, per module). This is a hard gate. +- `just test` / `mvn test` — full suite, no coverage gate. Building the full reactor needs JDK + 22+ (the FFM module); `just test-jna` / `just coverage-jna` build only parent + `hegel-jna` + and work on JDK 17+ (CI runs them on 17 and 21). - `mvn test -Dtest=RunnerTest` / `-Dtest=RunnerTest#happyPathMarksValidAndFreesEverything` — one class / one method. `-Dtest='*Conformance*,*Behaviour*'` is what `just conformance` runs. - `just conformance` — the behaviour suite against the real engine. @@ -19,10 +42,13 @@ authoritative references are hegel-rust (the engine and canonical client) and it - `just format` / `just lint` — palantir-java-format via spotless-maven-plugin. - `just check` — full CI gate: lint + coverage + docs. -Java 22 (`maven.compiler.release`) for the FFM API; tests run with -`--enable-native-access=ALL-UNNAMED` (the `hegel.argLine` property). The pinned engine version -(`` in `pom.xml`) is filtered into `BuildInfo.ENGINE_VERSION` from -`src/main/java-templates/dev/hegel/BuildInfo.java` (maven templating-plugin → generated-sources); +`maven.compiler.release` is 22 for the `hegel` module (FFM) and 17 for `hegel-jna` and the shared +tree; keep shared code Java-17-clean. Both modules' tests run with +`--enable-native-access=ALL-UNNAMED` (the `hegel.argLine` property — FFM needs it on 22+, JNA on +24+ under JEP 472; the flag is accepted on every supported JDK). The +pinned engine version (`` in the parent `pom.xml`) is filtered into +`BuildInfo.ENGINE_VERSION` from `shared/src/main/java-templates/dev/hegel/BuildInfo.java` +(maven-resources-plugin filtering → generated-sources, added to the compile path by build-helper); bump that property to ship a new engine. A user-supplied `$HEGEL_LIBHEGEL_PATH` of a different version triggers a warning against `BuildInfo.ENGINE_VERSION`. @@ -53,14 +79,19 @@ subpackage (one class per generator: `IntegerGenerator`, `TextGenerator`, `ListG `RegexGenerator`, `EmailGenerator`, `DateTimeGenerator`, `Derive`/`RecordGenerator`, etc.). The public `Generator`/`TestCase`/`Generators`/`Hegel`/`Stateful` surface stays in `dev.hegel`. -- **FFI binding** — `Libhegel` (a fakeable interface) and `RealLibhegel` (FFM). `LibraryLoader` - resolves the library (override / OS library path / jar-bundled native unpacked to a cache); - `Engine` is the process-wide lazy holder of the loaded shared object (with a test hook) — it - caches only the immutable, thread-safe library, never per-test state. `RealLibhegel` keeps one - libhegel error context per thread (read back by `lastErrorMessage()`), passes date/time/datetime - structs by value, copies engine-allocated string/bytes buffers out and frees them, and bridges - the per-run output callback (`hegel_output_callback_t`) to a `Consumer` through an FFM - upcall stub whose arena lives until `runFree`. `Abi` holds the C constants. +- **FFI binding** — `Libhegel` (a fakeable, backend-neutral interface: opaque handles cross as + raw `long` addresses, 0 = NULL, so it never mentions any FFI library's types) with one + implementation per artifact: `RealLibhegel` (FFM, in `hegel/`) and `JnaLibhegel` (JNA, in + `hegel-jna/`), each selected by its module's `LibhegelBackend`. `LibraryLoader` resolves the + library (override / OS library path / jar-bundled native unpacked to a cache); `Engine` is the + process-wide lazy holder of the loaded shared object (with a test hook) — it caches only the + immutable, thread-safe library, never per-test state. Both bindings keep one libhegel error + context per thread (read back by `lastErrorMessage()`), pass date/time/datetime structs by + value, copy engine-allocated string/bytes buffers out and free them, and bridge the per-run + output callback (`hegel_output_callback_t`) to a `Consumer` — an FFM upcall stub whose + arena lives until `runFree`, or a JNA `Callback` strongly referenced until `runFree`. In the + JNA binding, C `bool` crosses as `byte` (JNA's default boolean mapping is a 32-bit int) and + `size_t` as `long` (the bundled natives are all 64-bit). `Abi` holds the C constants. - **Per-case primitives** — `DataSource` is the abstraction generators draw against; `LiveDataSource` wraps the engine, translating return codes (`StopTest` → OVERRUN, `AssumeRejected` → INVALID, `INVALID_ARG` → `IllegalArgumentException` with the engine's @@ -97,7 +128,9 @@ public `Generator`/`TestCase`/`Generators`/`Hegel`/`Stateful` surface stays in ` ## Coverage notes -A few genuinely-unreachable defensive blocks are excluded via `@Generated` (JaCoCo ignores +The 100% gate applies per module, so each backend jar carries its own binding-edge tests +(`RealLibhegelTest` + `FfmCoverageTest` for FFM, `JnaLibhegelTest` for JNA) on top of the shared +suites. A few genuinely-unreachable defensive blocks are excluded via `@Generated` (JaCoCo ignores `*Generated*`-named annotations): the `NoSuchAlgorithmException` for SHA-256, the lookup of the output-callback bridge method, and the `IllegalAccessException` after `setAccessible(true)` succeeded in `Stateful`. Everything else is covered by real-engine integration tests plus diff --git a/.github/scripts/release.py b/.github/scripts/release.py index 9fe722b..dd9c131 100644 --- a/.github/scripts/release.py +++ b/.github/scripts/release.py @@ -27,6 +27,8 @@ ROOT = Path(__file__).resolve().parent.parent.parent POM = ROOT / "pom.xml" +MODULE_POMS = [ROOT / "hegel" / "pom.xml", ROOT / "hegel-jna" / "pom.xml"] +PUBLISHED_ARTIFACTS = ["hegel", "hegel-jna"] # Printed by central-publishing-maven-plugin once the bundle is on the portal. From that point # the deployment validates and publishes server-side (autoPublish) no matter how the mvn @@ -45,10 +47,13 @@ def git(*args: str) -> None: def is_source_file(path: str) -> bool: - # A PR that changes published source (src/main/**) or the pom must carry a RELEASE.md; - # test-only and tooling changes don't. Mirrors the per-library source definition the other - # Hegel libraries use (which likewise exclude their test trees). - return path.startswith("src/main/") or path == "pom.xml" + # A PR that changes published source (any module's src/main/**) or a build pom must carry a + # RELEASE.md; test-only and tooling changes don't. Mirrors the per-library source definition + # the other Hegel libraries use (which likewise exclude their test trees). + return ( + path.startswith(("shared/src/main/", "hegel/src/main/", "hegel-jna/src/main/")) + or path in ("pom.xml", "hegel/pom.xml", "hegel-jna/pom.xml") + ) def parse_release_file(path: Path) -> tuple[str, str]: @@ -81,9 +86,9 @@ def bump_version(current: str, release_type: str) -> str: def pom_version() -> str: - """The last released version, read from the pom's project (the first - element; modelVersion uses a different tag). Seeded at ``0.0.0`` before the first release, so - the bootstrap ``RELEASE_TYPE: minor`` lands at ``0.1.0``.""" + """The last released version, read from the parent pom's project (the first + element; modelVersion uses a different tag). Seeded at ``0.0.0`` before the first + release, so the bootstrap ``RELEASE_TYPE: minor`` lands at ``0.1.0``.""" match = re.search(r"([^<]+)", POM.read_text()) if match is None: raise ValueError("could not find in pom.xml") @@ -91,8 +96,12 @@ def pom_version() -> str: def set_pom_version(new_version: str) -> None: - text = POM.read_text() - POM.write_text(re.sub(r"[^<]+", f"{new_version}", text, count=1)) + # The first is the project version in the parent pom and the reference in + # each module pom (modelVersion uses a different tag, and modules declare no version of + # their own). + for pom in [POM, *MODULE_POMS]: + text = pom.read_text() + pom.write_text(re.sub(r"[^<]+", f"{new_version}", text, count=1)) def add_changelog(path: Path, *, version: str, content: str) -> None: @@ -123,18 +132,24 @@ def run_deploy(mvn_args: list[str]) -> tuple[int, bool]: def is_published(version: str) -> bool: - """Ask the Central Publisher API whether dev.hegel:hegel:{version} is live on Maven Central. - Network and server errors count as "not (yet) published", so a transiently failing status - endpoint — the very thing being recovered from — just means polling again.""" + """Ask the Central Publisher API whether every published artifact (dev.hegel:hegel and + dev.hegel:hegel-jna) at {version} is live on Maven Central. Network and server errors count + as "not (yet) published", so a transiently failing status endpoint — the very thing being + recovered from — just means polling again.""" credentials = f"{os.environ['CENTRAL_TOKEN_USER']}:{os.environ['CENTRAL_TOKEN_PASS']}" token = base64.b64encode(credentials.encode()).decode() - query = urllib.parse.urlencode({"namespace": "dev.hegel", "name": "hegel", "version": version}) - request = urllib.request.Request(f"{CENTRAL_PUBLISHED_URL}?{query}", headers={"Authorization": f"Bearer {token}"}) - try: - with urllib.request.urlopen(request, timeout=30) as response: - return bool(json.load(response).get("published")) - except (urllib.error.URLError, TimeoutError, ValueError): - return False + for name in PUBLISHED_ARTIFACTS: + query = urllib.parse.urlencode({"namespace": "dev.hegel", "name": name, "version": version}) + request = urllib.request.Request( + f"{CENTRAL_PUBLISHED_URL}?{query}", headers={"Authorization": f"Bearer {token}"} + ) + try: + with urllib.request.urlopen(request, timeout=30) as response: + if not json.load(response).get("published"): + return False + except (urllib.error.URLError, TimeoutError, ValueError): + return False + return True def deploy_and_verify(mvn_args: list[str], version: str) -> None: @@ -246,7 +261,7 @@ def release() -> None: git("config", "user.name", f"{app_slug}[bot]") git("config", "user.email", f"{bot_user_id}+{app_slug}[bot]@users.noreply.github.com") - git("add", "pom.xml", "CHANGELOG.md") + git("add", "pom.xml", "hegel/pom.xml", "hegel-jna/pom.xml", "CHANGELOG.md") git("rm", "RELEASE.md") git("commit", "-m", f"Bump to version {new_version} and update changelog\n\n[skip ci]") git("tag", f"v{new_version}") diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78a8e61..fdf2a0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,36 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: just coverage + test-jna: + name: "test hegel-jna (java ${{ matrix.java-version }})" + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + java-version: ["17", "21"] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: ${{ matrix.java-version }} + cache: maven + + - uses: ./.github/actions/install-tools + with: + tools: just + + - name: Test hegel-jna with 100% coverage enforcement + env: + # Raises the GitHub API rate limit for fetch_natives.py asset discovery. + GITHUB_TOKEN: ${{ github.token }} + run: just coverage-jna + test-os: name: "test (${{ matrix.os }})" runs-on: ${{ matrix.os }} @@ -129,7 +159,7 @@ jobs: release: name: release if: github.event_name == 'push' && github.repository == 'hegeldev/hegel-java' - needs: [lint, test, test-os, docs] + needs: [lint, test, test-jna, test-os, docs] runs-on: ubuntu-latest permissions: contents: write diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/README.md b/README.md index b0354c6..acda0dc 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,21 @@ Instead of writing tests with hand-picked example inputs, you describe a *proper ## Installation +Hegel for Java ships as two interchangeable artifacts with the same API — pick the one that +matches your JVM: + +- **`dev.hegel:hegel`** — requires **Java 22+**; binds the engine over the + [Foreign Function & Memory API](https://docs.oracle.com/en/java/javase/22/core/foreign-function-and-memory-api.html) + with no extra dependencies. +- **`dev.hegel:hegel-jna`** — requires **Java 17+**; binds the engine over + [JNA](https://github.com/java-native-access/jna). + Add the dependency with Maven: ```xml dev.hegel - hegel + hegel 0.1.0 test @@ -30,12 +39,17 @@ Add the dependency with Maven: or with Gradle: ```kotlin -testImplementation("dev.hegel:hegel:0.1.0") +testImplementation("dev.hegel:hegel:0.1.0") // or "dev.hegel:hegel-jna:0.1.0" ``` -Hegel for Java requires **Java 22+** and uses the [Foreign Function & Memory API](https://docs.oracle.com/en/java/javase/22/core/foreign-function-and-memory-api.html). The native engine is bundled in the jar for Linux (x86-64 and arm64), macOS (Apple Silicon), and Windows (x86-64 and arm64). +Depend on exactly one of the two — they contain the same classes and differ only in how they call +the native engine. The engine is bundled in both jars for Linux (x86-64 and arm64), macOS (Apple +Silicon), and Windows (x86-64 and arm64). -Because Hegel calls native code, pass `--enable-native-access=ALL-UNNAMED` to silence the JVM's native-access warning. With Maven Surefire: +Because Hegel calls native code, pass `--enable-native-access=ALL-UNNAMED` to silence the JVM's +native-access warning — printed by JDK 22+ for `hegel` (FFM) and by JDK 24+ for `hegel-jna` (JNA, +under [JEP 472](https://openjdk.org/jeps/472)). The flag is accepted on every supported JDK +(17+), so it is safe to set unconditionally. With Maven Surefire: ```xml --enable-native-access=ALL-UNNAMED diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..38a3f6f --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,22 @@ +RELEASE_TYPE: patch + +This patch adds a second published artifact, `dev.hegel:hegel-jna`, which binds the native engine +over [JNA](https://github.com/java-native-access/jna) and runs on Java 17+. The existing +`dev.hegel:hegel` artifact is unchanged: it binds over the Foreign Function and Memory API and +requires Java 22+. + +Both artifacts expose the identical `dev.hegel` API and behave the same, so tests written against +one run unchanged against the other. Depend on exactly one of them — `hegel` on Java 22+, or +`hegel-jna` on older JVMs: + +```xml + + dev.hegel + hegel-jna + 0.5.1 + +``` + +`hegel-jna` pulls in `net.java.dev.jna:jna` as its only dependency. On JDK 24+ pass +`--enable-native-access=ALL-UNNAMED` to silence the JVM's native-access warning (the flag is +accepted on every supported JDK). diff --git a/hegel-jna/pom.xml b/hegel-jna/pom.xml new file mode 100644 index 0000000..c0d29dc --- /dev/null +++ b/hegel-jna/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + + dev.hegel + hegel-parent + 0.5.0 + + + hegel-jna + jar + + hegel-java (JNA) + Property-based testing for Java, built on Hypothesis (JNA binding, Java 17+) + + + 17 + + + + + net.java.dev.jna + jna + 5.17.0 + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + dev.hegel.jna + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + org.codehaus.mojo + exec-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + com.diffplug.spotless + spotless-maven-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + org.jacoco + jacoco-maven-plugin + + + + diff --git a/hegel-jna/src/main/java/dev/hegel/JnaLibhegel.java b/hegel-jna/src/main/java/dev/hegel/JnaLibhegel.java new file mode 100644 index 0000000..dcab7cd --- /dev/null +++ b/hegel-jna/src/main/java/dev/hegel/JnaLibhegel.java @@ -0,0 +1,872 @@ +package dev.hegel; + +import com.sun.jna.Callback; +import com.sun.jna.Library; +import com.sun.jna.Memory; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.StringArray; +import com.sun.jna.Structure; +import com.sun.jna.ptr.ByteByReference; +import com.sun.jna.ptr.DoubleByReference; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.ptr.LongByReference; +import com.sun.jna.ptr.PointerByReference; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +/** + * The real libhegel binding, driving the C ABI over JNA (Java Native Access), for JVMs without the + * Foreign Function and Memory API (Java 17+; the {@code hegel} artifact binds via FFM on 22+). + * + *

Every fallible call takes a {@code hegel_context_t*} as its first argument, on which libhegel + * records the diagnostic of a failed call. A context must not be shared between threads, so each + * thread lazily creates its own; {@link #lastErrorMessage()} reads the current thread's context, + * which is what the failing call just wrote. Contexts are never freed — one small allocation per + * thread that touches the engine, for the life of the process. + */ +final class JnaLibhegel implements Libhegel { + // hegel_run_start registers the output callback with the engine until hegel_run_free, so a + // strong reference is held per run (JNA frees the native thunk when the Callback is collected). + private final Map runCallbacks = new ConcurrentHashMap<>(); + + private final HegelNative lib; + private final ThreadLocal context; + + JnaLibhegel(Path libraryPath) { + try { + this.lib = Native.load( + libraryPath.toString(), HegelNative.class, Map.of(Library.OPTION_STRING_ENCODING, "UTF-8")); + } catch (UnsatisfiedLinkError e) { + throw new HegelException("Failed to open libhegel at " + libraryPath + ": " + e.getMessage()); + } + this.context = ThreadLocal.withInitial(lib::hegel_context_new); + } + + interface HegelNative extends Library { + Pointer hegel_context_new(); + + Pointer hegel_context_last_error(Pointer ctx); + + int hegel_settings_new(Pointer ctx, PointerByReference out); + + int hegel_settings_free(Pointer ctx, Pointer s); + + int hegel_settings_set_mode(Pointer ctx, Pointer s, int mode); + + int hegel_settings_set_backend(Pointer ctx, Pointer s, int backend); + + int hegel_settings_set_test_cases(Pointer ctx, Pointer s, long n); + + int hegel_settings_set_verbosity(Pointer ctx, Pointer s, int v); + + int hegel_settings_set_seed(Pointer ctx, Pointer s, long seed, byte hasSeed); + + int hegel_settings_set_derandomize(Pointer ctx, Pointer s, byte derandomize); + + int hegel_settings_set_report_multiple_failures(Pointer ctx, Pointer s, byte yes); + + int hegel_settings_set_database(Pointer ctx, Pointer s, String path); + + int hegel_settings_set_database_key(Pointer ctx, Pointer s, String key); + + int hegel_settings_set_phases(Pointer ctx, Pointer s, int mask); + + int hegel_settings_set_suppress_health_check(Pointer ctx, Pointer s, int mask); + + int hegel_run_start( + Pointer ctx, Pointer settings, LineCallback callback, Pointer userData, PointerByReference out); + + int hegel_next_test_case(Pointer ctx, Pointer run, PointerByReference out); + + int hegel_run_result(Pointer ctx, Pointer run, PointerByReference out); + + int hegel_run_result_free(Pointer ctx, Pointer result); + + int hegel_run_free(Pointer ctx, Pointer run); + + int hegel_test_case_from_blob( + Pointer ctx, + Pointer settings, + String blob, + LineCallback callback, + Pointer userData, + PointerByReference out); + + int hegel_test_case_free(Pointer ctx, Pointer tc); + + int hegel_generate_boolean(Pointer ctx, Pointer tc, double p, byte hasForced, byte forced, ByteByReference out); + + int hegel_generate_integer(Pointer ctx, Pointer tc, long min, long max, LongByReference out); + + int hegel_generate_float( + Pointer ctx, + Pointer tc, + int width, + double min, + double max, + byte allowNan, + byte allowInfinity, + byte excludeMin, + byte excludeMax, + double smallestNonzeroMagnitude, + DoubleByReference out); + + int hegel_generate_bytes(Pointer ctx, Pointer tc, long minSize, long maxSize, BufferResult out); + + int hegel_generate_bytes_result_free(Pointer ctx, BufferResult result); + + int hegel_generate_string(Pointer ctx, Pointer tc, Pointer generator, BufferResult out); + + int hegel_generate_string_result_free(Pointer ctx, BufferResult result); + + int hegel_generate_date(Pointer ctx, Pointer tc, HegelDate.ByValue min, HegelDate.ByValue max, HegelDate out); + + int hegel_generate_time(Pointer ctx, Pointer tc, HegelTime.ByValue min, HegelTime.ByValue max, HegelTime out); + + int hegel_generate_datetime( + Pointer ctx, Pointer tc, HegelDatetime.ByValue min, HegelDatetime.ByValue max, HegelDatetime out); + + int hegel_generate_uuid(Pointer ctx, Pointer tc, byte version, byte hasVersion, Pointer out); + + int hegel_generate_ipv4(Pointer ctx, Pointer tc, Pointer out); + + int hegel_generate_ipv6(Pointer ctx, Pointer tc, Pointer out); + + int hegel_string_generator_text( + Pointer ctx, + long minSize, + long maxSize, + String codec, + int minCodepoint, + int maxCodepoint, + Pointer categories, + long categoriesLen, + Pointer excludeCategories, + long excludeCategoriesLen, + Pointer includeCharacters, + long includeCharactersLen, + Pointer excludeCharacters, + long excludeCharactersLen, + PointerByReference out); + + int hegel_string_generator_regex( + Pointer ctx, String pattern, byte fullmatch, Pointer alphabet, PointerByReference out); + + int hegel_string_generator_email(Pointer ctx, PointerByReference out); + + int hegel_string_generator_url(Pointer ctx, PointerByReference out); + + int hegel_string_generator_domain(Pointer ctx, long maxLength, PointerByReference out); + + int hegel_string_generator_free(Pointer ctx, Pointer generator); + + int hegel_start_span(Pointer ctx, Pointer tc, long label); + + int hegel_stop_span(Pointer ctx, Pointer tc, byte discard); + + int hegel_new_collection(Pointer ctx, Pointer tc, long minSize, long maxSize, LongByReference out); + + int hegel_collection_more(Pointer ctx, Pointer tc, long id, ByteByReference out); + + int hegel_collection_reject(Pointer ctx, Pointer tc, long id, String why); + + int hegel_new_pool(Pointer ctx, Pointer tc, LongByReference out); + + int hegel_pool_add(Pointer ctx, Pointer tc, long poolId, LongByReference out); + + int hegel_pool_generate(Pointer ctx, Pointer tc, long poolId, byte consume, LongByReference out); + + int hegel_new_state_machine( + Pointer ctx, + Pointer tc, + Pointer ruleNames, + long ruleNamesLen, + Pointer invariantNames, + long invariantNamesLen, + LongByReference out); + + int hegel_state_machine_next_rule(Pointer ctx, Pointer tc, long stateMachineId, LongByReference out); + + int hegel_target(Pointer ctx, Pointer tc, double value, String label); + + int hegel_mark_complete(Pointer ctx, Pointer tc, int status, String origin); + + int hegel_run_result_status(Pointer ctx, Pointer result, IntByReference out); + + int hegel_run_result_error(Pointer ctx, Pointer result, PointerByReference out); + + int hegel_run_result_failure_count(Pointer ctx, Pointer result, LongByReference out); + + int hegel_run_result_failure(Pointer ctx, Pointer result, long index, PointerByReference out); + + int hegel_failure_free(Pointer ctx, Pointer failure); + + int hegel_failure_reproduction_blob(Pointer ctx, Pointer failure, PointerByReference out); + + int hegel_version(Pointer ctx, PointerByReference out); + } + + /** {@code struct hegel_date_t { int32_t year; uint8_t month; uint8_t day; }} */ + @Structure.FieldOrder({"year", "month", "day"}) + public static class HegelDate extends Structure { + public int year; + public byte month; + public byte day; + + public static class ByValue extends HegelDate implements Structure.ByValue {} + } + + /** {@code struct hegel_time_t { uint8_t hour; uint8_t minute; uint8_t second; uint32_t microsecond; }} */ + @Structure.FieldOrder({"hour", "minute", "second", "microsecond"}) + public static class HegelTime extends Structure { + public byte hour; + public byte minute; + public byte second; + public int microsecond; + + public static class ByValue extends HegelTime implements Structure.ByValue {} + } + + /** {@code struct hegel_datetime_t { hegel_date_t date; hegel_time_t time; }} */ + @Structure.FieldOrder({"date", "time"}) + public static class HegelDatetime extends Structure { + public HegelDate date; + public HegelTime time; + + public static class ByValue extends HegelDatetime implements Structure.ByValue {} + } + + /** {@code struct hegel_generate_bytes_result_t / hegel_generate_string_result_t { T* data; size_t len; }} */ + @Structure.FieldOrder({"data", "len"}) + public static class BufferResult extends Structure { + public Pointer data; + public long len; + } + + interface OutputCallback extends Callback { + void invoke(Pointer userData, Pointer line, long len); + } + + /** + * Bridges engine output lines to the run's {@link Consumer}. A named class rather than a + * lambda so JNA can reliably resolve the callback method reflectively. + */ + static final class LineCallback implements OutputCallback { + private final Consumer output; + + LineCallback(Consumer output) { + this.output = output; + } + + @Override + public void invoke(Pointer userData, Pointer line, long len) { + emitLine(output, line, len); + } + } + + /** + * Bridge one line of engine output to the run's {@link Consumer}. An exception escaping a + * native callback must never unwind into the engine, so every throwable is swallowed here. + */ + static void emitLine(Consumer output, Pointer line, long len) { + try { + byte[] bytes = line.getByteArray(0, (int) len); + output.accept(new String(bytes, StandardCharsets.UTF_8)); + } catch (Throwable t) { + // Deliberately dropped: output delivery must never unwind into the engine. + } + } + + private Pointer ctx() { + return context.get(); + } + + private static Pointer pointer(long handle) { + return handle == 0 ? null : new Pointer(handle); + } + + private static long address(Pointer p) { + return Pointer.nativeValue(p); + } + + private static byte cbool(boolean value) { + return (byte) (value ? 1 : 0); + } + + private void check(String op, int code) { + if (code != Abi.OK) { + throw new HegelException( + op + " failed (rc=" + code + "): " + java.util.Objects.toString(lastErrorMessage(), "")); + } + } + + static String readCString(Pointer ptr) { + return ptr == null ? null : ptr.getString(0, "UTF-8"); + } + + // --- settings --- + + @Override + public long settingsNew() { + PointerByReference out = new PointerByReference(); + check("hegel_settings_new", lib.hegel_settings_new(ctx(), out)); + return address(out.getValue()); + } + + @Override + public void settingsFree(long s) { + check("hegel_settings_free", lib.hegel_settings_free(ctx(), pointer(s))); + } + + @Override + public void settingsMode(long s, int mode) { + check("hegel_settings_set_mode", lib.hegel_settings_set_mode(ctx(), pointer(s), mode)); + } + + @Override + public void settingsBackend(long s, int backend) { + check("hegel_settings_set_backend", lib.hegel_settings_set_backend(ctx(), pointer(s), backend)); + } + + @Override + public void settingsTestCases(long s, long n) { + check("hegel_settings_set_test_cases", lib.hegel_settings_set_test_cases(ctx(), pointer(s), n)); + } + + @Override + public void settingsVerbosity(long s, int v) { + check("hegel_settings_set_verbosity", lib.hegel_settings_set_verbosity(ctx(), pointer(s), v)); + } + + @Override + public void settingsSeed(long s, long seed, boolean hasSeed) { + check("hegel_settings_set_seed", lib.hegel_settings_set_seed(ctx(), pointer(s), seed, cbool(hasSeed))); + } + + @Override + public void settingsDerandomize(long s, boolean derandomize) { + check( + "hegel_settings_set_derandomize", + lib.hegel_settings_set_derandomize(ctx(), pointer(s), cbool(derandomize))); + } + + @Override + public void settingsReportMultipleFailures(long s, boolean yes) { + check( + "hegel_settings_set_report_multiple_failures", + lib.hegel_settings_set_report_multiple_failures(ctx(), pointer(s), cbool(yes))); + } + + @Override + public void settingsDatabase(long s, String path) { + check("hegel_settings_set_database", lib.hegel_settings_set_database(ctx(), pointer(s), path)); + } + + @Override + public void settingsDatabaseKey(long s, String key) { + check("hegel_settings_set_database_key", lib.hegel_settings_set_database_key(ctx(), pointer(s), key)); + } + + @Override + public void settingsPhases(long s, int mask) { + check("hegel_settings_set_phases", lib.hegel_settings_set_phases(ctx(), pointer(s), mask)); + } + + @Override + public void settingsSuppressHealthCheck(long s, int mask) { + check( + "hegel_settings_set_suppress_health_check", + lib.hegel_settings_set_suppress_health_check(ctx(), pointer(s), mask)); + } + + // --- run lifecycle --- + + @Override + public long runStart(long settings, Consumer output) { + LineCallback callback = output == null ? null : new LineCallback(output); + PointerByReference out = new PointerByReference(); + int code = lib.hegel_run_start(ctx(), pointer(settings), callback, null, out); + if (code != Abi.OK) { + throw new HegelException( + "hegel_run_start failed (rc=" + code + "): " + java.util.Objects.toString(lastErrorMessage(), "")); + } + long run = address(out.getValue()); + if (callback != null) { + runCallbacks.put(run, callback); + } + return run; + } + + @Override + public long nextTestCase(long run) { + PointerByReference out = new PointerByReference(); + check("hegel_next_test_case", lib.hegel_next_test_case(ctx(), pointer(run), out)); + return address(out.getValue()); + } + + @Override + public long runResult(long run) { + PointerByReference out = new PointerByReference(); + check("hegel_run_result", lib.hegel_run_result(ctx(), pointer(run), out)); + return address(out.getValue()); + } + + @Override + public void runResultFree(long result) { + check("hegel_run_result_free", lib.hegel_run_result_free(ctx(), pointer(result))); + } + + @Override + public void runFree(long run) { + check("hegel_run_free", lib.hegel_run_free(ctx(), pointer(run))); + runCallbacks.remove(run); + } + + @Override + public int testCaseFromBlob(long settings, String blob, Consumer output, long[] out) { + // The blob replay's output is emitted synchronously during this call, so the callback only + // needs to live for its duration. + LineCallback callback = output == null ? null : new LineCallback(output); + PointerByReference outRef = new PointerByReference(); + int code = lib.hegel_test_case_from_blob(ctx(), pointer(settings), blob, callback, null, outRef); + if (code == Abi.OK) { + out[0] = address(outRef.getValue()); + } + return code; + } + + @Override + public void testCaseFree(long tc) { + check("hegel_test_case_free", lib.hegel_test_case_free(ctx(), pointer(tc))); + } + + // --- draws --- + + @Override + public int generateBoolean(long tc, double p, boolean[] out) { + ByteByReference ref = new ByteByReference(); + int code = lib.hegel_generate_boolean(ctx(), pointer(tc), p, cbool(false), cbool(false), ref); + if (code == Abi.OK) { + out[0] = ref.getValue() != 0; + } + return code; + } + + @Override + public int generateInteger(long tc, long min, long max, long[] out) { + LongByReference ref = new LongByReference(); + int code = lib.hegel_generate_integer(ctx(), pointer(tc), min, max, ref); + if (code == Abi.OK) { + out[0] = ref.getValue(); + } + return code; + } + + @Override + public int generateFloat( + long tc, + int width, + double min, + double max, + boolean allowNan, + boolean allowInfinity, + boolean excludeMin, + boolean excludeMax, + double smallestNonzeroMagnitude, + double[] out) { + DoubleByReference ref = new DoubleByReference(); + int code = lib.hegel_generate_float( + ctx(), + pointer(tc), + width, + min, + max, + cbool(allowNan), + cbool(allowInfinity), + cbool(excludeMin), + cbool(excludeMax), + smallestNonzeroMagnitude, + ref); + if (code == Abi.OK) { + out[0] = ref.getValue(); + } + return code; + } + + @Override + public int generateBytes(long tc, long minSize, long maxSize, byte[][] out) { + BufferResult result = new BufferResult(); + int code = lib.hegel_generate_bytes(ctx(), pointer(tc), minSize, maxSize, result); + if (code == Abi.OK) { + out[0] = copyBuffer(result); + check("hegel_generate_bytes_result_free", lib.hegel_generate_bytes_result_free(ctx(), result)); + } + return code; + } + + @Override + public int generateString(long tc, long generator, String[] out) { + BufferResult result = new BufferResult(); + int code = lib.hegel_generate_string(ctx(), pointer(tc), pointer(generator), result); + if (code == Abi.OK) { + out[0] = new String(copyBuffer(result), StandardCharsets.UTF_8); + check("hegel_generate_string_result_free", lib.hegel_generate_string_result_free(ctx(), result)); + } + return code; + } + + /** Copy an engine-allocated {@code {data, len}} buffer out before it is freed. */ + private static byte[] copyBuffer(BufferResult result) { + return result.data.getByteArray(0, (int) result.len); + } + + @Override + public int generateDate(long tc, LocalDate min, LocalDate max, LocalDate[] out) { + HegelDate result = new HegelDate(); + int code = lib.hegel_generate_date(ctx(), pointer(tc), dateValue(min), dateValue(max), result); + if (code == Abi.OK) { + out[0] = readDate(result); + } + return code; + } + + @Override + public int generateTime(long tc, LocalTime min, LocalTime max, LocalTime[] out) { + HegelTime result = new HegelTime(); + int code = lib.hegel_generate_time(ctx(), pointer(tc), timeValue(min), timeValue(max), result); + if (code == Abi.OK) { + out[0] = readTime(result); + } + return code; + } + + @Override + public int generateDatetime(long tc, LocalDateTime min, LocalDateTime max, LocalDateTime[] out) { + HegelDatetime result = new HegelDatetime(); + int code = lib.hegel_generate_datetime(ctx(), pointer(tc), datetimeValue(min), datetimeValue(max), result); + if (code == Abi.OK) { + out[0] = LocalDateTime.of(readDate(result.date), readTime(result.time)); + } + return code; + } + + private static HegelDate.ByValue dateValue(LocalDate date) { + HegelDate.ByValue value = new HegelDate.ByValue(); + fillDate(value, date); + return value; + } + + private static HegelTime.ByValue timeValue(LocalTime time) { + HegelTime.ByValue value = new HegelTime.ByValue(); + fillTime(value, time); + return value; + } + + private static HegelDatetime.ByValue datetimeValue(LocalDateTime dt) { + HegelDatetime.ByValue value = new HegelDatetime.ByValue(); + value.date = new HegelDate(); + value.time = new HegelTime(); + fillDate(value.date, dt.toLocalDate()); + fillTime(value.time, dt.toLocalTime()); + return value; + } + + private static void fillDate(HegelDate value, LocalDate date) { + value.year = date.getYear(); + value.month = (byte) date.getMonthValue(); + value.day = (byte) date.getDayOfMonth(); + } + + private static LocalDate readDate(HegelDate value) { + return LocalDate.of(value.year, value.month, value.day); + } + + private static void fillTime(HegelTime value, LocalTime time) { + value.hour = (byte) time.getHour(); + value.minute = (byte) time.getMinute(); + value.second = (byte) time.getSecond(); + value.microsecond = time.getNano() / 1_000; + } + + private static LocalTime readTime(HegelTime value) { + return LocalTime.of(value.hour, value.minute, value.second, value.microsecond * 1_000); + } + + @Override + public int generateUuid(long tc, int version, boolean hasVersion, byte[] out16) { + return fixedBytesDraw( + buf -> lib.hegel_generate_uuid(ctx(), pointer(tc), (byte) version, cbool(hasVersion), buf), out16); + } + + @Override + public int generateIpv4(long tc, byte[] out4) { + return fixedBytesDraw(buf -> lib.hegel_generate_ipv4(ctx(), pointer(tc), buf), out4); + } + + @Override + public int generateIpv6(long tc, byte[] out16) { + return fixedBytesDraw(buf -> lib.hegel_generate_ipv6(ctx(), pointer(tc), buf), out16); + } + + @FunctionalInterface + private interface BytesDraw { + int run(Pointer out); + } + + /** Run a draw writing into a fixed-size byte buffer, copying it out on success. */ + private static int fixedBytesDraw(BytesDraw draw, byte[] out) { + Memory buf = new Memory(out.length); + int code = draw.run(buf); + if (code == Abi.OK) { + buf.read(0, out, 0, out.length); + } + return code; + } + + // --- string-generator handles --- + + @Override + public int stringGeneratorText( + long minSize, + long maxSize, + String codec, + long minCodepoint, + long maxCodepoint, + List categories, + List excludeCategories, + String includeCharacters, + String excludeCharacters, + long[] out) { + Pointer categoriesArr = cstrArray(categories); + Pointer excludeArr = cstrArray(excludeCategories); + byte[] include = utf8OrNull(includeCharacters); + byte[] exclude = utf8OrNull(excludeCharacters); + PointerByReference outRef = new PointerByReference(); + int code = lib.hegel_string_generator_text( + ctx(), + minSize, + maxSize, + codec, + (int) minCodepoint, + (int) maxCodepoint, + categoriesArr, + categories == null ? 0L : categories.size(), + excludeArr, + excludeCategories == null ? 0L : excludeCategories.size(), + bytesOrNull(include), + include == null ? 0L : include.length, + bytesOrNull(exclude), + exclude == null ? 0L : exclude.length, + outRef); + // Read the (null-initialised) out slot unconditionally: callers check the return code + // before using it. + out[0] = address(outRef.getValue()); + return code; + } + + private static byte[] utf8OrNull(String s) { + return s == null ? null : s.getBytes(StandardCharsets.UTF_8); + } + + private static Pointer bytesOrNull(byte[] bytes) { + if (bytes == null) { + return null; + } + Memory buf = new Memory(Math.max(bytes.length, 1)); + buf.write(0, bytes, 0, bytes.length); + return buf; + } + + /** A NULL-distinct {@code char**}: {@code null} maps to NULL, an empty list to a valid pointer. */ + private static Pointer cstrArray(List strings) { + return strings == null ? null : new StringArray(strings.toArray(new String[0]), "UTF-8"); + } + + @Override + public int stringGeneratorRegex(String pattern, boolean fullmatch, long alphabet, long[] out) { + PointerByReference outRef = new PointerByReference(); + int code = lib.hegel_string_generator_regex(ctx(), pattern, cbool(fullmatch), pointer(alphabet), outRef); + out[0] = address(outRef.getValue()); + return code; + } + + @Override + public int stringGeneratorEmail(long[] out) { + PointerByReference outRef = new PointerByReference(); + int code = lib.hegel_string_generator_email(ctx(), outRef); + out[0] = address(outRef.getValue()); + return code; + } + + @Override + public int stringGeneratorUrl(long[] out) { + PointerByReference outRef = new PointerByReference(); + int code = lib.hegel_string_generator_url(ctx(), outRef); + out[0] = address(outRef.getValue()); + return code; + } + + @Override + public int stringGeneratorDomain(long maxLength, long[] out) { + PointerByReference outRef = new PointerByReference(); + int code = lib.hegel_string_generator_domain(ctx(), maxLength, outRef); + out[0] = address(outRef.getValue()); + return code; + } + + @Override + public void stringGeneratorFree(long generator) { + check("hegel_string_generator_free", lib.hegel_string_generator_free(ctx(), pointer(generator))); + } + + // --- structure --- + + @Override + public int startSpan(long tc, long label) { + return lib.hegel_start_span(ctx(), pointer(tc), label); + } + + @Override + public int stopSpan(long tc, boolean discard) { + return lib.hegel_stop_span(ctx(), pointer(tc), cbool(discard)); + } + + @Override + public int newCollection(long tc, long minSize, long maxSize, long[] outId) { + LongByReference ref = new LongByReference(); + int code = lib.hegel_new_collection(ctx(), pointer(tc), minSize, maxSize, ref); + outId[0] = ref.getValue(); + return code; + } + + @Override + public int collectionMore(long tc, long id, boolean[] outMore) { + ByteByReference ref = new ByteByReference(); + int code = lib.hegel_collection_more(ctx(), pointer(tc), id, ref); + if (code == Abi.OK) { + outMore[0] = ref.getValue() != 0; + } + return code; + } + + @Override + public int collectionReject(long tc, long id, String why) { + return lib.hegel_collection_reject(ctx(), pointer(tc), id, why); + } + + @Override + public int newPool(long tc, long[] outId) { + LongByReference ref = new LongByReference(); + int code = lib.hegel_new_pool(ctx(), pointer(tc), ref); + outId[0] = ref.getValue(); + return code; + } + + @Override + public int poolAdd(long tc, long poolId, long[] outVariableId) { + LongByReference ref = new LongByReference(); + int code = lib.hegel_pool_add(ctx(), pointer(tc), poolId, ref); + outVariableId[0] = ref.getValue(); + return code; + } + + @Override + public int poolGenerate(long tc, long poolId, boolean consume, long[] outVariableId) { + LongByReference ref = new LongByReference(); + int code = lib.hegel_pool_generate(ctx(), pointer(tc), poolId, cbool(consume), ref); + outVariableId[0] = ref.getValue(); + return code; + } + + @Override + public int newStateMachine(long tc, List ruleNames, List invariantNames, long[] outId) { + LongByReference ref = new LongByReference(); + int code = lib.hegel_new_state_machine( + ctx(), + pointer(tc), + cstrArray(ruleNames), + ruleNames.size(), + cstrArray(invariantNames), + invariantNames.size(), + ref); + outId[0] = ref.getValue(); + return code; + } + + @Override + public int stateMachineNextRule(long tc, long stateMachineId, long[] outRuleIndex) { + LongByReference ref = new LongByReference(); + int code = lib.hegel_state_machine_next_rule(ctx(), pointer(tc), stateMachineId, ref); + if (code == Abi.OK) { + outRuleIndex[0] = ref.getValue(); + } + return code; + } + + @Override + public int target(long tc, double value, String label) { + return lib.hegel_target(ctx(), pointer(tc), value, label); + } + + @Override + public int markComplete(long tc, int status, String origin) { + return lib.hegel_mark_complete(ctx(), pointer(tc), status, origin); + } + + // --- results --- + + @Override + public int runResultStatus(long result) { + IntByReference ref = new IntByReference(); + check("hegel_run_result_status", lib.hegel_run_result_status(ctx(), pointer(result), ref)); + return ref.getValue(); + } + + @Override + public String runResultError(long result) { + PointerByReference ref = new PointerByReference(); + check("hegel_run_result_error", lib.hegel_run_result_error(ctx(), pointer(result), ref)); + return readCString(ref.getValue()); + } + + @Override + public long runResultFailureCount(long result) { + LongByReference ref = new LongByReference(); + check("hegel_run_result_failure_count", lib.hegel_run_result_failure_count(ctx(), pointer(result), ref)); + return ref.getValue(); + } + + @Override + public String failureBlob(long result, long index) { + PointerByReference failureOut = new PointerByReference(); + check("hegel_run_result_failure", lib.hegel_run_result_failure(ctx(), pointer(result), index, failureOut)); + Pointer failure = failureOut.getValue(); + PointerByReference blobOut = new PointerByReference(); + check("hegel_failure_reproduction_blob", lib.hegel_failure_reproduction_blob(ctx(), failure, blobOut)); + String blob = readCString(blobOut.getValue()); + check("hegel_failure_free", lib.hegel_failure_free(ctx(), failure)); + return blob; + } + + // --- diagnostics --- + + @Override + public String lastErrorMessage() { + return readCString(lib.hegel_context_last_error(ctx())); + } + + @Override + public String version() { + PointerByReference ref = new PointerByReference(); + check("hegel_version", lib.hegel_version(ctx(), ref)); + return readCString(ref.getValue()); + } +} diff --git a/hegel-jna/src/main/java/dev/hegel/LibhegelBackend.java b/hegel-jna/src/main/java/dev/hegel/LibhegelBackend.java new file mode 100644 index 0000000..2874490 --- /dev/null +++ b/hegel-jna/src/main/java/dev/hegel/LibhegelBackend.java @@ -0,0 +1,11 @@ +package dev.hegel; + +import java.nio.file.Path; + +final class LibhegelBackend { + private LibhegelBackend() {} + + static Libhegel open(Path libraryPath) { + return new JnaLibhegel(libraryPath); + } +} diff --git a/hegel-jna/src/test/java/dev/hegel/JnaLibhegelTest.java b/hegel-jna/src/test/java/dev/hegel/JnaLibhegelTest.java new file mode 100644 index 0000000..32f9c13 --- /dev/null +++ b/hegel-jna/src/test/java/dev/hegel/JnaLibhegelTest.java @@ -0,0 +1,167 @@ +package dev.hegel; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; + +/** Covers {@link JnaLibhegel} edge branches that the normal engine path does not reach. */ +class JnaLibhegelTest { + + private static JnaLibhegel real() { + return new JnaLibhegel(LibraryLoader.fromEnvironment().resolve()); + } + + @Test + void constructorRejectsBadPath() { + assertThrows(HegelException.class, () -> new JnaLibhegel(Path.of("/nonexistent/libhegel.so"))); + } + + @Test + void callbackDecodesAndSwallowsExceptions() { + byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + Memory line = new Memory(bytes.length); + line.write(0, bytes, 0, bytes.length); + AtomicReference got = new AtomicReference<>(); + new JnaLibhegel.LineCallback(got::set).invoke(Pointer.NULL, line, bytes.length); + assertEquals("hello", got.get()); + // A throwing consumer must be swallowed: an exception escaping a native callback must + // never unwind into the engine. + Consumer throwing = s -> { + throw new IllegalStateException("never escapes"); + }; + JnaLibhegel.emitLine(throwing, line, bytes.length); + } + + @Test + void readCStringHandlesNullAndValue() { + assertNull(JnaLibhegel.readCString(null)); + byte[] bytes = "hello\0".getBytes(StandardCharsets.UTF_8); + Memory value = new Memory(bytes.length); + value.write(0, bytes, 0, bytes.length); + assertEquals("hello", JnaLibhegel.readCString(value)); + } + + @Test + void versionReadsAndFreshContextHasNoError() { + JnaLibhegel lib = real(); + assertNotNull(lib.version()); + String message = lib.lastErrorMessage(); + assertTrue(message == null || message.isEmpty(), String.valueOf(message)); + } + + @Test + void infrastructureCallsReportNullHandles() { + JnaLibhegel lib = real(); + // A NULL handle on an infra call surfaces as a HegelException carrying the engine's + // diagnostic rather than undefined behaviour. + assertThrows(HegelException.class, () -> lib.runResultStatus(0)); + assertThrows(HegelException.class, () -> lib.runStart(0, null)); + } + + @Test + void runWithDefaultOutputStartsAndFrees() { + JnaLibhegel lib = real(); + long s = lib.settingsNew(); + long run = lib.runStart(s, null); + assertNotEquals(0, run); + lib.runFree(run); + lib.settingsFree(s); + } + + @Test + void scalarDrawsReportNullHandles() { + JnaLibhegel lib = real(); + // Out-slots are seeded with sentinels: a failed call must not copy anything out. + boolean[] b = {true}; + assertEquals(Abi.E_INVALID_HANDLE, lib.generateBoolean(0, 0.5, b)); + assertTrue(b[0]); + long[] i = {7}; + assertEquals(Abi.E_INVALID_HANDLE, lib.generateInteger(0, 0, 10, i)); + assertEquals(7, i[0]); + double[] f = {1.5}; + assertEquals(Abi.E_INVALID_HANDLE, lib.generateFloat(0, 64, 0, 1, false, false, false, false, 0, f)); + assertEquals(1.5, f[0]); + } + + @Test + void structuredDrawsReportNullHandles() { + JnaLibhegel lib = real(); + java.time.LocalDate d = java.time.LocalDate.of(2000, 1, 1); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateDate(0, d, d, new java.time.LocalDate[1])); + java.time.LocalTime t = java.time.LocalTime.NOON; + assertEquals(Abi.E_INVALID_HANDLE, lib.generateTime(0, t, t, new java.time.LocalTime[1])); + java.time.LocalDateTime dt = java.time.LocalDateTime.of(d, t); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateDatetime(0, dt, dt, new java.time.LocalDateTime[1])); + } + + @Test + void fixedBytesDrawsReportNullHandles() { + JnaLibhegel lib = real(); + // A NULL test case is rejected before the engine writes anything, so fixedBytesDraw never + // reaches its copy-out and each buffer keeps the caller's bytes. + byte[] uuid = sentinel(16); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateUuid(0, 0, false, uuid)); + assertArrayEquals(sentinel(16), uuid); + + byte[] ipv4 = sentinel(4); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateIpv4(0, ipv4)); + assertArrayEquals(sentinel(4), ipv4); + + byte[] ipv6 = sentinel(16); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateIpv6(0, ipv6)); + assertArrayEquals(sentinel(16), ipv6); + } + + /** A buffer of non-zero bytes, so an unwanted copy-out is visible rather than a no-op. */ + private static byte[] sentinel(int length) { + byte[] b = new byte[length]; + java.util.Arrays.fill(b, (byte) 0x7f); + return b; + } + + @Test + void stateMachineNextRuleReportsNullHandle() { + JnaLibhegel lib = real(); + // Both handles are NULL: the engine rejects the call on the test case before it + // dereferences the state machine, so this is a clean error rather than undefined behaviour. + long[] out = {7}; + assertEquals(Abi.E_INVALID_HANDLE, lib.stateMachineNextRule(0, 0, out)); + // The rule index is read only on success, so a failed call leaves the caller's value alone. + assertEquals(7, out[0]); + } + + @Test + void undecodableBlobWithDefaultOutputIsRejected() { + JnaLibhegel lib = real(); + long s = lib.settingsNew(); + long[] out = new long[1]; + // A null output callback leaves replay output on stderr; the garbage blob is rejected. + assertEquals(Abi.E_INVALID_ARG, lib.testCaseFromBlob(s, "not-a-blob!!!", null, out)); + lib.settingsFree(s); + } + + @Test + void regexGeneratorAcceptsATextAlphabet() { + JnaLibhegel lib = real(); + long[] alphabet = new long[1]; + assertEquals( + Abi.OK, + lib.stringGeneratorText(0, 5, "ascii", 0, Abi.NO_MAX_CODEPOINT, null, null, null, null, alphabet)); + long[] regex = new long[1]; + assertEquals(Abi.OK, lib.stringGeneratorRegex("[a-z]+", true, alphabet[0], regex)); + lib.stringGeneratorFree(regex[0]); + lib.stringGeneratorFree(alphabet[0]); + } +} diff --git a/hegel/pom.xml b/hegel/pom.xml new file mode 100644 index 0000000..a0d6683 --- /dev/null +++ b/hegel/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + + dev.hegel + hegel-parent + 0.5.0 + + + hegel + jar + + hegel-java + Property-based testing for Java, built on Hypothesis (FFM binding, Java 22+) + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + dev.hegel + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + org.codehaus.mojo + exec-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.apache.maven.plugins + maven-invoker-plugin + + + module-consumer-it + + install + run + + + + + + + com.diffplug.spotless + spotless-maven-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + org.jacoco + jacoco-maven-plugin + + + + diff --git a/src/it/module-consumer/invoker.properties b/hegel/src/it/module-consumer/invoker.properties similarity index 100% rename from src/it/module-consumer/invoker.properties rename to hegel/src/it/module-consumer/invoker.properties diff --git a/src/it/module-consumer/pom.xml b/hegel/src/it/module-consumer/pom.xml similarity index 100% rename from src/it/module-consumer/pom.xml rename to hegel/src/it/module-consumer/pom.xml diff --git a/src/it/module-consumer/src/main/java/module-info.java b/hegel/src/it/module-consumer/src/main/java/module-info.java similarity index 100% rename from src/it/module-consumer/src/main/java/module-info.java rename to hegel/src/it/module-consumer/src/main/java/module-info.java diff --git a/src/it/module-consumer/src/test/java/dev/hegel/consumer/ConsumerTest.java b/hegel/src/it/module-consumer/src/test/java/dev/hegel/consumer/ConsumerTest.java similarity index 100% rename from src/it/module-consumer/src/test/java/dev/hegel/consumer/ConsumerTest.java rename to hegel/src/it/module-consumer/src/test/java/dev/hegel/consumer/ConsumerTest.java diff --git a/hegel/src/main/java/dev/hegel/LibhegelBackend.java b/hegel/src/main/java/dev/hegel/LibhegelBackend.java new file mode 100644 index 0000000..e9ae33b --- /dev/null +++ b/hegel/src/main/java/dev/hegel/LibhegelBackend.java @@ -0,0 +1,11 @@ +package dev.hegel; + +import java.nio.file.Path; + +final class LibhegelBackend { + private LibhegelBackend() {} + + static Libhegel open(Path libraryPath) { + return new RealLibhegel(libraryPath); + } +} diff --git a/src/main/java/dev/hegel/RealLibhegel.java b/hegel/src/main/java/dev/hegel/RealLibhegel.java similarity index 83% rename from src/main/java/dev/hegel/RealLibhegel.java rename to hegel/src/main/java/dev/hegel/RealLibhegel.java index 8593dd8..f509994 100644 --- a/src/main/java/dev/hegel/RealLibhegel.java +++ b/hegel/src/main/java/dev/hegel/RealLibhegel.java @@ -429,6 +429,10 @@ private MemorySegment ctx() { return context.get(); } + private static MemorySegment segment(long handle) { + return MemorySegment.ofAddress(handle); + } + private int rc(MethodHandle handle, Object... args) { Object[] withCtx = new Object[args.length + 1]; withCtx[0] = ctx(); @@ -474,143 +478,143 @@ private MemorySegment upcallStub(Arena arena, Consumer output) { // --- settings --- @Override - public MemorySegment settingsNew() { + public long settingsNew() { MemorySegment out = Arena.ofAuto().allocate(ADDRESS); check("hegel_settings_new", rc(settingsNew, out)); - return out.get(ADDRESS, 0); + return out.get(ADDRESS, 0).address(); } @Override - public void settingsFree(MemorySegment s) { - check("hegel_settings_free", rc(settingsFree, s)); + public void settingsFree(long s) { + check("hegel_settings_free", rc(settingsFree, segment(s))); } @Override - public void settingsMode(MemorySegment s, int mode) { - check("hegel_settings_set_mode", rc(settingsSetMode, s, mode)); + public void settingsMode(long s, int mode) { + check("hegel_settings_set_mode", rc(settingsSetMode, segment(s), mode)); } @Override - public void settingsBackend(MemorySegment s, int backend) { - check("hegel_settings_set_backend", rc(settingsSetBackend, s, backend)); + public void settingsBackend(long s, int backend) { + check("hegel_settings_set_backend", rc(settingsSetBackend, segment(s), backend)); } @Override - public void settingsTestCases(MemorySegment s, long n) { - check("hegel_settings_set_test_cases", rc(settingsSetTestCases, s, n)); + public void settingsTestCases(long s, long n) { + check("hegel_settings_set_test_cases", rc(settingsSetTestCases, segment(s), n)); } @Override - public void settingsVerbosity(MemorySegment s, int v) { - check("hegel_settings_set_verbosity", rc(settingsSetVerbosity, s, v)); + public void settingsVerbosity(long s, int v) { + check("hegel_settings_set_verbosity", rc(settingsSetVerbosity, segment(s), v)); } @Override - public void settingsSeed(MemorySegment s, long seed, boolean hasSeed) { - check("hegel_settings_set_seed", rc(settingsSetSeed, s, seed, hasSeed)); + public void settingsSeed(long s, long seed, boolean hasSeed) { + check("hegel_settings_set_seed", rc(settingsSetSeed, segment(s), seed, hasSeed)); } @Override - public void settingsDerandomize(MemorySegment s, boolean derandomize) { - check("hegel_settings_set_derandomize", rc(settingsSetDerandomize, s, derandomize)); + public void settingsDerandomize(long s, boolean derandomize) { + check("hegel_settings_set_derandomize", rc(settingsSetDerandomize, segment(s), derandomize)); } @Override - public void settingsReportMultipleFailures(MemorySegment s, boolean yes) { - check("hegel_settings_set_report_multiple_failures", rc(settingsSetReportMultipleFailures, s, yes)); + public void settingsReportMultipleFailures(long s, boolean yes) { + check("hegel_settings_set_report_multiple_failures", rc(settingsSetReportMultipleFailures, segment(s), yes)); } @Override - public void settingsDatabase(MemorySegment s, String path) { + public void settingsDatabase(long s, String path) { // libhegel copies the string during the call, so a per-call auto arena suffices. - check("hegel_settings_set_database", rc(settingsSetDatabase, s, cstr(Arena.ofAuto(), path))); + check("hegel_settings_set_database", rc(settingsSetDatabase, segment(s), cstr(Arena.ofAuto(), path))); } @Override - public void settingsDatabaseKey(MemorySegment s, String key) { - check("hegel_settings_set_database_key", rc(settingsSetDatabaseKey, s, cstr(Arena.ofAuto(), key))); + public void settingsDatabaseKey(long s, String key) { + check("hegel_settings_set_database_key", rc(settingsSetDatabaseKey, segment(s), cstr(Arena.ofAuto(), key))); } @Override - public void settingsPhases(MemorySegment s, int mask) { - check("hegel_settings_set_phases", rc(settingsSetPhases, s, mask)); + public void settingsPhases(long s, int mask) { + check("hegel_settings_set_phases", rc(settingsSetPhases, segment(s), mask)); } @Override - public void settingsSuppressHealthCheck(MemorySegment s, int mask) { - check("hegel_settings_set_suppress_health_check", rc(settingsSetSuppressHealthCheck, s, mask)); + public void settingsSuppressHealthCheck(long s, int mask) { + check("hegel_settings_set_suppress_health_check", rc(settingsSetSuppressHealthCheck, segment(s), mask)); } // --- run lifecycle --- @Override - public MemorySegment runStart(MemorySegment settings, Consumer output) { + public long runStart(long settings, Consumer output) { Arena arena = Arena.ofConfined(); MemorySegment callback = output == null ? MemorySegment.NULL : upcallStub(arena, output); MemorySegment out = arena.allocate(ADDRESS); - int code = rc(runStart, settings, callback, MemorySegment.NULL, out); + int code = rc(runStart, segment(settings), callback, MemorySegment.NULL, out); if (code != Abi.OK) { arena.close(); throw new HegelException( "hegel_run_start failed (rc=" + code + "): " + java.util.Objects.toString(lastErrorMessage(), "")); } - MemorySegment run = out.get(ADDRESS, 0); - runArenas.put(run.address(), arena); + long run = out.get(ADDRESS, 0).address(); + runArenas.put(run, arena); return run; } @Override - public MemorySegment nextTestCase(MemorySegment run) { + public long nextTestCase(long run) { MemorySegment out = Arena.ofAuto().allocate(ADDRESS); - check("hegel_next_test_case", rc(nextTestCase, run, out)); - return out.get(ADDRESS, 0); + check("hegel_next_test_case", rc(nextTestCase, segment(run), out)); + return out.get(ADDRESS, 0).address(); } @Override - public MemorySegment runResult(MemorySegment run) { + public long runResult(long run) { MemorySegment out = Arena.ofAuto().allocate(ADDRESS); - check("hegel_run_result", rc(runResult, run, out)); - return out.get(ADDRESS, 0); + check("hegel_run_result", rc(runResult, segment(run), out)); + return out.get(ADDRESS, 0).address(); } @Override - public void runResultFree(MemorySegment result) { - check("hegel_run_result_free", rc(runResultFree, result)); + public void runResultFree(long result) { + check("hegel_run_result_free", rc(runResultFree, segment(result))); } @Override - public void runFree(MemorySegment run) { - check("hegel_run_free", rc(runFree, run)); - Arena arena = runArenas.remove(run.address()); + public void runFree(long run) { + check("hegel_run_free", rc(runFree, segment(run))); + Arena arena = runArenas.remove(run); arena.close(); } @Override - public int testCaseFromBlob(MemorySegment settings, String blob, Consumer output, MemorySegment[] out) { + public int testCaseFromBlob(long settings, String blob, Consumer output, long[] out) { // The blob replay's output is emitted synchronously during this call, so the stub only // needs to live for its duration. try (Arena arena = Arena.ofConfined()) { MemorySegment callback = output == null ? MemorySegment.NULL : upcallStub(arena, output); MemorySegment outSeg = arena.allocate(ADDRESS); - int code = rc(testCaseFromBlob, settings, cstr(arena, blob), callback, MemorySegment.NULL, outSeg); + int code = rc(testCaseFromBlob, segment(settings), cstr(arena, blob), callback, MemorySegment.NULL, outSeg); if (code == Abi.OK) { - out[0] = outSeg.get(ADDRESS, 0); + out[0] = outSeg.get(ADDRESS, 0).address(); } return code; } } @Override - public void testCaseFree(MemorySegment tc) { - check("hegel_test_case_free", rc(testCaseFree, tc)); + public void testCaseFree(long tc) { + check("hegel_test_case_free", rc(testCaseFree, segment(tc))); } // --- draws --- @Override - public int generateBoolean(MemorySegment tc, double p, boolean[] out) { + public int generateBoolean(long tc, double p, boolean[] out) { MemorySegment seg = Arena.ofAuto().allocate(JAVA_BOOLEAN); - int code = rc(generateBoolean, tc, p, false, false, seg); + int code = rc(generateBoolean, segment(tc), p, false, false, seg); if (code == Abi.OK) { out[0] = seg.get(JAVA_BOOLEAN, 0); } @@ -618,9 +622,9 @@ public int generateBoolean(MemorySegment tc, double p, boolean[] out) { } @Override - public int generateInteger(MemorySegment tc, long min, long max, long[] out) { + public int generateInteger(long tc, long min, long max, long[] out) { MemorySegment seg = Arena.ofAuto().allocate(JAVA_LONG); - int code = rc(generateInteger, tc, min, max, seg); + int code = rc(generateInteger, segment(tc), min, max, seg); if (code == Abi.OK) { out[0] = seg.get(JAVA_LONG, 0); } @@ -629,7 +633,7 @@ public int generateInteger(MemorySegment tc, long min, long max, long[] out) { @Override public int generateFloat( - MemorySegment tc, + long tc, int width, double min, double max, @@ -642,7 +646,7 @@ public int generateFloat( MemorySegment seg = Arena.ofAuto().allocate(JAVA_DOUBLE); int code = rc( generateFloat, - tc, + segment(tc), width, min, max, @@ -659,10 +663,10 @@ public int generateFloat( } @Override - public int generateBytes(MemorySegment tc, long minSize, long maxSize, byte[][] out) { + public int generateBytes(long tc, long minSize, long maxSize, byte[][] out) { try (Arena arena = Arena.ofConfined()) { MemorySegment result = arena.allocate(BUFFER_RESULT_LAYOUT); - int code = rc(generateBytes, tc, minSize, maxSize, result); + int code = rc(generateBytes, segment(tc), minSize, maxSize, result); if (code == Abi.OK) { out[0] = copyBuffer(result); check("hegel_generate_bytes_result_free", rc(generateBytesResultFree, result)); @@ -672,10 +676,10 @@ public int generateBytes(MemorySegment tc, long minSize, long maxSize, byte[][] } @Override - public int generateString(MemorySegment tc, MemorySegment generator, String[] out) { + public int generateString(long tc, long generator, String[] out) { try (Arena arena = Arena.ofConfined()) { MemorySegment result = arena.allocate(BUFFER_RESULT_LAYOUT); - int code = rc(generateString, tc, generator, result); + int code = rc(generateString, segment(tc), segment(generator), result); if (code == Abi.OK) { out[0] = new String(copyBuffer(result), StandardCharsets.UTF_8); check("hegel_generate_string_result_free", rc(generateStringResultFree, result)); @@ -692,10 +696,10 @@ private static byte[] copyBuffer(MemorySegment result) { } @Override - public int generateDate(MemorySegment tc, LocalDate min, LocalDate max, LocalDate[] out) { + public int generateDate(long tc, LocalDate min, LocalDate max, LocalDate[] out) { try (Arena arena = Arena.ofConfined()) { MemorySegment outSeg = arena.allocate(DATE_LAYOUT); - int code = rc(generateDate, tc, dateSegment(arena, min), dateSegment(arena, max), outSeg); + int code = rc(generateDate, segment(tc), dateSegment(arena, min), dateSegment(arena, max), outSeg); if (code == Abi.OK) { out[0] = readDate(outSeg, 0); } @@ -704,10 +708,10 @@ public int generateDate(MemorySegment tc, LocalDate min, LocalDate max, LocalDat } @Override - public int generateTime(MemorySegment tc, LocalTime min, LocalTime max, LocalTime[] out) { + public int generateTime(long tc, LocalTime min, LocalTime max, LocalTime[] out) { try (Arena arena = Arena.ofConfined()) { MemorySegment outSeg = arena.allocate(TIME_LAYOUT); - int code = rc(generateTime, tc, timeSegment(arena, min), timeSegment(arena, max), outSeg); + int code = rc(generateTime, segment(tc), timeSegment(arena, min), timeSegment(arena, max), outSeg); if (code == Abi.OK) { out[0] = readTime(outSeg, 0); } @@ -716,10 +720,11 @@ public int generateTime(MemorySegment tc, LocalTime min, LocalTime max, LocalTim } @Override - public int generateDatetime(MemorySegment tc, LocalDateTime min, LocalDateTime max, LocalDateTime[] out) { + public int generateDatetime(long tc, LocalDateTime min, LocalDateTime max, LocalDateTime[] out) { try (Arena arena = Arena.ofConfined()) { MemorySegment outSeg = arena.allocate(DATETIME_LAYOUT); - int code = rc(generateDatetime, tc, datetimeSegment(arena, min), datetimeSegment(arena, max), outSeg); + int code = + rc(generateDatetime, segment(tc), datetimeSegment(arena, min), datetimeSegment(arena, max), outSeg); if (code == Abi.OK) { out[0] = LocalDateTime.of(readDate(outSeg, 0), readTime(outSeg, DATE_LAYOUT.byteSize())); } @@ -772,18 +777,18 @@ private static LocalTime readTime(MemorySegment seg, long offset) { } @Override - public int generateUuid(MemorySegment tc, int version, boolean hasVersion, byte[] out16) { - return fixedBytesDraw(seg -> rc(generateUuid, tc, (byte) version, hasVersion, seg), out16); + public int generateUuid(long tc, int version, boolean hasVersion, byte[] out16) { + return fixedBytesDraw(seg -> rc(generateUuid, segment(tc), (byte) version, hasVersion, seg), out16); } @Override - public int generateIpv4(MemorySegment tc, byte[] out4) { - return fixedBytesDraw(seg -> rc(generateIpv4, tc, seg), out4); + public int generateIpv4(long tc, byte[] out4) { + return fixedBytesDraw(seg -> rc(generateIpv4, segment(tc), seg), out4); } @Override - public int generateIpv6(MemorySegment tc, byte[] out16) { - return fixedBytesDraw(seg -> rc(generateIpv6, tc, seg), out16); + public int generateIpv6(long tc, byte[] out16) { + return fixedBytesDraw(seg -> rc(generateIpv6, segment(tc), seg), out16); } @FunctionalInterface @@ -816,7 +821,7 @@ public int stringGeneratorText( List excludeCategories, String includeCharacters, String excludeCharacters, - MemorySegment[] out) { + long[] out) { try (Arena arena = Arena.ofConfined()) { MemorySegment categoriesSeg = cstrArray(arena, categories); MemorySegment excludeSeg = cstrArray(arena, excludeCategories); @@ -841,7 +846,7 @@ public int stringGeneratorText( outSeg); // Read the (zero-initialised) out slot unconditionally: callers check the return code // before using it. - out[0] = outSeg.get(ADDRESS, 0); + out[0] = outSeg.get(ADDRESS, 0).address(); return code; } } @@ -872,75 +877,70 @@ private static MemorySegment cstrArray(Arena arena, List strings) { } @Override - public int stringGeneratorRegex(String pattern, boolean fullmatch, MemorySegment alphabet, MemorySegment[] out) { + public int stringGeneratorRegex(String pattern, boolean fullmatch, long alphabet, long[] out) { try (Arena arena = Arena.ofConfined()) { MemorySegment outSeg = arena.allocate(ADDRESS); - int code = rc( - stringGeneratorRegex, - cstr(arena, pattern), - fullmatch, - alphabet == null ? MemorySegment.NULL : alphabet, - outSeg); - out[0] = outSeg.get(ADDRESS, 0); + int code = rc(stringGeneratorRegex, cstr(arena, pattern), fullmatch, segment(alphabet), outSeg); + out[0] = outSeg.get(ADDRESS, 0).address(); return code; } } @Override - public int stringGeneratorEmail(MemorySegment[] out) { + public int stringGeneratorEmail(long[] out) { return handleConstructor(stringGeneratorEmail, out); } @Override - public int stringGeneratorUrl(MemorySegment[] out) { + public int stringGeneratorUrl(long[] out) { return handleConstructor(stringGeneratorUrl, out); } /** Run a no-argument string-generator constructor. */ - private int handleConstructor(MethodHandle constructor, MemorySegment[] out) { + private int handleConstructor(MethodHandle constructor, long[] out) { MemorySegment outSeg = Arena.ofAuto().allocate(ADDRESS); int code = rc(constructor, outSeg); - out[0] = outSeg.get(ADDRESS, 0); + out[0] = outSeg.get(ADDRESS, 0).address(); return code; } @Override - public int stringGeneratorDomain(long maxLength, MemorySegment[] out) { + public int stringGeneratorDomain(long maxLength, long[] out) { MemorySegment outSeg = Arena.ofAuto().allocate(ADDRESS); int code = rc(stringGeneratorDomain, maxLength, outSeg); - out[0] = outSeg.get(ADDRESS, 0); + out[0] = outSeg.get(ADDRESS, 0).address(); return code; } @Override - public void stringGeneratorFree(MemorySegment generator) { - check("hegel_string_generator_free", rc(stringGeneratorFree, generator)); + public void stringGeneratorFree(long generator) { + check("hegel_string_generator_free", rc(stringGeneratorFree, segment(generator))); } // --- structure --- @Override - public int startSpan(MemorySegment tc, long label) { - return rc(startSpan, tc, label); + public int startSpan(long tc, long label) { + return rc(startSpan, segment(tc), label); } @Override - public int stopSpan(MemorySegment tc, boolean discard) { - return rc(stopSpan, tc, discard); + public int stopSpan(long tc, boolean discard) { + return rc(stopSpan, segment(tc), discard); } @Override - public int newCollection(MemorySegment tc, long minSize, long maxSize, long[] outId) { + public int newCollection(long tc, long minSize, long maxSize, long[] outId) { MemorySegment seg = Arena.ofAuto().allocate(JAVA_LONG); - int code = rc(newCollection, tc, minSize, maxSize, seg); + int code = rc(newCollection, segment(tc), minSize, maxSize, seg); outId[0] = seg.get(JAVA_LONG, 0); return code; } @Override - public int collectionMore(MemorySegment tc, long id, boolean[] outMore) { + public int collectionMore(long tc, long id, boolean[] outMore) { MemorySegment seg = Arena.ofAuto().allocate(JAVA_BOOLEAN); - int code = rc(collectionMore, tc, id, seg); + int code = rc(collectionMore, segment(tc), id, seg); if (code == Abi.OK) { outMore[0] = seg.get(JAVA_BOOLEAN, 0); } @@ -948,51 +948,57 @@ public int collectionMore(MemorySegment tc, long id, boolean[] outMore) { } @Override - public int collectionReject(MemorySegment tc, long id, String why) { - return rc(collectionReject, tc, id, cstr(Arena.ofAuto(), why)); + public int collectionReject(long tc, long id, String why) { + return rc(collectionReject, segment(tc), id, cstr(Arena.ofAuto(), why)); } @Override - public int newPool(MemorySegment tc, long[] outId) { + public int newPool(long tc, long[] outId) { MemorySegment seg = Arena.ofAuto().allocate(JAVA_LONG); - int code = rc(newPool, tc, seg); + int code = rc(newPool, segment(tc), seg); outId[0] = seg.get(JAVA_LONG, 0); return code; } @Override - public int poolAdd(MemorySegment tc, long poolId, long[] outVariableId) { + public int poolAdd(long tc, long poolId, long[] outVariableId) { MemorySegment seg = Arena.ofAuto().allocate(JAVA_LONG); - int code = rc(poolAdd, tc, poolId, seg); + int code = rc(poolAdd, segment(tc), poolId, seg); outVariableId[0] = seg.get(JAVA_LONG, 0); return code; } @Override - public int poolGenerate(MemorySegment tc, long poolId, boolean consume, long[] outVariableId) { + public int poolGenerate(long tc, long poolId, boolean consume, long[] outVariableId) { MemorySegment seg = Arena.ofAuto().allocate(JAVA_LONG); - int code = rc(poolGenerate, tc, poolId, consume, seg); + int code = rc(poolGenerate, segment(tc), poolId, consume, seg); outVariableId[0] = seg.get(JAVA_LONG, 0); return code; } @Override - public int newStateMachine(MemorySegment tc, List ruleNames, List invariantNames, long[] outId) { + public int newStateMachine(long tc, List ruleNames, List invariantNames, long[] outId) { try (Arena arena = Arena.ofConfined()) { MemorySegment rules = cstrArray(arena, ruleNames); MemorySegment invariants = cstrArray(arena, invariantNames); MemorySegment seg = arena.allocate(JAVA_LONG); int code = rc( - newStateMachine, tc, rules, (long) ruleNames.size(), invariants, (long) invariantNames.size(), seg); + newStateMachine, + segment(tc), + rules, + (long) ruleNames.size(), + invariants, + (long) invariantNames.size(), + seg); outId[0] = seg.get(JAVA_LONG, 0); return code; } } @Override - public int stateMachineNextRule(MemorySegment tc, long stateMachineId, long[] outRuleIndex) { + public int stateMachineNextRule(long tc, long stateMachineId, long[] outRuleIndex) { MemorySegment seg = Arena.ofAuto().allocate(JAVA_LONG); - int code = rc(stateMachineNextRule, tc, stateMachineId, seg); + int code = rc(stateMachineNextRule, segment(tc), stateMachineId, seg); if (code == Abi.OK) { outRuleIndex[0] = seg.get(JAVA_LONG, 0); } @@ -1000,42 +1006,42 @@ public int stateMachineNextRule(MemorySegment tc, long stateMachineId, long[] ou } @Override - public int target(MemorySegment tc, double value, String label) { - return rc(target, tc, value, cstr(Arena.ofAuto(), label)); + public int target(long tc, double value, String label) { + return rc(target, segment(tc), value, cstr(Arena.ofAuto(), label)); } @Override - public int markComplete(MemorySegment tc, int status, String origin) { - return rc(markComplete, tc, status, cstr(Arena.ofAuto(), origin)); + public int markComplete(long tc, int status, String origin) { + return rc(markComplete, segment(tc), status, cstr(Arena.ofAuto(), origin)); } // --- results --- @Override - public int runResultStatus(MemorySegment result) { + public int runResultStatus(long result) { MemorySegment seg = Arena.ofAuto().allocate(JAVA_INT); - check("hegel_run_result_status", rc(runResultStatus, result, seg)); + check("hegel_run_result_status", rc(runResultStatus, segment(result), seg)); return seg.get(JAVA_INT, 0); } @Override - public String runResultError(MemorySegment result) { + public String runResultError(long result) { MemorySegment seg = Arena.ofAuto().allocate(ADDRESS); - check("hegel_run_result_error", rc(runResultError, result, seg)); + check("hegel_run_result_error", rc(runResultError, segment(result), seg)); return readCString(seg.get(ADDRESS, 0)); } @Override - public long runResultFailureCount(MemorySegment result) { + public long runResultFailureCount(long result) { MemorySegment seg = Arena.ofAuto().allocate(JAVA_LONG); - check("hegel_run_result_failure_count", rc(runResultFailureCount, result, seg)); + check("hegel_run_result_failure_count", rc(runResultFailureCount, segment(result), seg)); return seg.get(JAVA_LONG, 0); } @Override - public String failureBlob(MemorySegment result, long index) { + public String failureBlob(long result, long index) { MemorySegment failureOut = Arena.ofAuto().allocate(ADDRESS); - check("hegel_run_result_failure", rc(runResultFailure, result, index, failureOut)); + check("hegel_run_result_failure", rc(runResultFailure, segment(result), index, failureOut)); MemorySegment failure = failureOut.get(ADDRESS, 0); MemorySegment blobOut = Arena.ofAuto().allocate(ADDRESS); check("hegel_failure_reproduction_blob", rc(failureReproductionBlob, failure, blobOut)); diff --git a/hegel/src/test/java/dev/hegel/FfmCoverageTest.java b/hegel/src/test/java/dev/hegel/FfmCoverageTest.java new file mode 100644 index 0000000..f77613f --- /dev/null +++ b/hegel/src/test/java/dev/hegel/FfmCoverageTest.java @@ -0,0 +1,32 @@ +package dev.hegel; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; + +/** Targeted tests closing FFM-binding coverage branches the engine path does not reach. */ +class FfmCoverageTest { + // --- output-callback bridge --- + @Test + void emitLineDecodesAndSwallowsExceptions() { + try (Arena arena = Arena.ofConfined()) { + byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + MemorySegment line = arena.allocate(bytes.length); + MemorySegment.copy(bytes, 0, line, ValueLayout.JAVA_BYTE, 0, bytes.length); + AtomicReference got = new AtomicReference<>(); + RealLibhegel.emitLine(got::set, MemorySegment.NULL, line, bytes.length); + assertEquals("hello", got.get()); + // A throwing consumer must be swallowed: an exception escaping an upcall kills the VM. + Consumer throwing = s -> { + throw new IllegalStateException("never escapes"); + }; + RealLibhegel.emitLine(throwing, MemorySegment.NULL, line, bytes.length); + } + } +} diff --git a/src/test/java/dev/hegel/RealLibhegelTest.java b/hegel/src/test/java/dev/hegel/RealLibhegelTest.java similarity index 81% rename from src/test/java/dev/hegel/RealLibhegelTest.java rename to hegel/src/test/java/dev/hegel/RealLibhegelTest.java index ab6f960..3ee682a 100644 --- a/src/test/java/dev/hegel/RealLibhegelTest.java +++ b/hegel/src/test/java/dev/hegel/RealLibhegelTest.java @@ -74,25 +74,19 @@ void infrastructureCallsReportNullHandles() { RealLibhegel lib = real(); // A NULL handle on an infra call surfaces as a HegelException carrying the engine's // diagnostic rather than undefined behaviour. - assertThrows(HegelException.class, () -> lib.runResultStatus(java.lang.foreign.MemorySegment.NULL)); - assertThrows(HegelException.class, () -> lib.runStart(java.lang.foreign.MemorySegment.NULL, null)); + assertThrows(HegelException.class, () -> lib.runResultStatus(0)); + assertThrows(HegelException.class, () -> lib.runStart(0, null)); } @Test void structuredDrawsReportNullHandles() { RealLibhegel lib = real(); java.time.LocalDate d = java.time.LocalDate.of(2000, 1, 1); - assertEquals( - Abi.E_INVALID_HANDLE, - lib.generateDate(java.lang.foreign.MemorySegment.NULL, d, d, new java.time.LocalDate[1])); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateDate(0, d, d, new java.time.LocalDate[1])); java.time.LocalTime t = java.time.LocalTime.NOON; - assertEquals( - Abi.E_INVALID_HANDLE, - lib.generateTime(java.lang.foreign.MemorySegment.NULL, t, t, new java.time.LocalTime[1])); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateTime(0, t, t, new java.time.LocalTime[1])); java.time.LocalDateTime dt = java.time.LocalDateTime.of(d, t); - assertEquals( - Abi.E_INVALID_HANDLE, - lib.generateDatetime(java.lang.foreign.MemorySegment.NULL, dt, dt, new java.time.LocalDateTime[1])); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateDatetime(0, dt, dt, new java.time.LocalDateTime[1])); } @Test @@ -101,7 +95,7 @@ void booleanDrawReportsNullHandle() { // Seeded true because the draw's scratch segment is arena-allocated and therefore zeroed: // a copy-out on this failed call would read back false rather than leave the value alone. boolean[] out = {true}; - assertEquals(Abi.E_INVALID_HANDLE, lib.generateBoolean(MemorySegment.NULL, 0.5, out)); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateBoolean(0, 0.5, out)); assertTrue(out[0]); } @@ -111,15 +105,15 @@ void fixedBytesDrawsReportNullHandles() { // A NULL test case is rejected before the engine writes anything, so fixedBytesDraw never // reaches its copy-out and each buffer keeps the caller's bytes. byte[] uuid = sentinel(16); - assertEquals(Abi.E_INVALID_HANDLE, lib.generateUuid(MemorySegment.NULL, 0, false, uuid)); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateUuid(0, 0, false, uuid)); assertArrayEquals(sentinel(16), uuid); byte[] ipv4 = sentinel(4); - assertEquals(Abi.E_INVALID_HANDLE, lib.generateIpv4(MemorySegment.NULL, ipv4)); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateIpv4(0, ipv4)); assertArrayEquals(sentinel(4), ipv4); byte[] ipv6 = sentinel(16); - assertEquals(Abi.E_INVALID_HANDLE, lib.generateIpv6(MemorySegment.NULL, ipv6)); + assertEquals(Abi.E_INVALID_HANDLE, lib.generateIpv6(0, ipv6)); assertArrayEquals(sentinel(16), ipv6); } @@ -139,7 +133,7 @@ void stateMachineNextRuleReportsNullHandle() { // Both handles are NULL: the engine rejects the call on the test case before it // dereferences the state machine, so this is a clean error rather than undefined behaviour. long[] out = {7}; - assertEquals(Abi.E_INVALID_HANDLE, lib.stateMachineNextRule(MemorySegment.NULL, 0, out)); + assertEquals(Abi.E_INVALID_HANDLE, lib.stateMachineNextRule(0, 0, out)); // The rule index is read only on success, so a failed call leaves the caller's value alone. assertEquals(7, out[0]); } @@ -147,8 +141,8 @@ void stateMachineNextRuleReportsNullHandle() { @Test void undecodableBlobWithDefaultOutputIsRejected() { RealLibhegel lib = real(); - MemorySegment s = lib.settingsNew(); - MemorySegment[] out = new MemorySegment[1]; + long s = lib.settingsNew(); + long[] out = new long[1]; // A null output callback leaves replay output on stderr; the garbage blob is rejected. assertEquals(Abi.E_INVALID_ARG, lib.testCaseFromBlob(s, "not-a-blob!!!", null, out)); lib.settingsFree(s); @@ -157,11 +151,11 @@ void undecodableBlobWithDefaultOutputIsRejected() { @Test void regexGeneratorAcceptsATextAlphabet() { RealLibhegel lib = real(); - MemorySegment[] alphabet = new MemorySegment[1]; + long[] alphabet = new long[1]; assertEquals( Abi.OK, lib.stringGeneratorText(0, 5, "ascii", 0, Abi.NO_MAX_CODEPOINT, null, null, null, null, alphabet)); - MemorySegment[] regex = new MemorySegment[1]; + long[] regex = new long[1]; assertEquals(Abi.OK, lib.stringGeneratorRegex("[a-z]+", true, alphabet[0], regex)); lib.stringGeneratorFree(regex[0]); lib.stringGeneratorFree(alphabet[0]); diff --git a/justfile b/justfile index 82bb96e..5774be7 100644 --- a/justfile +++ b/justfile @@ -26,6 +26,12 @@ test: coverage: mvn -B verify +test-jna: + mvn -B -pl hegel-jna -am test + +coverage-jna: + mvn -B -pl hegel-jna -am verify + conformance: mvn -B test -Dtest='*Conformance*,*Behaviour*' @@ -40,7 +46,7 @@ check-docs: docs: mvn -B -q javadoc:javadoc - open target/reports/apidocs/index.html + open hegel/target/reports/apidocs/index.html clean: mvn -B -q clean diff --git a/pom.xml b/pom.xml index 2bb0cb1..1ecaca8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,11 +5,11 @@ 4.0.0 dev.hegel - hegel + hegel-parent 0.5.0 - jar + pom - hegel-java + hegel-java parent Property-based testing for Java, built on Hypothesis https://github.com/hegeldev/hegel-java @@ -42,12 +42,17 @@ https://github.com/hegeldev/hegel-java/issues + + hegel + hegel-jna + + UTF-8 22 5.11.4 0.8.14 - 0.32.5 @@ -57,8 +62,9 @@ python3 - + --enable-native-access=ALL-UNNAMED + ${maven.multiModuleProjectDirectory}/shared @@ -88,200 +94,221 @@ - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - - dev.hegel - - - - + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + - - - org.apache.maven.plugins - maven-resources-plugin - 3.3.1 - - - filter-java-templates - generate-sources - - copy-resources - - - ${project.build.directory}/generated-sources/java-templates - - - src/main/java-templates - true - - - - - - + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + filter-java-templates + generate-sources + + copy-resources + + + ${project.build.directory}/generated-sources/java-templates + + + ${hegel.shared}/src/main/java-templates + true + + + + + + - - - org.codehaus.mojo - build-helper-maven-plugin - 3.6.0 - - - add-generated-sources - generate-sources - - add-source - - - - ${project.build.directory}/generated-sources/java-templates - - - - - + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + add-shared-sources + generate-sources + + add-source + + + + ${hegel.shared}/src/main/java + ${project.build.directory}/generated-sources/java-templates + + + + + add-shared-test-sources + generate-test-sources + + add-test-source + + + + ${hegel.shared}/src/test/java + + + + + - - - org.codehaus.mojo - exec-maven-plugin - 3.5.0 - - - fetch-natives - generate-resources - - exec - - - ${hegel.natives.skip} - ${hegel.python} - - ${project.basedir}/scripts/fetch_natives.py - --version - ${libhegel.version} - --out - ${project.build.outputDirectory} - - - - - + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + + fetch-natives + generate-resources + + exec + + + ${hegel.natives.skip} + ${hegel.python} + + ${maven.multiModuleProjectDirectory}/scripts/fetch_natives.py + --version + ${libhegel.version} + --out + ${project.build.outputDirectory} + + + + + - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.2 - - @{argLine} ${hegel.argLine} - - + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + @{argLine} ${hegel.argLine} + + - - org.apache.maven.plugins - maven-invoker-plugin - 3.9.0 - - src/it - ${project.build.directory}/it - true - - - - module-consumer-it - - install - run - - - - + + org.apache.maven.plugins + maven-invoker-plugin + 3.9.0 + + src/it + ${project.build.directory}/it + true + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.46.1 + + + + 2.91.0 + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 + + all,-missing + true + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + check + verify + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 1.00 + + + BRANCH + COVEREDRATIO + 1.00 + + + + + + + + + + + + com.diffplug.spotless spotless-maven-plugin - 2.46.1 + false + + shared/src/main/java/**/*.java + shared/src/main/java-templates/**/*.java + shared/src/test/java/**/*.java + 2.91.0 - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.11.2 - - all,-missing - true - - - - - org.jacoco - jacoco-maven-plugin - ${jacoco.version} - - - prepare-agent - - prepare-agent - - - - report - verify - - report - - - - check - verify - - check - - - - - BUNDLE - - - INSTRUCTION - COVEREDRATIO - 1.00 - - - BRANCH - COVEREDRATIO - 1.00 - - - - - - - - diff --git a/src/main/java-templates/dev/hegel/BuildInfo.java b/shared/src/main/java-templates/dev/hegel/BuildInfo.java similarity index 100% rename from src/main/java-templates/dev/hegel/BuildInfo.java rename to shared/src/main/java-templates/dev/hegel/BuildInfo.java diff --git a/src/main/java/dev/hegel/Abi.java b/shared/src/main/java/dev/hegel/Abi.java similarity index 100% rename from src/main/java/dev/hegel/Abi.java rename to shared/src/main/java/dev/hegel/Abi.java diff --git a/src/main/java/dev/hegel/AssumeRejected.java b/shared/src/main/java/dev/hegel/AssumeRejected.java similarity index 100% rename from src/main/java/dev/hegel/AssumeRejected.java rename to shared/src/main/java/dev/hegel/AssumeRejected.java diff --git a/src/main/java/dev/hegel/Backend.java b/shared/src/main/java/dev/hegel/Backend.java similarity index 100% rename from src/main/java/dev/hegel/Backend.java rename to shared/src/main/java/dev/hegel/Backend.java diff --git a/src/main/java/dev/hegel/DataSource.java b/shared/src/main/java/dev/hegel/DataSource.java similarity index 100% rename from src/main/java/dev/hegel/DataSource.java rename to shared/src/main/java/dev/hegel/DataSource.java diff --git a/src/main/java/dev/hegel/Database.java b/shared/src/main/java/dev/hegel/Database.java similarity index 100% rename from src/main/java/dev/hegel/Database.java rename to shared/src/main/java/dev/hegel/Database.java diff --git a/src/main/java/dev/hegel/Engine.java b/shared/src/main/java/dev/hegel/Engine.java similarity index 96% rename from src/main/java/dev/hegel/Engine.java rename to shared/src/main/java/dev/hegel/Engine.java index f8038f2..3608f66 100644 --- a/src/main/java/dev/hegel/Engine.java +++ b/shared/src/main/java/dev/hegel/Engine.java @@ -20,7 +20,7 @@ private Engine() {} static synchronized Libhegel get() { if (instance == null) { Path path = LibraryLoader.fromEnvironment().resolve(); - RealLibhegel lib = new RealLibhegel(path); + Libhegel lib = LibhegelBackend.open(path); LibraryLoader.warnOnVersionMismatch(lib, LibraryLoader.targetEngineVersion(), System.err); instance = lib; } diff --git a/src/main/java/dev/hegel/Generated.java b/shared/src/main/java/dev/hegel/Generated.java similarity index 100% rename from src/main/java/dev/hegel/Generated.java rename to shared/src/main/java/dev/hegel/Generated.java diff --git a/src/main/java/dev/hegel/Generator.java b/shared/src/main/java/dev/hegel/Generator.java similarity index 100% rename from src/main/java/dev/hegel/Generator.java rename to shared/src/main/java/dev/hegel/Generator.java diff --git a/src/main/java/dev/hegel/Generators.java b/shared/src/main/java/dev/hegel/Generators.java similarity index 100% rename from src/main/java/dev/hegel/Generators.java rename to shared/src/main/java/dev/hegel/Generators.java diff --git a/src/main/java/dev/hegel/HealthCheck.java b/shared/src/main/java/dev/hegel/HealthCheck.java similarity index 100% rename from src/main/java/dev/hegel/HealthCheck.java rename to shared/src/main/java/dev/hegel/HealthCheck.java diff --git a/src/main/java/dev/hegel/HealthCheckFailure.java b/shared/src/main/java/dev/hegel/HealthCheckFailure.java similarity index 100% rename from src/main/java/dev/hegel/HealthCheckFailure.java rename to shared/src/main/java/dev/hegel/HealthCheckFailure.java diff --git a/src/main/java/dev/hegel/Hegel.java b/shared/src/main/java/dev/hegel/Hegel.java similarity index 100% rename from src/main/java/dev/hegel/Hegel.java rename to shared/src/main/java/dev/hegel/Hegel.java diff --git a/src/main/java/dev/hegel/HegelException.java b/shared/src/main/java/dev/hegel/HegelException.java similarity index 100% rename from src/main/java/dev/hegel/HegelException.java rename to shared/src/main/java/dev/hegel/HegelException.java diff --git a/src/main/java/dev/hegel/HegelTest.java b/shared/src/main/java/dev/hegel/HegelTest.java similarity index 100% rename from src/main/java/dev/hegel/HegelTest.java rename to shared/src/main/java/dev/hegel/HegelTest.java diff --git a/src/main/java/dev/hegel/HegelTestExtension.java b/shared/src/main/java/dev/hegel/HegelTestExtension.java similarity index 100% rename from src/main/java/dev/hegel/HegelTestExtension.java rename to shared/src/main/java/dev/hegel/HegelTestExtension.java diff --git a/src/main/java/dev/hegel/Invariant.java b/shared/src/main/java/dev/hegel/Invariant.java similarity index 100% rename from src/main/java/dev/hegel/Invariant.java rename to shared/src/main/java/dev/hegel/Invariant.java diff --git a/src/main/java/dev/hegel/Libhegel.java b/shared/src/main/java/dev/hegel/Libhegel.java similarity index 53% rename from src/main/java/dev/hegel/Libhegel.java rename to shared/src/main/java/dev/hegel/Libhegel.java index bb2860b..5eb8a84 100644 --- a/src/main/java/dev/hegel/Libhegel.java +++ b/shared/src/main/java/dev/hegel/Libhegel.java @@ -1,6 +1,5 @@ package dev.hegel; -import java.lang.foreign.MemorySegment; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -12,12 +11,14 @@ * *

Modelled as an interface so tests can substitute a fake binding that returns chosen return * codes, exercising every error path without the real engine. The production implementation is - * {@link RealLibhegel}, which drives libhegel over the Foreign Function and Memory API. + * {@code RealLibhegel} (Foreign Function and Memory API, in {@code hegel}) or {@code JnaLibhegel} + * (JNA, in {@code hegel-jna}). * *

Opaque handles ({@code hegel_settings_t*}, {@code hegel_run_t*}, {@code hegel_test_case_t*}, - * {@code hegel_run_result_t*}, {@code hegel_string_generator_t*}) are passed as {@link - * MemorySegment}; callers treat them as opaque and never dereference them. Handles are caller-owned: - * every handle a method returns must be released with its matching {@code *Free} method. + * {@code hegel_run_result_t*}, {@code hegel_string_generator_t*}) are passed as raw addresses + * ({@code long}; {@code 0} is NULL); callers treat them as opaque and never dereference them. + * Handles are caller-owned: every handle a method returns must be released with its matching + * {@code *Free} method. * *

Two calling conventions coexist here, mirroring how the frontend consumes the ABI: * @@ -37,34 +38,34 @@ */ interface Libhegel { // Settings. Setters cannot fail with this binding's inputs; implementations throw on non-OK. - MemorySegment settingsNew(); + long settingsNew(); - void settingsFree(MemorySegment s); + void settingsFree(long s); - void settingsMode(MemorySegment s, int mode); + void settingsMode(long s, int mode); - void settingsBackend(MemorySegment s, int backend); + void settingsBackend(long s, int backend); - void settingsTestCases(MemorySegment s, long n); + void settingsTestCases(long s, long n); - void settingsVerbosity(MemorySegment s, int v); + void settingsVerbosity(long s, int v); - void settingsSeed(MemorySegment s, long seed, boolean hasSeed); + void settingsSeed(long s, long seed, boolean hasSeed); - void settingsDerandomize(MemorySegment s, boolean derandomize); + void settingsDerandomize(long s, boolean derandomize); - void settingsReportMultipleFailures(MemorySegment s, boolean yes); + void settingsReportMultipleFailures(long s, boolean yes); /** * {@code path == null} leaves the engine default; {@code ""} disables; otherwise sets the dir. */ - void settingsDatabase(MemorySegment s, String path); + void settingsDatabase(long s, String path); - void settingsDatabaseKey(MemorySegment s, String key); + void settingsDatabaseKey(long s, String key); - void settingsPhases(MemorySegment s, int mask); + void settingsPhases(long s, int mask); - void settingsSuppressHealthCheck(MemorySegment s, int mask); + void settingsSuppressHealthCheck(long s, int mask); // Run lifecycle. @@ -73,17 +74,17 @@ interface Libhegel { * output}; {@code null} leaves it on stderr. The callback stays registered until {@link * #runFree}. */ - MemorySegment runStart(MemorySegment settings, Consumer output); + long runStart(long settings, Consumer output); - /** The next test-case handle, or {@code NULL} once the run is finished. */ - MemorySegment nextTestCase(MemorySegment run); + /** The next test-case handle, or {@code 0} once the run is finished. */ + long nextTestCase(long run); /** A caller-owned snapshot of the finished run's result; release with {@link #runResultFree}. */ - MemorySegment runResult(MemorySegment run); + long runResult(long run); - void runResultFree(MemorySegment result); + void runResultFree(long result); - void runFree(MemorySegment run); + void runFree(long run); /** * Replay a base64 reproduce blob as a standalone test case. Returns the raw rc ({@link @@ -91,17 +92,17 @@ interface Libhegel { * caller-owned handle. {@code output} has the same contract as in {@link #runStart} but need not * outlive the call. */ - int testCaseFromBlob(MemorySegment settings, String blob, Consumer output, MemorySegment[] out); + int testCaseFromBlob(long settings, String blob, Consumer output, long[] out); - void testCaseFree(MemorySegment tc); + void testCaseFree(long tc); // Per-test-case draws. Each returns the raw rc. - int generateBoolean(MemorySegment tc, double p, boolean[] out); + int generateBoolean(long tc, double p, boolean[] out); - int generateInteger(MemorySegment tc, long min, long max, long[] out); + int generateInteger(long tc, long min, long max, long[] out); int generateFloat( - MemorySegment tc, + long tc, int width, double min, double max, @@ -112,25 +113,25 @@ int generateFloat( double smallestNonzeroMagnitude, double[] out); - int generateBytes(MemorySegment tc, long minSize, long maxSize, byte[][] out); + int generateBytes(long tc, long minSize, long maxSize, byte[][] out); - int generateString(MemorySegment tc, MemorySegment generator, String[] out); + int generateString(long tc, long generator, String[] out); - int generateDate(MemorySegment tc, LocalDate min, LocalDate max, LocalDate[] out); + int generateDate(long tc, LocalDate min, LocalDate max, LocalDate[] out); /** Time bounds and results are at microsecond resolution (the engine's granularity). */ - int generateTime(MemorySegment tc, LocalTime min, LocalTime max, LocalTime[] out); + int generateTime(long tc, LocalTime min, LocalTime max, LocalTime[] out); - int generateDatetime(MemorySegment tc, LocalDateTime min, LocalDateTime max, LocalDateTime[] out); + int generateDatetime(long tc, LocalDateTime min, LocalDateTime max, LocalDateTime[] out); /** On OK writes the UUID's 16 big-endian bytes into {@code out16}. */ - int generateUuid(MemorySegment tc, int version, boolean hasVersion, byte[] out16); + int generateUuid(long tc, int version, boolean hasVersion, byte[] out16); /** On OK writes the address's 4 network-order bytes into {@code out4}. */ - int generateIpv4(MemorySegment tc, byte[] out4); + int generateIpv4(long tc, byte[] out4); /** On OK writes the address's 16 network-order bytes into {@code out16}. */ - int generateIpv6(MemorySegment tc, byte[] out16); + int generateIpv6(long tc, byte[] out16); // String-generator handles. Constructors return the raw rc (INVALID_ARG for a configuration // that is rejected, e.g. an empty alphabet with max_size > 0); handles are released with @@ -145,57 +146,57 @@ int stringGeneratorText( List excludeCategories, String includeCharacters, String excludeCharacters, - MemorySegment[] out); + long[] out); - int stringGeneratorRegex(String pattern, boolean fullmatch, MemorySegment alphabet, MemorySegment[] out); + int stringGeneratorRegex(String pattern, boolean fullmatch, long alphabet, long[] out); - int stringGeneratorEmail(MemorySegment[] out); + int stringGeneratorEmail(long[] out); - int stringGeneratorUrl(MemorySegment[] out); + int stringGeneratorUrl(long[] out); - int stringGeneratorDomain(long maxLength, MemorySegment[] out); + int stringGeneratorDomain(long maxLength, long[] out); - void stringGeneratorFree(MemorySegment generator); + void stringGeneratorFree(long generator); // Structure: spans, collections, pools, state machines. Each returns the raw rc. - int startSpan(MemorySegment tc, long label); + int startSpan(long tc, long label); - int stopSpan(MemorySegment tc, boolean discard); + int stopSpan(long tc, boolean discard); - int newCollection(MemorySegment tc, long minSize, long maxSize, long[] outId); + int newCollection(long tc, long minSize, long maxSize, long[] outId); - int collectionMore(MemorySegment tc, long id, boolean[] outMore); + int collectionMore(long tc, long id, boolean[] outMore); - int collectionReject(MemorySegment tc, long id, String why); + int collectionReject(long tc, long id, String why); - int newPool(MemorySegment tc, long[] outId); + int newPool(long tc, long[] outId); - int poolAdd(MemorySegment tc, long poolId, long[] outVariableId); + int poolAdd(long tc, long poolId, long[] outVariableId); - int poolGenerate(MemorySegment tc, long poolId, boolean consume, long[] outVariableId); + int poolGenerate(long tc, long poolId, boolean consume, long[] outVariableId); - int newStateMachine(MemorySegment tc, List ruleNames, List invariantNames, long[] outId); + int newStateMachine(long tc, List ruleNames, List invariantNames, long[] outId); /** {@code outRuleIndex[0]} receives the rule index, or {@link Abi#STATE_MACHINE_DONE}. */ - int stateMachineNextRule(MemorySegment tc, long stateMachineId, long[] outRuleIndex); + int stateMachineNextRule(long tc, long stateMachineId, long[] outRuleIndex); - int target(MemorySegment tc, double value, String label); + int target(long tc, double value, String label); - int markComplete(MemorySegment tc, int status, String origin); + int markComplete(long tc, int status, String origin); // Results. - int runResultStatus(MemorySegment result); + int runResultStatus(long result); /** The run-level error message, or {@code null} when the run completed normally. */ - String runResultError(MemorySegment result); + String runResultError(long result); - long runResultFailureCount(MemorySegment result); + long runResultFailureCount(long result); /** * The reproduce blob of the {@code index}-th distinct failure, or {@code null} if libhegel * produced none for it. */ - String failureBlob(MemorySegment result, long index); + String failureBlob(long result, long index); // Diagnostics. String lastErrorMessage(); diff --git a/src/main/java/dev/hegel/LibraryLoader.java b/shared/src/main/java/dev/hegel/LibraryLoader.java similarity index 100% rename from src/main/java/dev/hegel/LibraryLoader.java rename to shared/src/main/java/dev/hegel/LibraryLoader.java diff --git a/src/main/java/dev/hegel/LiveDataSource.java b/shared/src/main/java/dev/hegel/LiveDataSource.java similarity index 94% rename from src/main/java/dev/hegel/LiveDataSource.java rename to shared/src/main/java/dev/hegel/LiveDataSource.java index ad3a641..eb45e96 100644 --- a/src/main/java/dev/hegel/LiveDataSource.java +++ b/shared/src/main/java/dev/hegel/LiveDataSource.java @@ -1,6 +1,5 @@ package dev.hegel; -import java.lang.foreign.MemorySegment; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -17,10 +16,10 @@ */ final class LiveDataSource implements DataSource { private final Libhegel lib; - private final MemorySegment tc; + private final long tc; private boolean aborted; - LiveDataSource(Libhegel lib, MemorySegment tc) { + LiveDataSource(Libhegel lib, long tc) { this.lib = lib; this.tc = tc; } @@ -113,7 +112,7 @@ public byte[] generateBytes(long minSize, long maxSize) { public String generateString(StringGeneratorHandle generator) { checkLive(); String[] out = new String[1]; - translate(lib.generateString(tc, generator.segment, out), "generate_string"); + translate(lib.generateString(tc, generator.handle, out), "generate_string"); return out[0]; } @@ -183,7 +182,7 @@ public StringGeneratorHandle textGenerator( String includeCharacters, String excludeCharacters) { checkLive(); - MemorySegment[] out = new MemorySegment[1]; + long[] out = new long[1]; translate( lib.stringGeneratorText( minSize, @@ -203,9 +202,9 @@ public StringGeneratorHandle textGenerator( @Override public StringGeneratorHandle regexGenerator(String pattern, boolean fullmatch, StringGeneratorHandle alphabet) { checkLive(); - MemorySegment[] out = new MemorySegment[1]; + long[] out = new long[1]; translate( - lib.stringGeneratorRegex(pattern, fullmatch, alphabet == null ? null : alphabet.segment, out), + lib.stringGeneratorRegex(pattern, fullmatch, alphabet == null ? 0 : alphabet.handle, out), "string_generator_regex"); return new StringGeneratorHandle(lib, out[0]); } @@ -213,7 +212,7 @@ public StringGeneratorHandle regexGenerator(String pattern, boolean fullmatch, S @Override public StringGeneratorHandle emailGenerator() { checkLive(); - MemorySegment[] out = new MemorySegment[1]; + long[] out = new long[1]; translate(lib.stringGeneratorEmail(out), "string_generator_email"); return new StringGeneratorHandle(lib, out[0]); } @@ -221,7 +220,7 @@ public StringGeneratorHandle emailGenerator() { @Override public StringGeneratorHandle urlGenerator() { checkLive(); - MemorySegment[] out = new MemorySegment[1]; + long[] out = new long[1]; translate(lib.stringGeneratorUrl(out), "string_generator_url"); return new StringGeneratorHandle(lib, out[0]); } @@ -229,7 +228,7 @@ public StringGeneratorHandle urlGenerator() { @Override public StringGeneratorHandle domainGenerator(long maxLength) { checkLive(); - MemorySegment[] out = new MemorySegment[1]; + long[] out = new long[1]; translate(lib.stringGeneratorDomain(maxLength, out), "string_generator_domain"); return new StringGeneratorHandle(lib, out[0]); } diff --git a/src/main/java/dev/hegel/Mode.java b/shared/src/main/java/dev/hegel/Mode.java similarity index 100% rename from src/main/java/dev/hegel/Mode.java rename to shared/src/main/java/dev/hegel/Mode.java diff --git a/src/main/java/dev/hegel/OptBoolean.java b/shared/src/main/java/dev/hegel/OptBoolean.java similarity index 100% rename from src/main/java/dev/hegel/OptBoolean.java rename to shared/src/main/java/dev/hegel/OptBoolean.java diff --git a/src/main/java/dev/hegel/Phase.java b/shared/src/main/java/dev/hegel/Phase.java similarity index 100% rename from src/main/java/dev/hegel/Phase.java rename to shared/src/main/java/dev/hegel/Phase.java diff --git a/src/main/java/dev/hegel/Pool.java b/shared/src/main/java/dev/hegel/Pool.java similarity index 100% rename from src/main/java/dev/hegel/Pool.java rename to shared/src/main/java/dev/hegel/Pool.java diff --git a/src/main/java/dev/hegel/Rule.java b/shared/src/main/java/dev/hegel/Rule.java similarity index 100% rename from src/main/java/dev/hegel/Rule.java rename to shared/src/main/java/dev/hegel/Rule.java diff --git a/src/main/java/dev/hegel/Runner.java b/shared/src/main/java/dev/hegel/Runner.java similarity index 89% rename from src/main/java/dev/hegel/Runner.java rename to shared/src/main/java/dev/hegel/Runner.java index 758ac7a..5c9a02f 100644 --- a/src/main/java/dev/hegel/Runner.java +++ b/shared/src/main/java/dev/hegel/Runner.java @@ -1,7 +1,6 @@ package dev.hegel; import java.io.PrintStream; -import java.lang.foreign.MemorySegment; import java.util.Map; import java.util.function.Consumer; @@ -43,26 +42,26 @@ static void run(Settings settings, Consumer body) { static void run( Libhegel lib, Settings settings, Consumer body, Map env, PrintStream out) { - MemorySegment s = lib.settingsNew(); + long s = lib.settingsNew(); try { applySettings(lib, s, settings, env); if (settings.reproduceFailure != null) { throw replayBlob(lib, s, settings.reproduceFailure, body, out); } - MemorySegment run = lib.runStart(s, out::println); + long run = lib.runStart(s, out::println); try { if (settings.mode == Mode.SINGLE_TEST_CASE) { driveSingleCase(lib, run, body, out); return; } while (true) { - MemorySegment tc = lib.nextTestCase(run); + long tc = lib.nextTestCase(run); if (isNull(tc)) { break; } driveOneCase(lib, tc, false, body, out); } - MemorySegment result = lib.runResult(run); + long result = lib.runResult(run); try { finish(lib, s, result, settings, body, out); } finally { @@ -78,12 +77,7 @@ static void run( /** Translate a drained run's result into a normal return or the failure to raise. */ private static void finish( - Libhegel lib, - MemorySegment s, - MemorySegment result, - Settings settings, - Consumer body, - PrintStream out) { + Libhegel lib, long s, long result, Settings settings, Consumer body, PrintStream out) { switch (lib.runResultStatus(result)) { case Abi.RUN_STATUS_PASSED: return; @@ -106,12 +100,7 @@ private static void finish( * bugs. */ private static AssertionError replayFailures( - Libhegel lib, - MemorySegment s, - MemorySegment result, - Settings settings, - Consumer body, - PrintStream out) { + Libhegel lib, long s, long result, Settings settings, Consumer body, PrintStream out) { long count = lib.runResultFailureCount(result); boolean multiple = count > 1; if (multiple) { @@ -126,7 +115,7 @@ private static AssertionError replayFailures( if (blob == null) { throw new HegelException("internal error: failure " + i + " carries no reproduce blob"); } - MemorySegment[] tcOut = new MemorySegment[1]; + long[] tcOut = new long[1]; int rc = lib.testCaseFromBlob(s, blob, out::println, tcOut); if (rc != Abi.OK) { throw new HegelException( @@ -163,8 +152,8 @@ private static AssertionError replayFailures( * verdict is that case's outcome. There is no shrinking or replay, so a failure re-raises * straight away. */ - private static void driveSingleCase(Libhegel lib, MemorySegment run, Consumer body, PrintStream out) { - MemorySegment tc = lib.nextTestCase(run); + private static void driveSingleCase(Libhegel lib, long run, Consumer body, PrintStream out) { + long tc = lib.nextTestCase(run); if (isNull(tc)) { throw new HegelException("hegel_next_test_case produced no case for a single-test-case run"); } @@ -180,8 +169,8 @@ private static void driveSingleCase(Libhegel lib, MemorySegment run, Consumer body, PrintStream out) { - MemorySegment[] tcOut = new MemorySegment[1]; + Libhegel lib, long s, String blob, Consumer body, PrintStream out) { + long[] tcOut = new long[1]; int rc = lib.testCaseFromBlob(s, blob, out::println, tcOut); if (rc != Abi.OK) { return new HegelException("reproduceFailure: the supplied blob is not valid (rc=" @@ -202,8 +191,7 @@ private static RuntimeException replayBlob( * exception that made the case interesting, or {@code null} for any other outcome. With {@code * reporting} enabled the case's draws and notes are printed to {@code out}. */ - static Throwable driveOneCase( - Libhegel lib, MemorySegment tc, boolean reporting, Consumer body, PrintStream out) { + static Throwable driveOneCase(Libhegel lib, long tc, boolean reporting, Consumer body, PrintStream out) { try { TestCase testCase = new TestCase(new LiveDataSource(lib, tc), reporting, out); int status; @@ -249,7 +237,7 @@ private static RuntimeException asUnchecked(Throwable t) { return (RuntimeException) t; } - static void applySettings(Libhegel lib, MemorySegment s, Settings st, Map env) { + static void applySettings(Libhegel lib, long s, Settings st, Map env) { boolean ci = Settings.isCi(env); lib.settingsTestCases(s, st.testCases); lib.settingsVerbosity(s, st.verbosity.code); @@ -316,7 +304,7 @@ private static String nullToEmpty(String s) { return s == null ? "" : s; } - static boolean isNull(MemorySegment seg) { - return seg == null || seg.address() == 0; + static boolean isNull(long handle) { + return handle == 0; } } diff --git a/src/main/java/dev/hegel/Settings.java b/shared/src/main/java/dev/hegel/Settings.java similarity index 100% rename from src/main/java/dev/hegel/Settings.java rename to shared/src/main/java/dev/hegel/Settings.java diff --git a/src/main/java/dev/hegel/Stateful.java b/shared/src/main/java/dev/hegel/Stateful.java similarity index 100% rename from src/main/java/dev/hegel/Stateful.java rename to shared/src/main/java/dev/hegel/Stateful.java diff --git a/src/main/java/dev/hegel/StopTest.java b/shared/src/main/java/dev/hegel/StopTest.java similarity index 100% rename from src/main/java/dev/hegel/StopTest.java rename to shared/src/main/java/dev/hegel/StopTest.java diff --git a/src/main/java/dev/hegel/StringGeneratorHandle.java b/shared/src/main/java/dev/hegel/StringGeneratorHandle.java similarity index 75% rename from src/main/java/dev/hegel/StringGeneratorHandle.java rename to shared/src/main/java/dev/hegel/StringGeneratorHandle.java index b31ac0c..e6063c1 100644 --- a/src/main/java/dev/hegel/StringGeneratorHandle.java +++ b/shared/src/main/java/dev/hegel/StringGeneratorHandle.java @@ -1,6 +1,5 @@ package dev.hegel; -import java.lang.foreign.MemorySegment; import java.lang.ref.Cleaner; /** @@ -19,22 +18,22 @@ public final class StringGeneratorHandle { private static final Cleaner CLEANER = Cleaner.create(); final Libhegel lib; - final MemorySegment segment; + final long handle; - StringGeneratorHandle(Libhegel lib, MemorySegment segment) { + StringGeneratorHandle(Libhegel lib, long handle) { this.lib = lib; - this.segment = segment; - CLEANER.register(this, new Free(lib, segment)); + this.handle = handle; + CLEANER.register(this, new Free(lib, handle)); } /** * The deferred release of the engine-side allocation. A record (not a lambda capturing {@code * this}) so the cleanable never keeps its own handle reachable. */ - record Free(Libhegel lib, MemorySegment segment) implements Runnable { + record Free(Libhegel lib, long handle) implements Runnable { @Override public void run() { - lib.stringGeneratorFree(segment); + lib.stringGeneratorFree(handle); } } } diff --git a/src/main/java/dev/hegel/TestCase.java b/shared/src/main/java/dev/hegel/TestCase.java similarity index 100% rename from src/main/java/dev/hegel/TestCase.java rename to shared/src/main/java/dev/hegel/TestCase.java diff --git a/src/main/java/dev/hegel/Tuple2.java b/shared/src/main/java/dev/hegel/Tuple2.java similarity index 100% rename from src/main/java/dev/hegel/Tuple2.java rename to shared/src/main/java/dev/hegel/Tuple2.java diff --git a/src/main/java/dev/hegel/Tuple3.java b/shared/src/main/java/dev/hegel/Tuple3.java similarity index 100% rename from src/main/java/dev/hegel/Tuple3.java rename to shared/src/main/java/dev/hegel/Tuple3.java diff --git a/src/main/java/dev/hegel/Tuple4.java b/shared/src/main/java/dev/hegel/Tuple4.java similarity index 100% rename from src/main/java/dev/hegel/Tuple4.java rename to shared/src/main/java/dev/hegel/Tuple4.java diff --git a/src/main/java/dev/hegel/Tuple5.java b/shared/src/main/java/dev/hegel/Tuple5.java similarity index 100% rename from src/main/java/dev/hegel/Tuple5.java rename to shared/src/main/java/dev/hegel/Tuple5.java diff --git a/src/main/java/dev/hegel/Tuple6.java b/shared/src/main/java/dev/hegel/Tuple6.java similarity index 100% rename from src/main/java/dev/hegel/Tuple6.java rename to shared/src/main/java/dev/hegel/Tuple6.java diff --git a/src/main/java/dev/hegel/Tuple7.java b/shared/src/main/java/dev/hegel/Tuple7.java similarity index 100% rename from src/main/java/dev/hegel/Tuple7.java rename to shared/src/main/java/dev/hegel/Tuple7.java diff --git a/src/main/java/dev/hegel/Tuple8.java b/shared/src/main/java/dev/hegel/Tuple8.java similarity index 100% rename from src/main/java/dev/hegel/Tuple8.java rename to shared/src/main/java/dev/hegel/Tuple8.java diff --git a/src/main/java/dev/hegel/Verbosity.java b/shared/src/main/java/dev/hegel/Verbosity.java similarity index 100% rename from src/main/java/dev/hegel/Verbosity.java rename to shared/src/main/java/dev/hegel/Verbosity.java diff --git a/src/main/java/dev/hegel/generators/BinaryGenerator.java b/shared/src/main/java/dev/hegel/generators/BinaryGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/BinaryGenerator.java rename to shared/src/main/java/dev/hegel/generators/BinaryGenerator.java diff --git a/src/main/java/dev/hegel/generators/BooleanGenerator.java b/shared/src/main/java/dev/hegel/generators/BooleanGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/BooleanGenerator.java rename to shared/src/main/java/dev/hegel/generators/BooleanGenerator.java diff --git a/src/main/java/dev/hegel/generators/CompositeGenerator.java b/shared/src/main/java/dev/hegel/generators/CompositeGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/CompositeGenerator.java rename to shared/src/main/java/dev/hegel/generators/CompositeGenerator.java diff --git a/src/main/java/dev/hegel/generators/ConstantGenerator.java b/shared/src/main/java/dev/hegel/generators/ConstantGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/ConstantGenerator.java rename to shared/src/main/java/dev/hegel/generators/ConstantGenerator.java diff --git a/src/main/java/dev/hegel/generators/DateGenerator.java b/shared/src/main/java/dev/hegel/generators/DateGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/DateGenerator.java rename to shared/src/main/java/dev/hegel/generators/DateGenerator.java diff --git a/src/main/java/dev/hegel/generators/DateTimeGenerator.java b/shared/src/main/java/dev/hegel/generators/DateTimeGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/DateTimeGenerator.java rename to shared/src/main/java/dev/hegel/generators/DateTimeGenerator.java diff --git a/src/main/java/dev/hegel/generators/Deferred.java b/shared/src/main/java/dev/hegel/generators/Deferred.java similarity index 100% rename from src/main/java/dev/hegel/generators/Deferred.java rename to shared/src/main/java/dev/hegel/generators/Deferred.java diff --git a/src/main/java/dev/hegel/generators/Derive.java b/shared/src/main/java/dev/hegel/generators/Derive.java similarity index 100% rename from src/main/java/dev/hegel/generators/Derive.java rename to shared/src/main/java/dev/hegel/generators/Derive.java diff --git a/src/main/java/dev/hegel/generators/DomainGenerator.java b/shared/src/main/java/dev/hegel/generators/DomainGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/DomainGenerator.java rename to shared/src/main/java/dev/hegel/generators/DomainGenerator.java diff --git a/src/main/java/dev/hegel/generators/DoubleGenerator.java b/shared/src/main/java/dev/hegel/generators/DoubleGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/DoubleGenerator.java rename to shared/src/main/java/dev/hegel/generators/DoubleGenerator.java diff --git a/src/main/java/dev/hegel/generators/DurationGenerator.java b/shared/src/main/java/dev/hegel/generators/DurationGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/DurationGenerator.java rename to shared/src/main/java/dev/hegel/generators/DurationGenerator.java diff --git a/src/main/java/dev/hegel/generators/EmailGenerator.java b/shared/src/main/java/dev/hegel/generators/EmailGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/EmailGenerator.java rename to shared/src/main/java/dev/hegel/generators/EmailGenerator.java diff --git a/src/main/java/dev/hegel/generators/FilteredGenerator.java b/shared/src/main/java/dev/hegel/generators/FilteredGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/FilteredGenerator.java rename to shared/src/main/java/dev/hegel/generators/FilteredGenerator.java diff --git a/src/main/java/dev/hegel/generators/FlatMappedGenerator.java b/shared/src/main/java/dev/hegel/generators/FlatMappedGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/FlatMappedGenerator.java rename to shared/src/main/java/dev/hegel/generators/FlatMappedGenerator.java diff --git a/src/main/java/dev/hegel/generators/FloatGenerator.java b/shared/src/main/java/dev/hegel/generators/FloatGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/FloatGenerator.java rename to shared/src/main/java/dev/hegel/generators/FloatGenerator.java diff --git a/src/main/java/dev/hegel/generators/Floats.java b/shared/src/main/java/dev/hegel/generators/Floats.java similarity index 100% rename from src/main/java/dev/hegel/generators/Floats.java rename to shared/src/main/java/dev/hegel/generators/Floats.java diff --git a/src/main/java/dev/hegel/generators/HandleCache.java b/shared/src/main/java/dev/hegel/generators/HandleCache.java similarity index 100% rename from src/main/java/dev/hegel/generators/HandleCache.java rename to shared/src/main/java/dev/hegel/generators/HandleCache.java diff --git a/src/main/java/dev/hegel/generators/IntegerGenerator.java b/shared/src/main/java/dev/hegel/generators/IntegerGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/IntegerGenerator.java rename to shared/src/main/java/dev/hegel/generators/IntegerGenerator.java diff --git a/src/main/java/dev/hegel/generators/IpAddressGenerator.java b/shared/src/main/java/dev/hegel/generators/IpAddressGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/IpAddressGenerator.java rename to shared/src/main/java/dev/hegel/generators/IpAddressGenerator.java diff --git a/src/main/java/dev/hegel/generators/ListGenerator.java b/shared/src/main/java/dev/hegel/generators/ListGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/ListGenerator.java rename to shared/src/main/java/dev/hegel/generators/ListGenerator.java diff --git a/src/main/java/dev/hegel/generators/LongGenerator.java b/shared/src/main/java/dev/hegel/generators/LongGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/LongGenerator.java rename to shared/src/main/java/dev/hegel/generators/LongGenerator.java diff --git a/src/main/java/dev/hegel/generators/MapGenerator.java b/shared/src/main/java/dev/hegel/generators/MapGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/MapGenerator.java rename to shared/src/main/java/dev/hegel/generators/MapGenerator.java diff --git a/src/main/java/dev/hegel/generators/MappedGenerator.java b/shared/src/main/java/dev/hegel/generators/MappedGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/MappedGenerator.java rename to shared/src/main/java/dev/hegel/generators/MappedGenerator.java diff --git a/src/main/java/dev/hegel/generators/OneOfGenerator.java b/shared/src/main/java/dev/hegel/generators/OneOfGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/OneOfGenerator.java rename to shared/src/main/java/dev/hegel/generators/OneOfGenerator.java diff --git a/src/main/java/dev/hegel/generators/RecordGenerator.java b/shared/src/main/java/dev/hegel/generators/RecordGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/RecordGenerator.java rename to shared/src/main/java/dev/hegel/generators/RecordGenerator.java diff --git a/src/main/java/dev/hegel/generators/RegexGenerator.java b/shared/src/main/java/dev/hegel/generators/RegexGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/RegexGenerator.java rename to shared/src/main/java/dev/hegel/generators/RegexGenerator.java diff --git a/src/main/java/dev/hegel/generators/SampledFromGenerator.java b/shared/src/main/java/dev/hegel/generators/SampledFromGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/SampledFromGenerator.java rename to shared/src/main/java/dev/hegel/generators/SampledFromGenerator.java diff --git a/src/main/java/dev/hegel/generators/SetGenerator.java b/shared/src/main/java/dev/hegel/generators/SetGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/SetGenerator.java rename to shared/src/main/java/dev/hegel/generators/SetGenerator.java diff --git a/src/main/java/dev/hegel/generators/Sizes.java b/shared/src/main/java/dev/hegel/generators/Sizes.java similarity index 100% rename from src/main/java/dev/hegel/generators/Sizes.java rename to shared/src/main/java/dev/hegel/generators/Sizes.java diff --git a/src/main/java/dev/hegel/generators/TextGenerator.java b/shared/src/main/java/dev/hegel/generators/TextGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/TextGenerator.java rename to shared/src/main/java/dev/hegel/generators/TextGenerator.java diff --git a/src/main/java/dev/hegel/generators/TimeGenerator.java b/shared/src/main/java/dev/hegel/generators/TimeGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/TimeGenerator.java rename to shared/src/main/java/dev/hegel/generators/TimeGenerator.java diff --git a/src/main/java/dev/hegel/generators/TupleGenerator.java b/shared/src/main/java/dev/hegel/generators/TupleGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/TupleGenerator.java rename to shared/src/main/java/dev/hegel/generators/TupleGenerator.java diff --git a/src/main/java/dev/hegel/generators/UrlGenerator.java b/shared/src/main/java/dev/hegel/generators/UrlGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/UrlGenerator.java rename to shared/src/main/java/dev/hegel/generators/UrlGenerator.java diff --git a/src/main/java/dev/hegel/generators/UuidGenerator.java b/shared/src/main/java/dev/hegel/generators/UuidGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/UuidGenerator.java rename to shared/src/main/java/dev/hegel/generators/UuidGenerator.java diff --git a/src/main/java/dev/hegel/generators/ZoneOffsetGenerator.java b/shared/src/main/java/dev/hegel/generators/ZoneOffsetGenerator.java similarity index 100% rename from src/main/java/dev/hegel/generators/ZoneOffsetGenerator.java rename to shared/src/main/java/dev/hegel/generators/ZoneOffsetGenerator.java diff --git a/src/main/java/dev/hegel/package-info.java b/shared/src/main/java/dev/hegel/package-info.java similarity index 100% rename from src/main/java/dev/hegel/package-info.java rename to shared/src/main/java/dev/hegel/package-info.java diff --git a/src/test/java/dev/hegel/BindingErrorPathsTest.java b/shared/src/test/java/dev/hegel/BindingErrorPathsTest.java similarity index 100% rename from src/test/java/dev/hegel/BindingErrorPathsTest.java rename to shared/src/test/java/dev/hegel/BindingErrorPathsTest.java diff --git a/src/test/java/dev/hegel/ConformanceTest.java b/shared/src/test/java/dev/hegel/ConformanceTest.java similarity index 95% rename from src/test/java/dev/hegel/ConformanceTest.java rename to shared/src/test/java/dev/hegel/ConformanceTest.java index 7aeed20..3ef5ff1 100644 --- a/src/test/java/dev/hegel/ConformanceTest.java +++ b/shared/src/test/java/dev/hegel/ConformanceTest.java @@ -85,9 +85,13 @@ void floatsRespectBoundsAndSpecials() { @Test void textRespectsLengthAndCharacters() { assertAllExamples(text().minSize(1).maxSize(3), s -> cp(s) >= 1 && cp(s) <= 3); + // The engine's Unicode tables may be newer than this JVM's (e.g. JDK 17 knows Unicode 13): + // a codepoint the JVM has no data for reads as UNASSIGNED and cannot be classified, so only + // codepoints the JVM knows are held to the category. assertAllExamples( text().categories("Nd").minSize(1).maxSize(2), - s -> s.codePoints().allMatch(Character::isDigit)); + s -> s.codePoints() + .allMatch(c -> Character.isDigit(c) || Character.getType(c) == Character.UNASSIGNED)); assertAllExamples( text().codepoints('a', 'z').minSize(1).maxSize(2), s -> s.codePoints().allMatch(c -> c >= 'a' && c <= 'z')); @@ -283,15 +287,15 @@ private static boolean isTree(Object t) { if (t instanceof Integer) { return true; } - if (t instanceof Tuple2(var left, var right)) { - return isTree(left) && isTree(right); + if (t instanceof Tuple2 pair) { + return isTree(pair.value1()) && isTree(pair.value2()); } return false; } private static int depth(Object t) { - if (t instanceof Tuple2(var left, var right)) { - return 1 + Math.max(depth(left), depth(right)); + if (t instanceof Tuple2 pair) { + return 1 + Math.max(depth(pair.value1()), depth(pair.value2())); } return 0; } diff --git a/src/test/java/dev/hegel/CoverageTest.java b/shared/src/test/java/dev/hegel/CoverageTest.java similarity index 92% rename from src/test/java/dev/hegel/CoverageTest.java rename to shared/src/test/java/dev/hegel/CoverageTest.java index eb2d54d..a270faf 100644 --- a/src/test/java/dev/hegel/CoverageTest.java +++ b/shared/src/test/java/dev/hegel/CoverageTest.java @@ -6,13 +6,10 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.lang.foreign.MemorySegment; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; import org.junit.jupiter.api.Test; /** Targeted tests closing remaining coverage branches. */ @@ -37,24 +34,6 @@ void fnv1aMatchesKnownVector() { assertTrue(Abi.LABEL_COMPOSITE == Abi.fnv1a("dev.hegel.composite")); } - // --- output-callback bridge --- - @Test - void emitLineDecodesAndSwallowsExceptions() { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { - byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); - MemorySegment line = arena.allocate(bytes.length); - MemorySegment.copy(bytes, 0, line, java.lang.foreign.ValueLayout.JAVA_BYTE, 0, bytes.length); - AtomicReference got = new AtomicReference<>(); - RealLibhegel.emitLine(got::set, MemorySegment.NULL, line, bytes.length); - assertEquals("hello", got.get()); - // A throwing consumer must be swallowed: an exception escaping an upcall kills the VM. - Consumer throwing = s -> { - throw new IllegalStateException("never escapes"); - }; - RealLibhegel.emitLine(throwing, MemorySegment.NULL, line, bytes.length); - } - } - // --- StringGeneratorHandle cleanup --- @Test void handleFreeReleasesThroughItsBinding() { @@ -355,7 +334,8 @@ void broken(TestCase t) { } @Test - void isNullHandlesJavaNull() { - assertTrue(Runner.isNull(null)); + void isNullChecksTheAddress() { + assertTrue(Runner.isNull(0)); + assertFalse(Runner.isNull(FakeLibhegel.TC)); } } diff --git a/src/test/java/dev/hegel/DerivationTest.java b/shared/src/test/java/dev/hegel/DerivationTest.java similarity index 100% rename from src/test/java/dev/hegel/DerivationTest.java rename to shared/src/test/java/dev/hegel/DerivationTest.java diff --git a/src/test/java/dev/hegel/EndToEndTest.java b/shared/src/test/java/dev/hegel/EndToEndTest.java similarity index 100% rename from src/test/java/dev/hegel/EndToEndTest.java rename to shared/src/test/java/dev/hegel/EndToEndTest.java diff --git a/src/test/java/dev/hegel/EngineEdgeTest.java b/shared/src/test/java/dev/hegel/EngineEdgeTest.java similarity index 100% rename from src/test/java/dev/hegel/EngineEdgeTest.java rename to shared/src/test/java/dev/hegel/EngineEdgeTest.java diff --git a/src/test/java/dev/hegel/EngineTest.java b/shared/src/test/java/dev/hegel/EngineTest.java similarity index 100% rename from src/test/java/dev/hegel/EngineTest.java rename to shared/src/test/java/dev/hegel/EngineTest.java diff --git a/src/test/java/dev/hegel/FakeLibhegel.java b/shared/src/test/java/dev/hegel/FakeLibhegel.java similarity index 72% rename from src/test/java/dev/hegel/FakeLibhegel.java rename to shared/src/test/java/dev/hegel/FakeLibhegel.java index 2989dca..06f55a4 100644 --- a/src/test/java/dev/hegel/FakeLibhegel.java +++ b/shared/src/test/java/dev/hegel/FakeLibhegel.java @@ -1,6 +1,5 @@ package dev.hegel; -import java.lang.foreign.MemorySegment; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -16,11 +15,11 @@ */ final class FakeLibhegel implements Libhegel { // Opaque handle sentinels (non-null addresses). - static final MemorySegment SETTINGS = MemorySegment.ofAddress(0x100); - static final MemorySegment RUN = MemorySegment.ofAddress(0x200); - static final MemorySegment TC = MemorySegment.ofAddress(0x300); - static final MemorySegment RESULT = MemorySegment.ofAddress(0x400); - static final MemorySegment STRING_GEN = MemorySegment.ofAddress(0x600); + static final long SETTINGS = 0x100; + static final long RUN = 0x200; + static final long TC = 0x300; + static final long RESULT = 0x400; + static final long STRING_GEN = 0x600; String lastError = "fake error"; String version = "0.0.0-fake"; @@ -126,66 +125,66 @@ final class FakeLibhegel implements Libhegel { private int ruleIndex; @Override - public MemorySegment settingsNew() { + public long settingsNew() { return SETTINGS; } @Override - public void settingsFree(MemorySegment s) { + public void settingsFree(long s) { settingsFreed = true; } @Override - public void settingsMode(MemorySegment s, int mode) { + public void settingsMode(long s, int mode) { modeCode = mode; } @Override - public void settingsBackend(MemorySegment s, int backend) { + public void settingsBackend(long s, int backend) { backendCode = backend; } @Override - public void settingsTestCases(MemorySegment s, long n) { + public void settingsTestCases(long s, long n) { testCases = n; } @Override - public void settingsVerbosity(MemorySegment s, int v) {} + public void settingsVerbosity(long s, int v) {} @Override - public void settingsSeed(MemorySegment s, long seed, boolean hasSeed) {} + public void settingsSeed(long s, long seed, boolean hasSeed) {} @Override - public void settingsDerandomize(MemorySegment s, boolean derandomize) { + public void settingsDerandomize(long s, boolean derandomize) { this.derandomize = derandomize; } @Override - public void settingsReportMultipleFailures(MemorySegment s, boolean yes) {} + public void settingsReportMultipleFailures(long s, boolean yes) {} @Override - public void settingsDatabase(MemorySegment s, String path) { + public void settingsDatabase(long s, String path) { databasePath = path; } @Override - public void settingsDatabaseKey(MemorySegment s, String key) { + public void settingsDatabaseKey(long s, String key) { databaseKey = key; } @Override - public void settingsPhases(MemorySegment s, int mask) { + public void settingsPhases(long s, int mask) { phasesMask = mask; } @Override - public void settingsSuppressHealthCheck(MemorySegment s, int mask) { + public void settingsSuppressHealthCheck(long s, int mask) { suppressMask = mask; } @Override - public MemorySegment runStart(MemorySegment settings, Consumer output) { + public long runStart(long settings, Consumer output) { if (runStartFails) { throw new HegelException("hegel_run_start failed: " + lastError); } @@ -194,34 +193,34 @@ public MemorySegment runStart(MemorySegment settings, Consumer output) { } @Override - public MemorySegment nextTestCase(MemorySegment run) { + public long nextTestCase(long run) { if (nextTestCaseFails) { throw new HegelException("hegel_next_test_case failed: " + lastError); } if (casesServed >= caseCount) { - return MemorySegment.NULL; + return 0; } casesServed++; return TC; } @Override - public MemorySegment runResult(MemorySegment run) { + public long runResult(long run) { return RESULT; } @Override - public void runResultFree(MemorySegment result) { + public void runResultFree(long result) { runResultFreed = true; } @Override - public void runFree(MemorySegment run) { + public void runFree(long run) { runFreed = true; } @Override - public int testCaseFromBlob(MemorySegment settings, String blob, Consumer output, MemorySegment[] out) { + public int testCaseFromBlob(long settings, String blob, Consumer output, long[] out) { if (fromBlobRc == Abi.OK) { replayedBlobs.add(blob); out[0] = TC; @@ -230,12 +229,12 @@ public int testCaseFromBlob(MemorySegment settings, String blob, Consumer excludeCategories, String includeCharacters, String excludeCharacters, - MemorySegment[] out) { + long[] out) { if (stringGeneratorTextRc == Abi.OK) { textMinSize = minSize; textMaxSize = maxSize; @@ -363,7 +362,7 @@ public int stringGeneratorText( } @Override - public int stringGeneratorRegex(String pattern, boolean fullmatch, MemorySegment alphabet, MemorySegment[] out) { + public int stringGeneratorRegex(String pattern, boolean fullmatch, long alphabet, long[] out) { if (stringGeneratorRegexRc == Abi.OK) { out[0] = STRING_GEN; } @@ -371,7 +370,7 @@ public int stringGeneratorRegex(String pattern, boolean fullmatch, MemorySegment } @Override - public int stringGeneratorEmail(MemorySegment[] out) { + public int stringGeneratorEmail(long[] out) { if (stringGeneratorEmailRc == Abi.OK) { out[0] = STRING_GEN; } @@ -379,7 +378,7 @@ public int stringGeneratorEmail(MemorySegment[] out) { } @Override - public int stringGeneratorUrl(MemorySegment[] out) { + public int stringGeneratorUrl(long[] out) { if (stringGeneratorUrlRc == Abi.OK) { out[0] = STRING_GEN; } @@ -387,7 +386,7 @@ public int stringGeneratorUrl(MemorySegment[] out) { } @Override - public int stringGeneratorDomain(long maxLength, MemorySegment[] out) { + public int stringGeneratorDomain(long maxLength, long[] out) { if (stringGeneratorDomainRc == Abi.OK) { domainMaxLength = maxLength; out[0] = STRING_GEN; @@ -396,12 +395,12 @@ public int stringGeneratorDomain(long maxLength, MemorySegment[] out) { } @Override - public void stringGeneratorFree(MemorySegment generator) { + public void stringGeneratorFree(long generator) { freedStringGenerators++; } @Override - public int startSpan(MemorySegment tc, long label) { + public int startSpan(long tc, long label) { if (startSpanRc == Abi.OK) { startedSpans.add(label); } @@ -409,12 +408,12 @@ public int startSpan(MemorySegment tc, long label) { } @Override - public int stopSpan(MemorySegment tc, boolean discard) { + public int stopSpan(long tc, boolean discard) { return stopSpanRc; } @Override - public int newCollection(MemorySegment tc, long minSize, long maxSize, long[] outId) { + public int newCollection(long tc, long minSize, long maxSize, long[] outId) { if (newCollectionRc == Abi.OK) { collectionMinSize = minSize; collectionMaxSize = maxSize; @@ -424,7 +423,7 @@ public int newCollection(MemorySegment tc, long minSize, long maxSize, long[] ou } @Override - public int collectionMore(MemorySegment tc, long id, boolean[] outMore) { + public int collectionMore(long tc, long id, boolean[] outMore) { if (collectionMoreRc == Abi.OK) { outMore[0] = moreIndex < moreSequence.length && moreSequence[moreIndex++]; } @@ -432,12 +431,12 @@ public int collectionMore(MemorySegment tc, long id, boolean[] outMore) { } @Override - public int collectionReject(MemorySegment tc, long id, String why) { + public int collectionReject(long tc, long id, String why) { return collectionRejectRc; } @Override - public int newPool(MemorySegment tc, long[] outId) { + public int newPool(long tc, long[] outId) { if (newPoolRc == Abi.OK) { outId[0] = poolId; } @@ -445,7 +444,7 @@ public int newPool(MemorySegment tc, long[] outId) { } @Override - public int poolAdd(MemorySegment tc, long poolId, long[] outVariableId) { + public int poolAdd(long tc, long poolId, long[] outVariableId) { if (poolAddRc == Abi.OK) { outVariableId[0] = nextVariableId++; } @@ -453,7 +452,7 @@ public int poolAdd(MemorySegment tc, long poolId, long[] outVariableId) { } @Override - public int poolGenerate(MemorySegment tc, long poolId, boolean consume, long[] outVariableId) { + public int poolGenerate(long tc, long poolId, boolean consume, long[] outVariableId) { if (poolGenerateRc == Abi.OK) { outVariableId[0] = poolGenerateValue == null ? 0 : poolGenerateValue; } @@ -461,7 +460,7 @@ public int poolGenerate(MemorySegment tc, long poolId, boolean consume, long[] o } @Override - public int newStateMachine(MemorySegment tc, List ruleNames, List invariantNames, long[] outId) { + public int newStateMachine(long tc, List ruleNames, List invariantNames, long[] outId) { if (newStateMachineRc == Abi.OK) { stateMachineRules = ruleNames; stateMachineInvariants = invariantNames; @@ -471,7 +470,7 @@ public int newStateMachine(MemorySegment tc, List ruleNames, List Date: Fri, 28 Aug 2026 16:51:38 +0100 Subject: [PATCH 2/2] Remove vscode file accidentally added, and prevent it from being added ever again --- .gitignore | 2 ++ .vscode/settings.json | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 729e988..a2a8af8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ target/ CLAUDE.local.md .claude/settings.local.json +.vscode/ + # whitelist .claude files, rather than blacklist. Some local claude files # (skills, agents) have to live in .claude - as opposed to eg CLAUDE.local.md, # which we gitignore at the top level. diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c5f3f6b..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "interactive" -} \ No newline at end of file