diff --git a/docs/TOOLING.md b/docs/TOOLING.md index babe2ce..07d07f7 100644 --- a/docs/TOOLING.md +++ b/docs/TOOLING.md @@ -52,10 +52,26 @@ This command blocks on: - at least 80% line coverage for all `sfqd-core` production classes; - SpotBugs at maximum effort with zero allowed findings; - JavaDoc errors or warnings; -- exact binary/source/JavaDoc artifact contents and public API surface. +- exact binary/source/JavaDoc artifact contents; +- the checked-in public API signature manifest, including public types, public/protected members, + generic signatures, modifiers, record components, and sealed permitted subclasses. The Java artifact verifier also inserts the project license into the source and -JavaDoc JARs without invoking platform-specific archive tools. +JavaDoc JARs without invoking platform-specific archive tools. Both the unit +gate and artifact verifier compare compiled output with +`sfqd-core/src/main/api/public-api.txt`; adding a documented public member to an +existing type therefore fails the build. Before the first release, regenerate +the candidate manifest after compilation with: + +```shell +java -cp sfqd-core/target/test-classes:sfqd-core/target/classes \ + io.github.pzhin.sfqd.build.PublicApiManifest sfqd-core/target/classes +``` + +Review and commit the output only for an intentional API change. After `0.1.0`, +artifact-to-artifact compatibility checking with Revapi or japicmp should use +the previous published release as its baseline; the manifest remains the +fail-closed exact-surface gate for the current release line. `tools/verify-javadoc-gate.sh` adds an undocumented public class, constructor, and method in a detached worktree. The build must fail at the JavaDoc goal. A diff --git a/sfqd-core/src/main/api/public-api.txt b/sfqd-core/src/main/api/public-api.txt new file mode 100644 index 0000000..11a7894 --- /dev/null +++ b/sfqd-core/src/main/api/public-api.txt @@ -0,0 +1,127 @@ +# SFQ(D) core public API signature manifest +# Generated from compiled classes by PublicApiManifest; update only for an intentional API change. +format=1 + +TYPE name=io.github.pzhin.sfqd.CancelResult kind=enum modifiers=[public, final] typeParameters=[] superclass=java.lang.Enum interfaces=[] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.CancellationAccounting kind=enum modifiers=[public, final] typeParameters=[] superclass=java.lang.Enum interfaces=[] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.CloseFlowResult kind=enum modifiers=[public, final] typeParameters=[] superclass=java.lang.Enum interfaces=[] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.CompletionResult kind=enum modifiers=[public, final] typeParameters=[] superclass=java.lang.Enum interfaces=[] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.Dispatch kind=class modifiers=[public, final] typeParameters=[F, J, P] superclass=java.lang.Object interfaces=[] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.EnqueueResult kind=interface modifiers=[public, abstract, sealed] typeParameters=[] superclass=- interfaces=[] permits=[io.github.pzhin.sfqd.EnqueueResult$Accepted, io.github.pzhin.sfqd.EnqueueResult$Rejected] synthetic=false +TYPE name=io.github.pzhin.sfqd.EnqueueResult$Accepted kind=class modifiers=[public, static, final] typeParameters=[] superclass=java.lang.Object interfaces=[io.github.pzhin.sfqd.EnqueueResult] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.EnqueueResult$Rejected kind=enum modifiers=[public, static, final] typeParameters=[] superclass=java.lang.Enum interfaces=[io.github.pzhin.sfqd.EnqueueResult] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.FlowHandle kind=class modifiers=[public, final] typeParameters=[] superclass=java.lang.Object interfaces=[] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.FlowSnapshot kind=class modifiers=[public, final] typeParameters=[] superclass=java.lang.Object interfaces=[] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.JobHandle kind=class modifiers=[public, final] typeParameters=[] superclass=java.lang.Object interfaces=[] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.RegisterFlowResult kind=interface modifiers=[public, abstract, sealed] typeParameters=[] superclass=- interfaces=[] permits=[io.github.pzhin.sfqd.RegisterFlowResult$Registered, io.github.pzhin.sfqd.RegisterFlowResult$Rejected] synthetic=false +TYPE name=io.github.pzhin.sfqd.RegisterFlowResult$Registered kind=class modifiers=[public, static, final] typeParameters=[] superclass=java.lang.Object interfaces=[io.github.pzhin.sfqd.RegisterFlowResult] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.RegisterFlowResult$Rejected kind=enum modifiers=[public, static, final] typeParameters=[] superclass=java.lang.Enum interfaces=[io.github.pzhin.sfqd.RegisterFlowResult] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.SchedulerConfig kind=record modifiers=[public, final] typeParameters=[] superclass=java.lang.Record interfaces=[] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.SchedulerSnapshot kind=class modifiers=[public, final] typeParameters=[] superclass=java.lang.Object interfaces=[] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.SfqdScheduler kind=class modifiers=[public, final] typeParameters=[F, J, P] superclass=java.lang.Object interfaces=[] permits=[] synthetic=false +TYPE name=io.github.pzhin.sfqd.WeightDomain kind=class modifiers=[public, final] typeParameters=[] superclass=java.lang.Object interfaces=[] permits=[] synthetic=false + +CONSTRUCTOR owner=io.github.pzhin.sfqd.SchedulerConfig modifiers=[public] typeParameters=[] parameters=[int, int, int, io.github.pzhin.sfqd.CancellationAccounting, io.github.pzhin.sfqd.WeightDomain] throws=[] +CONSTRUCTOR owner=io.github.pzhin.sfqd.SchedulerConfig modifiers=[public] typeParameters=[] parameters=[int, int, int, io.github.pzhin.sfqd.CancellationAccounting] throws=[] +CONSTRUCTOR owner=io.github.pzhin.sfqd.SchedulerConfig modifiers=[public] typeParameters=[] parameters=[int, int, int] throws=[] +CONSTRUCTOR owner=io.github.pzhin.sfqd.SfqdScheduler modifiers=[public] typeParameters=[] parameters=[io.github.pzhin.sfqd.SchedulerConfig] throws=[] +FIELD owner=io.github.pzhin.sfqd.CancelResult name=CANCELLED modifiers=[public, static, final] type=io.github.pzhin.sfqd.CancelResult enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.CancelResult name=NOT_LIVE modifiers=[public, static, final] type=io.github.pzhin.sfqd.CancelResult enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.CancelResult name=TOO_LATE_ALREADY_DISPATCHED modifiers=[public, static, final] type=io.github.pzhin.sfqd.CancelResult enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.CancellationAccounting name=CHARGE_RESERVED_COST modifiers=[public, static, final] type=io.github.pzhin.sfqd.CancellationAccounting enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.CloseFlowResult name=CLOSED modifiers=[public, static, final] type=io.github.pzhin.sfqd.CloseFlowResult enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.CloseFlowResult name=FAIRNESS_DEBT_ACTIVE modifiers=[public, static, final] type=io.github.pzhin.sfqd.CloseFlowResult enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.CloseFlowResult name=FLOW_ACTIVE modifiers=[public, static, final] type=io.github.pzhin.sfqd.CloseFlowResult enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.CloseFlowResult name=FLOW_NOT_REGISTERED modifiers=[public, static, final] type=io.github.pzhin.sfqd.CloseFlowResult enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.CompletionResult name=COMPLETED modifiers=[public, static, final] type=io.github.pzhin.sfqd.CompletionResult enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.CompletionResult name=NOT_DISPATCHED modifiers=[public, static, final] type=io.github.pzhin.sfqd.CompletionResult enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.CompletionResult name=NOT_LIVE modifiers=[public, static, final] type=io.github.pzhin.sfqd.CompletionResult enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.EnqueueResult$Rejected name=DUPLICATE_LIVE_ID modifiers=[public, static, final] type=io.github.pzhin.sfqd.EnqueueResult$Rejected enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.EnqueueResult$Rejected name=FLOW_NOT_REGISTERED modifiers=[public, static, final] type=io.github.pzhin.sfqd.EnqueueResult$Rejected enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.EnqueueResult$Rejected name=LIVE_LIMIT modifiers=[public, static, final] type=io.github.pzhin.sfqd.EnqueueResult$Rejected enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.EnqueueResult$Rejected name=NUMERIC_LIMIT modifiers=[public, static, final] type=io.github.pzhin.sfqd.EnqueueResult$Rejected enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.EnqueueResult$Rejected name=SEQUENCE_EXHAUSTED modifiers=[public, static, final] type=io.github.pzhin.sfqd.EnqueueResult$Rejected enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.RegisterFlowResult$Rejected name=DUPLICATE_REGISTERED_ID modifiers=[public, static, final] type=io.github.pzhin.sfqd.RegisterFlowResult$Rejected enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.RegisterFlowResult$Rejected name=FLOW_LIMIT modifiers=[public, static, final] type=io.github.pzhin.sfqd.RegisterFlowResult$Rejected enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.RegisterFlowResult$Rejected name=FLOW_SEQUENCE_EXHAUSTED modifiers=[public, static, final] type=io.github.pzhin.sfqd.RegisterFlowResult$Rejected enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.RegisterFlowResult$Rejected name=WEIGHT_OUTSIDE_DOMAIN modifiers=[public, static, final] type=io.github.pzhin.sfqd.RegisterFlowResult$Rejected enumConstant=true synthetic=false +FIELD owner=io.github.pzhin.sfqd.SchedulerConfig name=MAX_DEPTH modifiers=[public, static, final] type=int enumConstant=false synthetic=false +METHOD owner=io.github.pzhin.sfqd.CancelResult name=valueOf modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.CancelResult parameters=[java.lang.String] throws=[] +METHOD owner=io.github.pzhin.sfqd.CancelResult name=values modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.CancelResult[] parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.CancellationAccounting name=valueOf modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.CancellationAccounting parameters=[java.lang.String] throws=[] +METHOD owner=io.github.pzhin.sfqd.CancellationAccounting name=values modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.CancellationAccounting[] parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.CloseFlowResult name=valueOf modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.CloseFlowResult parameters=[java.lang.String] throws=[] +METHOD owner=io.github.pzhin.sfqd.CloseFlowResult name=values modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.CloseFlowResult[] parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.CompletionResult name=valueOf modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.CompletionResult parameters=[java.lang.String] throws=[] +METHOD owner=io.github.pzhin.sfqd.CompletionResult name=values modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.CompletionResult[] parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.Dispatch name=cost modifiers=[public] typeParameters=[] return=long parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.Dispatch name=flowId modifiers=[public] typeParameters=[] return=F parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.Dispatch name=jobHandle modifiers=[public] typeParameters=[] return=io.github.pzhin.sfqd.JobHandle parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.Dispatch name=jobId modifiers=[public] typeParameters=[] return=J parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.Dispatch name=payload modifiers=[public] typeParameters=[] return=P parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.EnqueueResult$Accepted name=jobHandle modifiers=[public] typeParameters=[] return=io.github.pzhin.sfqd.JobHandle parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.EnqueueResult$Rejected name=valueOf modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.EnqueueResult$Rejected parameters=[java.lang.String] throws=[] +METHOD owner=io.github.pzhin.sfqd.EnqueueResult$Rejected name=values modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.EnqueueResult$Rejected[] parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowHandle name=equals modifiers=[public] typeParameters=[] return=boolean parameters=[java.lang.Object] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowHandle name=hashCode modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowHandle name=toString modifiers=[public] typeParameters=[] return=java.lang.String parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowSnapshot name=acceptedSuppliedCost modifiers=[public] typeParameters=[] return=java.math.BigInteger parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowSnapshot name=cancelledSuppliedCost modifiers=[public] typeParameters=[] return=java.math.BigInteger parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowSnapshot name=completedSuppliedCost modifiers=[public] typeParameters=[] return=java.math.BigInteger parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowSnapshot name=dispatchedSuppliedCost modifiers=[public] typeParameters=[] return=java.math.BigInteger parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowSnapshot name=equals modifiers=[public] typeParameters=[] return=boolean parameters=[java.lang.Object] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowSnapshot name=hashCode modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowSnapshot name=queuedJobs modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowSnapshot name=queuedSuppliedCost modifiers=[public] typeParameters=[] return=java.math.BigInteger parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowSnapshot name=runningJobs modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowSnapshot name=runningSuppliedCost modifiers=[public] typeParameters=[] return=java.math.BigInteger parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.FlowSnapshot name=toString modifiers=[public] typeParameters=[] return=java.lang.String parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.JobHandle name=equals modifiers=[public] typeParameters=[] return=boolean parameters=[java.lang.Object] throws=[] +METHOD owner=io.github.pzhin.sfqd.JobHandle name=hashCode modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.JobHandle name=toString modifiers=[public] typeParameters=[] return=java.lang.String parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.RegisterFlowResult$Registered name=flowHandle modifiers=[public] typeParameters=[] return=io.github.pzhin.sfqd.FlowHandle parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.RegisterFlowResult$Rejected name=valueOf modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.RegisterFlowResult$Rejected parameters=[java.lang.String] throws=[] +METHOD owner=io.github.pzhin.sfqd.RegisterFlowResult$Rejected name=values modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.RegisterFlowResult$Rejected[] parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerConfig name=cancellationAccounting modifiers=[public] typeParameters=[] return=io.github.pzhin.sfqd.CancellationAccounting parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerConfig name=depth modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerConfig name=equals modifiers=[public, final] typeParameters=[] return=boolean parameters=[java.lang.Object] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerConfig name=hashCode modifiers=[public, final] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerConfig name=maxFlows modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerConfig name=maxLiveJobs modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerConfig name=toString modifiers=[public, final] typeParameters=[] return=java.lang.String parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerConfig name=weightDomain modifiers=[public] typeParameters=[] return=io.github.pzhin.sfqd.WeightDomain parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=acceptedTotal modifiers=[public] typeParameters=[] return=long parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=activeFlows modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=backloggedFlows modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=cancelledTotal modifiers=[public] typeParameters=[] return=long parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=completedTotal modifiers=[public] typeParameters=[] return=long parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=depth modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=dispatchedTotal modifiers=[public] typeParameters=[] return=long parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=equals modifiers=[public] typeParameters=[] return=boolean parameters=[java.lang.Object] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=freeSlots modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=hashCode modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=maxFlows modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=maxLiveJobs modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=queuedJobs modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=registeredFlows modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=runningJobs modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SchedulerSnapshot name=toString modifiers=[public] typeParameters=[] return=java.lang.String parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SfqdScheduler name=cancel modifiers=[public] typeParameters=[] return=io.github.pzhin.sfqd.CancelResult parameters=[io.github.pzhin.sfqd.JobHandle] throws=[] +METHOD owner=io.github.pzhin.sfqd.SfqdScheduler name=closeFlow modifiers=[public] typeParameters=[] return=io.github.pzhin.sfqd.CloseFlowResult parameters=[io.github.pzhin.sfqd.FlowHandle] throws=[] +METHOD owner=io.github.pzhin.sfqd.SfqdScheduler name=complete modifiers=[public] typeParameters=[] return=io.github.pzhin.sfqd.CompletionResult parameters=[io.github.pzhin.sfqd.JobHandle] throws=[] +METHOD owner=io.github.pzhin.sfqd.SfqdScheduler name=dispatchUpTo modifiers=[public] typeParameters=[] return=java.util.List> parameters=[int] throws=[] +METHOD owner=io.github.pzhin.sfqd.SfqdScheduler name=enqueue modifiers=[public] typeParameters=[] return=io.github.pzhin.sfqd.EnqueueResult parameters=[io.github.pzhin.sfqd.FlowHandle, J, P, long] throws=[] +METHOD owner=io.github.pzhin.sfqd.SfqdScheduler name=registerFlow modifiers=[public] typeParameters=[] return=io.github.pzhin.sfqd.RegisterFlowResult parameters=[F, long] throws=[] +METHOD owner=io.github.pzhin.sfqd.SfqdScheduler name=snapshot modifiers=[public] typeParameters=[] return=io.github.pzhin.sfqd.SchedulerSnapshot parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.SfqdScheduler name=snapshot modifiers=[public] typeParameters=[] return=java.util.Optional parameters=[io.github.pzhin.sfqd.FlowHandle] throws=[] +METHOD owner=io.github.pzhin.sfqd.WeightDomain name=commonScale modifiers=[public] typeParameters=[] return=java.util.OptionalLong parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.WeightDomain name=divisorsOf modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.WeightDomain parameters=[long] throws=[] +METHOD owner=io.github.pzhin.sfqd.WeightDomain name=equals modifiers=[public] typeParameters=[] return=boolean parameters=[java.lang.Object] throws=[] +METHOD owner=io.github.pzhin.sfqd.WeightDomain name=hashCode modifiers=[public] typeParameters=[] return=int parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.WeightDomain name=toString modifiers=[public] typeParameters=[] return=java.lang.String parameters=[] throws=[] +METHOD owner=io.github.pzhin.sfqd.WeightDomain name=unrestricted modifiers=[public, static] typeParameters=[] return=io.github.pzhin.sfqd.WeightDomain parameters=[] throws=[] +RECORD_COMPONENT owner=io.github.pzhin.sfqd.SchedulerConfig name=cancellationAccounting type=io.github.pzhin.sfqd.CancellationAccounting +RECORD_COMPONENT owner=io.github.pzhin.sfqd.SchedulerConfig name=depth type=int +RECORD_COMPONENT owner=io.github.pzhin.sfqd.SchedulerConfig name=maxFlows type=int +RECORD_COMPONENT owner=io.github.pzhin.sfqd.SchedulerConfig name=maxLiveJobs type=int +RECORD_COMPONENT owner=io.github.pzhin.sfqd.SchedulerConfig name=weightDomain type=io.github.pzhin.sfqd.WeightDomain diff --git a/sfqd-core/src/test/java/io/github/pzhin/sfqd/PublicArtifactSurfaceTest.java b/sfqd-core/src/test/java/io/github/pzhin/sfqd/PublicArtifactSurfaceTest.java index e8b9229..4e95372 100644 --- a/sfqd-core/src/test/java/io/github/pzhin/sfqd/PublicArtifactSurfaceTest.java +++ b/sfqd-core/src/test/java/io/github/pzhin/sfqd/PublicArtifactSurfaceTest.java @@ -3,79 +3,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.github.pzhin.sfqd.build.PublicApiManifest; import java.io.IOException; -import java.lang.reflect.Modifier; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; final class PublicArtifactSurfaceTest { - private static final Set EXPECTED_PUBLIC_TYPES = Set.of( - "io.github.pzhin.sfqd.CancellationAccounting", - "io.github.pzhin.sfqd.CancelResult", - "io.github.pzhin.sfqd.CloseFlowResult", - "io.github.pzhin.sfqd.CompletionResult", - "io.github.pzhin.sfqd.Dispatch", - "io.github.pzhin.sfqd.EnqueueResult", - "io.github.pzhin.sfqd.EnqueueResult$Accepted", - "io.github.pzhin.sfqd.EnqueueResult$Rejected", - "io.github.pzhin.sfqd.FlowHandle", - "io.github.pzhin.sfqd.FlowSnapshot", - "io.github.pzhin.sfqd.JobHandle", - "io.github.pzhin.sfqd.RegisterFlowResult", - "io.github.pzhin.sfqd.RegisterFlowResult$Registered", - "io.github.pzhin.sfqd.RegisterFlowResult$Rejected", - "io.github.pzhin.sfqd.SchedulerConfig", - "io.github.pzhin.sfqd.SchedulerSnapshot", - "io.github.pzhin.sfqd.SfqdScheduler", - "io.github.pzhin.sfqd.WeightDomain"); - @Test - void exposesOnlyTheSpecifiedPublicTypes() throws IOException, URISyntaxException { + void exposesExactlyTheSpecifiedPublicApi() throws IOException, URISyntaxException, ClassNotFoundException { Path classesRoot = Path.of(SfqdScheduler.class.getProtectionDomain() .getCodeSource().getLocation().toURI()); assertTrue(Files.isDirectory(classesRoot), "core tests must run from the compiled classes directory"); - Set actualPublicTypes; - try (Stream files = Files.walk(classesRoot)) { - actualPublicTypes = files - .filter(Files::isRegularFile) - .filter(PublicArtifactSurfaceTest::isTypeClassFile) - .map(path -> binaryName(classesRoot, path)) - .filter(PublicArtifactSurfaceTest::isPublic) - .collect(Collectors.toUnmodifiableSet()); - } - - assertEquals(EXPECTED_PUBLIC_TYPES, actualPublicTypes); - } - - private static boolean isTypeClassFile(Path path) { - Path fileName = path.getFileName(); - if (fileName == null) { - return false; - } - String name = fileName.toString(); - return name.endsWith(".class") - && !name.equals("module-info.class") - && !name.equals("package-info.class"); - } - - private static String binaryName(Path classesRoot, Path classFile) { - String relativeName = classesRoot.relativize(classFile).toString(); - return relativeName.substring(0, relativeName.length() - ".class".length()) - .replace(classFile.getFileSystem().getSeparator(), "."); - } + String expected = PublicApiManifest.read(Path.of("src/main/api/public-api.txt")); + String actual = PublicApiManifest.describeDirectory(SfqdScheduler.class.getClassLoader(), classesRoot); - private static boolean isPublic(String binaryName) { - try { - Class type = Class.forName(binaryName, false, SfqdScheduler.class.getClassLoader()); - return Modifier.isPublic(type.getModifiers()); - } catch (ClassNotFoundException impossible) { - throw new AssertionError("compiled class cannot be loaded: " + binaryName, impossible); - } + assertEquals(expected, actual, () -> "public API signature manifest differs" + + PublicApiManifest.difference(expected, actual)); } } diff --git a/sfqd-core/src/test/java/io/github/pzhin/sfqd/build/CoreArtifactVerifier.java b/sfqd-core/src/test/java/io/github/pzhin/sfqd/build/CoreArtifactVerifier.java index 4c5d6c5..b1f6704 100644 --- a/sfqd-core/src/test/java/io/github/pzhin/sfqd/build/CoreArtifactVerifier.java +++ b/sfqd-core/src/test/java/io/github/pzhin/sfqd/build/CoreArtifactVerifier.java @@ -6,7 +6,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.lang.reflect.Modifier; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.AtomicMoveNotSupportedException; @@ -35,25 +34,6 @@ public final class CoreArtifactVerifier { private static final String LICENSE_ENTRY = "META-INF/LICENSE"; private static final String SCHEDULER_PATH = "io/github/pzhin/sfqd/SfqdScheduler"; - private static final Set EXPECTED_PUBLIC_TYPES = Set.of( - "io.github.pzhin.sfqd.CancelResult", - "io.github.pzhin.sfqd.CancellationAccounting", - "io.github.pzhin.sfqd.CloseFlowResult", - "io.github.pzhin.sfqd.CompletionResult", - "io.github.pzhin.sfqd.Dispatch", - "io.github.pzhin.sfqd.EnqueueResult", - "io.github.pzhin.sfqd.EnqueueResult$Accepted", - "io.github.pzhin.sfqd.EnqueueResult$Rejected", - "io.github.pzhin.sfqd.FlowHandle", - "io.github.pzhin.sfqd.FlowSnapshot", - "io.github.pzhin.sfqd.JobHandle", - "io.github.pzhin.sfqd.RegisterFlowResult", - "io.github.pzhin.sfqd.RegisterFlowResult$Registered", - "io.github.pzhin.sfqd.RegisterFlowResult$Rejected", - "io.github.pzhin.sfqd.SchedulerConfig", - "io.github.pzhin.sfqd.SchedulerSnapshot", - "io.github.pzhin.sfqd.SfqdScheduler", - "io.github.pzhin.sfqd.WeightDomain"); private CoreArtifactVerifier() { } @@ -90,6 +70,8 @@ public static void main(String[] arguments) throws IOException, ClassNotFoundExc require(Files.isDirectory(targetDirectory.resolve("classes")), "compiled main output is missing"); require(Files.isRegularFile(licensePath), "project LICENSE is missing"); + require(Files.isRegularFile(projectDirectory.resolve("src/main/api/public-api.txt")), + "checked-in public API signature manifest is missing"); selfTestDiscovery(outputTimestamp); Map artifacts = classifyArtifacts(targetDirectory); @@ -129,8 +111,10 @@ private static void verifyArtifacts( verifyLicense(sourcesArtifact, license, "sources"); verifyLicense(javadocArtifact, license, "JavaDoc"); verifyClassRelease(binaryArtifact, binaryEntries, javaRelease); - verifyPublicTypes(binaryArtifact, binaryEntries); - verifyJavadocTypes(javadocEntries); + String expectedPublicApi = PublicApiManifest.read( + projectDirectory.resolve("src/main/api/public-api.txt")); + verifyPublicApi(binaryArtifact, binaryEntries, expectedPublicApi); + verifyJavadocTypes(javadocEntries, PublicApiManifest.publicTypeNames(expectedPublicApi)); verifyNoToolingPackage(artifacts); } @@ -153,29 +137,29 @@ private static void verifyClassRelease(Path binaryArtifact, Set entries, } } - private static void verifyPublicTypes(Path binaryArtifact, Set binaryEntries) + private static void verifyPublicApi( + Path binaryArtifact, + Set binaryEntries, + String expectedPublicApi) throws IOException, ClassNotFoundException { - Set actualPublicTypes = new TreeSet<>(); + Set binaryNames = new TreeSet<>(); + for (String entry : entriesEndingWith(binaryEntries, ".class")) { + if (!entry.equals("module-info.class") && !entry.endsWith("/package-info.class")) { + binaryNames.add(entry.substring(0, entry.length() - ".class".length()).replace('/', '.')); + } + } URL[] urls = {binaryArtifact.toUri().toURL()}; try (URLClassLoader loader = new URLClassLoader(urls, ClassLoader.getPlatformClassLoader())) { - for (String entry : entriesEndingWith(binaryEntries, ".class")) { - if (entry.equals("module-info.class") || entry.endsWith("/package-info.class")) { - continue; - } - String binaryName = entry.substring(0, entry.length() - ".class".length()).replace('/', '.'); - Class type = Class.forName(binaryName, false, loader); - if (Modifier.isPublic(type.getModifiers())) { - actualPublicTypes.add(binaryName); - } - } + String actualPublicApi = PublicApiManifest.describe(loader, binaryNames); + require(expectedPublicApi.equals(actualPublicApi), + "binary JAR public API signatures differ from the checked-in manifest" + + PublicApiManifest.difference(expectedPublicApi, actualPublicApi)); } - assertEqual("binary JAR public type surface differs from the specified API", - EXPECTED_PUBLIC_TYPES, actualPublicTypes); } - private static void verifyJavadocTypes(Set entries) { + private static void verifyJavadocTypes(Set entries, Set expectedPublicTypes) { Set expected = new TreeSet<>(); - for (String binaryName : EXPECTED_PUBLIC_TYPES) { + for (String binaryName : expectedPublicTypes) { String simpleName = binaryName.substring("io.github.pzhin.sfqd.".length()).replace('$', '.'); expected.add("io/github/pzhin/sfqd/" + simpleName + ".html"); } diff --git a/sfqd-core/src/test/java/io/github/pzhin/sfqd/build/PublicApiManifest.java b/sfqd-core/src/test/java/io/github/pzhin/sfqd/build/PublicApiManifest.java new file mode 100644 index 0000000..efd05cc --- /dev/null +++ b/sfqd-core/src/test/java/io/github/pzhin/sfqd/build/PublicApiManifest.java @@ -0,0 +1,395 @@ +package io.github.pzhin.sfqd.build; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.RecordComponent; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** Generates and reads the canonical public API signature manifest. */ +public final class PublicApiManifest { + private static final String HEADER = """ + # SFQ(D) core public API signature manifest + # Generated from compiled classes by PublicApiManifest; update only for an intentional API change. + format=1 + """; + + private PublicApiManifest() { + } + + /** + * Prints a manifest for a compiled classes directory. + * + * @param arguments one compiled classes directory + * @throws IOException when the directory cannot be read + * @throws ClassNotFoundException when a compiled class cannot be loaded + */ + public static void main(String[] arguments) throws IOException, ClassNotFoundException { + if (arguments.length != 1) { + throw new IllegalArgumentException("expected one compiled classes directory"); + } + Path classesRoot = Path.of(arguments[0]).toAbsolutePath().normalize(); + URL[] urls = {classesRoot.toUri().toURL()}; + try (URLClassLoader loader = new URLClassLoader(urls, ClassLoader.getPlatformClassLoader())) { + System.out.print(describe(loader, binaryNames(classesRoot))); + } + } + + /** + * Describes every public type and its declared public or protected members. + * + * @param loader isolated loader for the classes being described + * @param binaryNames all binary class names in the artifact + * @return canonical manifest text with LF line endings + * @throws ClassNotFoundException when an artifact class cannot be loaded + */ + public static String describe(ClassLoader loader, Collection binaryNames) + throws ClassNotFoundException { + Set typeLines = new TreeSet<>(); + Set memberLines = new TreeSet<>(); + for (String binaryName : new TreeSet<>(binaryNames)) { + Class type = Class.forName(binaryName, false, loader); + if (!Modifier.isPublic(type.getModifiers())) { + continue; + } + typeLines.add(describeType(type)); + describeConstructors(type, memberLines); + describeFields(type, memberLines); + describeMethods(type, memberLines); + describeRecordComponents(type, memberLines); + } + return HEADER + '\n' + String.join("\n", typeLines) + "\n\n" + + String.join("\n", memberLines) + '\n'; + } + + /** + * Describes all compiled types below a classes directory. + * + * @param loader loader for the classes being described + * @param classesRoot compiled classes directory + * @return canonical manifest text with LF line endings + * @throws IOException when the directory cannot be read + * @throws ClassNotFoundException when a compiled class cannot be loaded + */ + public static String describeDirectory(ClassLoader loader, Path classesRoot) + throws IOException, ClassNotFoundException { + return describe(loader, binaryNames(classesRoot)); + } + + /** + * Reads a checked-in manifest and normalizes platform line endings. + * + * @param path manifest path + * @return manifest text with LF line endings + * @throws IOException when the manifest cannot be read + */ + public static String read(Path path) throws IOException { + return Files.readString(path).replace("\r\n", "\n"); + } + + /** + * Extracts public binary type names from canonical manifest text. + * + * @param manifest canonical manifest text + * @return sorted public binary names + */ + public static Set publicTypeNames(String manifest) { + Set names = new TreeSet<>(); + for (String line : manifest.split("\n")) { + if (line.startsWith("TYPE name=")) { + int end = line.indexOf(' ', "TYPE name=".length()); + if (end < 0) { + throw new IllegalArgumentException("malformed TYPE manifest line: " + line); + } + names.add(line.substring("TYPE name=".length(), end)); + } + } + if (names.isEmpty()) { + throw new IllegalArgumentException("manifest contains no public types"); + } + return names; + } + + /** + * Reports missing and unexpected canonical lines. + * + * @param expected checked-in manifest + * @param actual generated manifest + * @return compact difference description + */ + public static String difference(String expected, String actual) { + Set missing = manifestLines(expected); + Set unexpected = manifestLines(actual); + missing.removeAll(manifestLines(actual)); + unexpected.removeAll(manifestLines(expected)); + if (missing.isEmpty() && unexpected.isEmpty() && !expected.equals(actual)) { + return firstOrderedDifference(expected, actual); + } + return "; missing=" + missing + "; unexpected=" + unexpected; + } + + private static String firstOrderedDifference(String expected, String actual) { + String[] expectedLines = expected.split("\n", -1); + String[] actualLines = actual.split("\n", -1); + int sharedLength = Math.min(expectedLines.length, actualLines.length); + for (int index = 0; index < sharedLength; index++) { + if (!expectedLines[index].equals(actualLines[index])) { + return "; firstDifferenceLine=" + (index + 1) + + "; expected=" + expectedLines[index] + + "; actual=" + actualLines[index]; + } + } + return "; lineCount expected=" + expectedLines.length + "; actual=" + actualLines.length; + } + + private static Collection binaryNames(Path classesRoot) throws IOException { + List names = new ArrayList<>(); + try (Stream files = Files.walk(classesRoot)) { + files.filter(Files::isRegularFile) + .filter(PublicApiManifest::isTypeClassFile) + .map(classesRoot::relativize) + .map(Path::toString) + .map(name -> name.substring(0, name.length() - ".class".length())) + .map(name -> name.replace(classesRoot.getFileSystem().getSeparator(), ".")) + .forEach(names::add); + } + return names; + } + + private static boolean isTypeClassFile(Path path) { + Path fileName = path.getFileName(); + if (fileName == null) { + return false; + } + String name = fileName.toString(); + return name.endsWith(".class") + && !name.equals("module-info.class") + && !name.equals("package-info.class"); + } + + private static String describeType(Class type) { + String superclass = type.getGenericSuperclass() == null + ? "-" : typeName(type.getGenericSuperclass()); + return "TYPE name=" + type.getName() + + " kind=" + kind(type) + + " modifiers=" + typeModifiers(type) + + " typeParameters=" + typeParameters(type.getTypeParameters()) + + " superclass=" + superclass + + " interfaces=" + types(type.getGenericInterfaces()) + + " permits=" + permittedSubclasses(type) + + " synthetic=" + type.isSynthetic(); + } + + private static void describeConstructors(Class type, Set lines) { + for (Constructor constructor : type.getDeclaredConstructors()) { + if (isVisible(constructor.getModifiers())) { + lines.add("CONSTRUCTOR owner=" + type.getName() + + " modifiers=" + executableModifiers(constructor) + + " typeParameters=" + typeParameters(constructor.getTypeParameters()) + + " parameters=" + types(constructor.getGenericParameterTypes()) + + " throws=" + types(constructor.getGenericExceptionTypes())); + } + } + } + + private static void describeFields(Class type, Set lines) { + for (Field field : type.getDeclaredFields()) { + if (isVisible(field.getModifiers())) { + lines.add("FIELD owner=" + type.getName() + + " name=" + field.getName() + + " modifiers=" + fieldModifiers(field) + + " type=" + typeName(field.getGenericType()) + + " enumConstant=" + field.isEnumConstant() + + " synthetic=" + field.isSynthetic()); + } + } + } + + private static void describeMethods(Class type, Set lines) { + for (Method method : type.getDeclaredMethods()) { + if (isVisible(method.getModifiers())) { + lines.add("METHOD owner=" + type.getName() + + " name=" + method.getName() + + " modifiers=" + methodModifiers(method) + + " typeParameters=" + typeParameters(method.getTypeParameters()) + + " return=" + typeName(method.getGenericReturnType()) + + " parameters=" + types(method.getGenericParameterTypes()) + + " throws=" + types(method.getGenericExceptionTypes())); + } + } + } + + private static void describeRecordComponents(Class type, Set lines) { + RecordComponent[] components = type.getRecordComponents(); + if (components == null) { + return; + } + for (RecordComponent component : components) { + lines.add("RECORD_COMPONENT owner=" + type.getName() + + " name=" + component.getName() + + " type=" + typeName(component.getGenericType())); + } + } + + private static String kind(Class type) { + if (type.isAnnotation()) { + return "annotation"; + } + if (type.isEnum()) { + return "enum"; + } + if (type.isRecord()) { + return "record"; + } + if (type.isInterface()) { + return "interface"; + } + return "class"; + } + + private static String typeModifiers(Class type) { + List modifiers = baseModifiers(type.getModifiers()); + if (type.isSealed()) { + modifiers.add("sealed"); + } else if (!Modifier.isFinal(type.getModifiers()) && directlyExtendsSealedType(type)) { + modifiers.add("non-sealed"); + } + return list(modifiers); + } + + private static boolean directlyExtendsSealedType(Class type) { + if (type.getSuperclass() != null && type.getSuperclass().isSealed()) { + return true; + } + return Arrays.stream(type.getInterfaces()).anyMatch(Class::isSealed); + } + + private static String fieldModifiers(Field field) { + List modifiers = baseModifiers(field.getModifiers()); + if (Modifier.isTransient(field.getModifiers())) { + modifiers.add("transient"); + } + if (Modifier.isVolatile(field.getModifiers())) { + modifiers.add("volatile"); + } + return list(modifiers); + } + + private static String executableModifiers(Executable executable) { + List modifiers = baseModifiers(executable.getModifiers()); + if (executable instanceof Method method) { + if (Modifier.isSynchronized(method.getModifiers())) { + modifiers.add("synchronized"); + } + if (Modifier.isNative(method.getModifiers())) { + modifiers.add("native"); + } + if (method.isDefault()) { + modifiers.add("default"); + } + if (method.isBridge()) { + modifiers.add("bridge"); + } + } + if (executable.isVarArgs()) { + modifiers.add("varargs"); + } + if (executable.isSynthetic()) { + modifiers.add("synthetic"); + } + return list(modifiers); + } + + private static String methodModifiers(Method method) { + return executableModifiers(method); + } + + private static List baseModifiers(int modifiers) { + List result = new ArrayList<>(); + if (Modifier.isPublic(modifiers)) { + result.add("public"); + } else if (Modifier.isProtected(modifiers)) { + result.add("protected"); + } else if (Modifier.isPrivate(modifiers)) { + result.add("private"); + } + if (Modifier.isAbstract(modifiers)) { + result.add("abstract"); + } + if (Modifier.isStatic(modifiers)) { + result.add("static"); + } + if (Modifier.isFinal(modifiers)) { + result.add("final"); + } + if (Modifier.isStrict(modifiers)) { + result.add("strictfp"); + } + return result; + } + + private static String permittedSubclasses(Class type) { + Class[] subclasses = type.getPermittedSubclasses(); + if (subclasses == null) { + return "[]"; + } + return Arrays.stream(subclasses) + .map(Class::getName) + .sorted() + .collect(Collectors.joining(", ", "[", "]")); + } + + private static String typeParameters(TypeVariable[] variables) { + return Arrays.stream(variables) + .map(PublicApiManifest::typeParameter) + .collect(Collectors.joining(", ", "[", "]")); + } + + private static String typeParameter(TypeVariable variable) { + Type[] bounds = variable.getBounds(); + if (bounds.length == 1 && bounds[0].equals(Object.class)) { + return variable.getName(); + } + return variable.getName() + " extends " + Arrays.stream(bounds) + .map(PublicApiManifest::typeName) + .collect(Collectors.joining(" & ")); + } + + private static String types(Type[] types) { + return Arrays.stream(types) + .map(PublicApiManifest::typeName) + .collect(Collectors.joining(", ", "[", "]")); + } + + private static String typeName(Type type) { + return type.getTypeName(); + } + + private static String list(List values) { + return '[' + String.join(", ", values) + ']'; + } + + private static boolean isVisible(int modifiers) { + return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers); + } + + private static Set manifestLines(String manifest) { + return new TreeSet<>(Arrays.asList(manifest.split("\n"))); + } +} diff --git a/tools/verify-javadoc-gate.sh b/tools/verify-javadoc-gate.sh index b33bc5b..e04c451 100755 --- a/tools/verify-javadoc-gate.sh +++ b/tools/verify-javadoc-gate.sh @@ -55,6 +55,25 @@ is_javadoc_failure() { grep -Fq 'UndocumentedPublicApi.java:7: warning: no comment' "$log" } +is_artifact_api_failure() { + local log=$1 + grep -Fq \ + 'exec-maven-plugin:3.6.3:java (verify-published-core-artifacts)' \ + "$log" && + grep -Fq \ + 'binary JAR public API signatures differ from the checked-in manifest' \ + "$log" && + grep -Fq \ + 'TYPE name=io.github.pzhin.sfqd.UndocumentedPublicApi kind=class' \ + "$log" && + grep -Fq \ + 'CONSTRUCTOR owner=io.github.pzhin.sfqd.UndocumentedPublicApi modifiers=[public]' \ + "$log" && + grep -Fq \ + 'METHOD owner=io.github.pzhin.sfqd.UndocumentedPublicApi name=value modifiers=[public]' \ + "$log" +} + if run_mutated_build "$LOG"; then echo "ERROR: undocumented public API passed the JavaDoc gate" >&2 exit 1 @@ -77,12 +96,7 @@ if is_javadoc_failure "$BYPASS_LOG"; then exit 1 fi -if ! grep -Fq \ - 'exec-maven-plugin:3.6.3:java (verify-published-core-artifacts)' \ - "$BYPASS_LOG" || - ! grep -Fq \ - 'binary JAR public type surface differs from the specified API' \ - "$BYPASS_LOG"; then +if ! is_artifact_api_failure "$BYPASS_LOG"; then echo "ERROR: disabled-gate mutation failed for an unexpected reason" >&2 sed -n '1,240p' "$BYPASS_LOG" >&2 exit 1