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