diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
index 47e45f8..e71d414 100644
--- a/.claude/CLAUDE.md
+++ b/.claude/CLAUDE.md
@@ -79,8 +79,7 @@ public `Generator`/`TestCase`/`Generators`/`Hegel` surface stays in `dev.hegel`.
## Coverage notes
-Two genuinely-unreachable defensive catch blocks are excluded via `@Generated` (JaCoCo ignores
-`*Generated*`-named annotations): the `NoSuchAlgorithmException` for SHA-256 and the reflective
-dispatch in `HegelTestExtension`. Everything else is covered by real-engine integration tests plus
+One genuinely-unreachable defensive catch block is excluded via `@Generated` (JaCoCo ignores
+`*Generated*`-named annotations): the `NoSuchAlgorithmException` for SHA-256. Everything else is covered by real-engine integration tests plus
`FakeLibhegel`-driven error-path tests. The engine's `collection_more`/`new_collection` out-params
are read unconditionally (the engine signals exhaustion on the following draw, not at those calls).
diff --git a/.gitignore b/.gitignore
index 5a0e1d8..59a41e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,11 @@ target/
.hegel/
*.class
*.pyc
+CLAUDE.local.md
+.claude/settings.local.json
+
+# 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.
+.claude/*
+!.claude/CLAUDE.md
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 0000000..478d6dd
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,8 @@
+RELEASE_TYPE: minor
+
+Improve Java Platform Module System support:
+
+- Define `Automatic-Module-Name: dev.hegel` in the jar manifest, giving the artifact a stable
+ module name on the module path.
+- `@HegelTest` now invokes the test method through the JUnit platform's reflection support, so
+ modular consumers no longer have to open their test package to `dev.hegel`.
diff --git a/pom.xml b/pom.xml
index 90b2553..2507004 100644
--- a/pom.xml
+++ b/pom.xml
@@ -99,6 +99,19 @@
3.13.0
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.4.2
+
+
+
+ dev.hegel
+
+
+
+
+
@@ -184,6 +197,26 @@
+
+ org.apache.maven.plugins
+ maven-invoker-plugin
+ 3.9.0
+
+ src/it
+ ${project.build.directory}/it
+ true
+
+
+
+ module-consumer-it
+
+ install
+ run
+
+
+
+
+
com.diffplug.spotless
spotless-maven-plugin
diff --git a/src/it/module-consumer/invoker.properties b/src/it/module-consumer/invoker.properties
new file mode 100644
index 0000000..bb145c5
--- /dev/null
+++ b/src/it/module-consumer/invoker.properties
@@ -0,0 +1,3 @@
+# `test` compiles the module (resolving `requires dev.hegel;`, the module-name guard) and runs
+# the @HegelTest against the real engine, consuming hegel on the module path end to end.
+invoker.goals = test
diff --git a/src/it/module-consumer/pom.xml b/src/it/module-consumer/pom.xml
new file mode 100644
index 0000000..788429a
--- /dev/null
+++ b/src/it/module-consumer/pom.xml
@@ -0,0 +1,51 @@
+
+
+ 4.0.0
+
+ dev.hegel.it
+ module-consumer
+ 1.0
+ jar
+
+
+ UTF-8
+ 22
+
+
+
+
+ dev.hegel
+ hegel
+
+ @project.version@
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.11.4
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.2
+
+ --enable-native-access=dev.hegel
+
+
+
+
+
diff --git a/src/it/module-consumer/src/main/java/module-info.java b/src/it/module-consumer/src/main/java/module-info.java
new file mode 100644
index 0000000..43617f6
--- /dev/null
+++ b/src/it/module-consumer/src/main/java/module-info.java
@@ -0,0 +1,6 @@
+// The presence of `module-info.java` puts hegel on the module path. `requires dev.hegel;`
+// names the automatic module by its committed `Automatic-Module-Name`; if that name ever
+// changes, this no longer resolves and the build fails — which is the regression we guard.
+module dev.hegel.consumer {
+ requires dev.hegel;
+}
diff --git a/src/it/module-consumer/src/test/java/dev/hegel/consumer/ConsumerTest.java b/src/it/module-consumer/src/test/java/dev/hegel/consumer/ConsumerTest.java
new file mode 100644
index 0000000..a4f4922
--- /dev/null
+++ b/src/it/module-consumer/src/test/java/dev/hegel/consumer/ConsumerTest.java
@@ -0,0 +1,16 @@
+package dev.hegel.consumer;
+
+import static dev.hegel.Generators.integers;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import dev.hegel.HegelTest;
+import dev.hegel.TestCase;
+
+class ConsumerTest {
+ @HegelTest
+ void additionCommutes(TestCase tc) {
+ int x = tc.draw(integers().min(0).max(100));
+ int y = tc.draw(integers().min(0).max(100));
+ assertEquals(x + y, y + x);
+ }
+}
diff --git a/src/main/java/dev/hegel/HegelTestExtension.java b/src/main/java/dev/hegel/HegelTestExtension.java
index e05169f..5a9bb5c 100644
--- a/src/main/java/dev/hegel/HegelTestExtension.java
+++ b/src/main/java/dev/hegel/HegelTestExtension.java
@@ -1,6 +1,5 @@
package dev.hegel;
-import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.EnumSet;
@@ -14,6 +13,7 @@
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
+import org.junit.platform.commons.support.ReflectionSupport;
/**
* JUnit 5 extension backing {@link HegelTest}. Reports the property as a single test entry and
@@ -76,7 +76,7 @@ public void interceptTestTemplateMethod(
HegelTest ann = method.getAnnotation(HegelTest.class);
Settings settings = settingsFrom(ann, method.getName());
Object target = invocationContext.getTarget().orElse(null);
- Hegel.test(tc -> invoke(method, target, tc), settings);
+ Hegel.test(tc -> ReflectionSupport.invokeMethod(method, target, tc), settings);
}
}
@@ -120,21 +120,4 @@ static boolean isDefaultPhases(Phase[] phases) {
static boolean isTestCaseParam(Class> type) {
return type == TestCase.class;
}
-
- @Generated // thin reflective dispatch; failure propagation is verified end-to-end.
- private static void invoke(Method method, Object target, TestCase tc) {
- try {
- method.setAccessible(true);
- method.invoke(target, tc);
- } catch (InvocationTargetException e) {
- sneakyThrow(e.getCause());
- } catch (IllegalAccessException e) {
- throw new HegelException("Cannot invoke @HegelTest method " + method.getName(), e);
- }
- }
-
- @SuppressWarnings("unchecked")
- private static void sneakyThrow(Throwable t) throws E {
- throw (E) t;
- }
}