diff --git a/allure-assertj/build.gradle.kts b/allure-assertj/build.gradle.kts index 0e32e5b3b..6b285a027 100644 --- a/allure-assertj/build.gradle.kts +++ b/allure-assertj/build.gradle.kts @@ -18,14 +18,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.assertj" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-assertj/src/main/java/module-info.java b/allure-assertj/src/main/java/module-info.java new file mode 100644 index 000000000..f0a8ffbe0 --- /dev/null +++ b/allure-assertj/src/main/java/module-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.assertj { + requires transitive io.qameta.allure.commons; + requires org.aspectj.runtime; + requires org.assertj.core; + + exports io.qameta.allure.assertj; + + provides io.qameta.allure.listener.FixtureLifecycleListener with io.qameta.allure.assertj.AssertJLifecycleListener; + provides io.qameta.allure.listener.TestLifecycleListener with io.qameta.allure.assertj.AssertJLifecycleListener; +} diff --git a/allure-awaitility/build.gradle.kts b/allure-awaitility/build.gradle.kts index b48443834..bbe0f2f5c 100644 --- a/allure-awaitility/build.gradle.kts +++ b/allure-awaitility/build.gradle.kts @@ -20,14 +20,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.awaitility" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-awaitility/src/main/java/module-info.java b/allure-awaitility/src/main/java/module-info.java new file mode 100644 index 000000000..e9a7968db --- /dev/null +++ b/allure-awaitility/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.awaitility { + requires transitive io.qameta.allure.commons; + requires awaitility; + + exports io.qameta.allure.awaitility; +} diff --git a/allure-bom/build.gradle.kts b/allure-bom/build.gradle.kts index 4e9722114..13cebfa89 100644 --- a/allure-bom/build.gradle.kts +++ b/allure-bom/build.gradle.kts @@ -8,7 +8,7 @@ val scalaTestBinaryVersions = listOf("2.12", "2.13", "3") dependencies { constraints { - rootProject.subprojects.sorted().forEach { + rootProject.subprojects.filter { it.plugins.hasPlugin("maven-publish") }.sorted().forEach { if (it.name == "allure-scalatest") { scalaTestBinaryVersions.forEach { scalaBinaryVersion -> api("${it.group}:${it.name}_$scalaBinaryVersion:${it.version}") diff --git a/allure-cucumber7-jvm/build.gradle.kts b/allure-cucumber7-jvm/build.gradle.kts index 9a644cd56..8f6586c8f 100644 --- a/allure-cucumber7-jvm/build.gradle.kts +++ b/allure-cucumber7-jvm/build.gradle.kts @@ -4,6 +4,7 @@ val cucumberVersion = "7.34.7" val minimumCucumberVersion = "7.3.0" dependencies { + implementation("org.slf4j:slf4j-api") api(project(":allure-java-commons")) compileOnly(platform("io.cucumber:cucumber-bom:$cucumberVersion")) compileOnly("io.cucumber:cucumber-plugin") @@ -24,16 +25,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes( - mapOf( - "Automatic-Module-Name" to "io.qameta.allure.cucumber7jvm" - ) - ) - } -} - tasks.test { useJUnitPlatform() systemProperty("allure.test.cucumber.version", cucumberVersion) diff --git a/allure-cucumber7-jvm/src/main/java/module-info.java b/allure-cucumber7-jvm/src/main/java/module-info.java new file mode 100644 index 000000000..d7f072e0f --- /dev/null +++ b/allure-cucumber7-jvm/src/main/java/module-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.cucumber7jvm { + requires transitive io.qameta.allure.commons; + requires io.cucumber.plugin; + requires io.cucumber.gherkin; + requires io.cucumber.messages; + requires org.slf4j; + + exports io.qameta.allure.cucumber7jvm; +} diff --git a/allure-descriptions-javadoc/build.gradle.kts b/allure-descriptions-javadoc/build.gradle.kts index 18b4210fc..391baac72 100644 --- a/allure-descriptions-javadoc/build.gradle.kts +++ b/allure-descriptions-javadoc/build.gradle.kts @@ -16,16 +16,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes( - mapOf( - "Automatic-Module-Name" to "io.qameta.allure.description" - ) - ) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-descriptions-javadoc/src/main/java/module-info.java b/allure-descriptions-javadoc/src/main/java/module-info.java new file mode 100644 index 000000000..587c52f63 --- /dev/null +++ b/allure-descriptions-javadoc/src/main/java/module-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.description { + requires java.compiler; + + exports io.qameta.allure.description; + + provides javax.annotation.processing.Processor with io.qameta.allure.description.JavaDocDescriptionsProcessor; +} diff --git a/allure-grpc/build.gradle.kts b/allure-grpc/build.gradle.kts index 044c50317..feb7a2ead 100644 --- a/allure-grpc/build.gradle.kts +++ b/allure-grpc/build.gradle.kts @@ -12,6 +12,8 @@ val grpcVersion = "1.84.0" val protobufVersion = "4.36.1" dependencies { + testImplementation("com.fasterxml.jackson.core:jackson-databind") + implementation("org.slf4j:slf4j-api") agent("org.aspectj:aspectjweaver") api(project(":allure-java-commons")) compileOnly("com.fasterxml.jackson.core:jackson-annotations") @@ -35,14 +37,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.grpc" - )) - } -} - tasks.test { useJUnitPlatform() doFirst { diff --git a/allure-grpc/src/main/java/module-info.java b/allure-grpc/src/main/java/module-info.java new file mode 100644 index 000000000..1ca616e86 --- /dev/null +++ b/allure-grpc/src/main/java/module-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.grpc { + requires transitive io.qameta.allure.commons; + requires io.grpc; + requires com.google.protobuf; + requires com.google.protobuf.util; + requires org.slf4j; + + exports io.qameta.allure.grpc; +} diff --git a/allure-hamcrest/build.gradle.kts b/allure-hamcrest/build.gradle.kts index 2ad7e8d3f..bd4fb7256 100644 --- a/allure-hamcrest/build.gradle.kts +++ b/allure-hamcrest/build.gradle.kts @@ -16,14 +16,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.hamcrest" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-hamcrest/src/main/java/module-info.java b/allure-hamcrest/src/main/java/module-info.java new file mode 100644 index 000000000..333e40490 --- /dev/null +++ b/allure-hamcrest/src/main/java/module-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.hamcrest { + requires transitive io.qameta.allure.commons; + requires org.aspectj.runtime; + requires org.hamcrest; + + exports io.qameta.allure.hamcrest; +} diff --git a/allure-httpclient/build.gradle.kts b/allure-httpclient/build.gradle.kts index 458c178fe..a1faa4c9b 100644 --- a/allure-httpclient/build.gradle.kts +++ b/allure-httpclient/build.gradle.kts @@ -19,14 +19,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.httpclient" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-httpclient/src/main/java/module-info.java b/allure-httpclient/src/main/java/module-info.java new file mode 100644 index 000000000..e33c9ccd3 --- /dev/null +++ b/allure-httpclient/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.httpclient { + requires transitive io.qameta.allure.commons; + requires org.apache.httpcomponents.httpcore; + + exports io.qameta.allure.httpclient; +} diff --git a/allure-httpclient5/build.gradle.kts b/allure-httpclient5/build.gradle.kts index 6cc212641..acfcc4723 100644 --- a/allure-httpclient5/build.gradle.kts +++ b/allure-httpclient5/build.gradle.kts @@ -19,14 +19,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.httpclient5" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-httpclient5/src/main/java/module-info.java b/allure-httpclient5/src/main/java/module-info.java new file mode 100644 index 000000000..ff618236b --- /dev/null +++ b/allure-httpclient5/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.httpclient5 { + requires transitive io.qameta.allure.commons; + requires org.apache.httpcomponents.core5.httpcore5; + + exports io.qameta.allure.httpclient5; +} diff --git a/allure-java-commons-test/build.gradle.kts b/allure-java-commons-test/build.gradle.kts index 29e3943b3..638d3db46 100644 --- a/allure-java-commons-test/build.gradle.kts +++ b/allure-java-commons-test/build.gradle.kts @@ -1,12 +1,11 @@ description = "Allure Java Commons Test Utils" dependencies { - api("commons-io:commons-io") + implementation("commons-io:commons-io") api("io.github.benas:random-beans") - api("org.apache.commons:commons-lang3") + implementation("org.apache.commons:commons-lang3") api(project(":allure-java-commons")) - api("org.junit.jupiter:junit-jupiter-api") - implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("org.junit.jupiter:junit-jupiter-api") testImplementation("org.assertj:assertj-core") testImplementation(project(":allure-assertj")) testImplementation(project(":allure-junit-platform")) @@ -14,14 +13,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.commonstest" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java b/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java index a94a75d41..487dfb24f 100644 --- a/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java +++ b/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java @@ -15,32 +15,21 @@ */ package io.qameta.allure.test; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; import io.qameta.allure.Allure; import io.qameta.allure.AllureConstants; import io.qameta.allure.AttachmentOptions; +import io.qameta.allure.internal.json.JsonSupport; import io.qameta.allure.model.Parameter; -import io.qameta.allure.model.Stage; -import io.qameta.allure.model.Status; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.util.Comparator; import java.util.List; -import java.util.Locale; import java.util.Objects; import java.util.UUID; import java.util.stream.Stream; -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_DEFAULT; -import static com.fasterxml.jackson.databind.MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME; import static io.qameta.allure.util.ResultsUtils.md5; /** @@ -55,18 +44,6 @@ public final class AllureTestCommonsUtils { private static final String JSON_TYPE = "application/json"; private static final String TEXT_EXTENSION = "txt"; private static final String TEXT_TYPE = "text/plain"; - private static final ObjectWriter WRITER = JsonMapper - .builder() - .configure(USE_WRAPPER_NAME_AS_PROPERTY_NAME, true) - .serializationInclusion(NON_DEFAULT) - .build() - .registerModule( - new SimpleModule() - .addSerializer(Status.class, new StatusSerializer()) - .addSerializer(Stage.class, new StageSerializer()) - .addSerializer(Parameter.Mode.class, new ParameterModeSerializer()) - ) - .writerWithDefaultPrettyPrinter(); private AllureTestCommonsUtils() { throw new IllegalStateException("do not instance"); @@ -81,9 +58,9 @@ public static void attach(final AllureResults allureResults) { Allure.attachment( testResult.getUuid() + AllureConstants.TEST_RESULT_FILE_SUFFIX, JSON_TYPE, - WRITER.writeValueAsString(testResult) + JsonSupport.writeModel(testResult) ); - } catch (JsonProcessingException e) { + } catch (IOException e) { throw new UncheckedIOException(e); } }); @@ -93,9 +70,9 @@ public static void attach(final AllureResults allureResults) { Allure.attachment( container.getUuid() + AllureConstants.TEST_RESULT_CONTAINER_FILE_SUFFIX, JSON_TYPE, - WRITER.writeValueAsString(container) + JsonSupport.writeModel(container) ); - } catch (JsonProcessingException e) { + } catch (IOException e) { throw new UncheckedIOException(e); } }); @@ -105,9 +82,9 @@ public static void attach(final AllureResults allureResults) { Allure.attachment( UUID.randomUUID() + AllureConstants.GLOBALS_FILE_SUFFIX, JSON_TYPE, - WRITER.writeValueAsString(globals) + JsonSupport.writeModel(globals) ); - } catch (JsonProcessingException e) { + } catch (IOException e) { throw new UncheckedIOException(e); } }); @@ -173,55 +150,4 @@ private static String extension(final String fileName) { return fileName.substring(index + 1); } - /** - * Parameter mode serializer. - */ - private static class ParameterModeSerializer extends StdSerializer { - protected ParameterModeSerializer() { - super(Parameter.Mode.class); - } - - @Override - public void serialize(final Parameter.Mode value, - final JsonGenerator gen, - final SerializerProvider provider) - throws IOException { - gen.writeString(value.name().toLowerCase(Locale.ENGLISH)); - } - } - - /** - * Stage serializer. - */ - private static class StageSerializer extends StdSerializer { - protected StageSerializer() { - super(Stage.class); - } - - @Override - public void serialize(final Stage value, - final JsonGenerator gen, - final SerializerProvider provider) - throws IOException { - gen.writeString(value.name().toLowerCase(Locale.ENGLISH)); - } - } - - /** - * Status serializer. - */ - private static class StatusSerializer extends StdSerializer { - protected StatusSerializer() { - super(Status.class); - } - - @Override - public void serialize(final Status value, - final JsonGenerator gen, - final SerializerProvider provider) - throws IOException { - gen.writeString(value.name().toLowerCase(Locale.ENGLISH)); - } - } - } diff --git a/allure-java-commons-test/src/main/java/module-info.java b/allure-java-commons-test/src/main/java/module-info.java new file mode 100644 index 000000000..9396d8475 --- /dev/null +++ b/allure-java-commons-test/src/main/java/module-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.commonstest { + requires transitive io.qameta.allure.commons; + requires org.junit.jupiter.api; + requires transitive random.beans; + requires org.apache.commons.io; + requires org.apache.commons.lang3; + + exports io.qameta.allure.test; +} diff --git a/allure-java-commons/README.md b/allure-java-commons/README.md index 06b8b65f7..5d1786906 100644 --- a/allure-java-commons/README.md +++ b/allure-java-commons/README.md @@ -82,8 +82,12 @@ The builder applies redaction and truncation before the exchange is attached. Co - Lifecycle listener interfaces. - File-system result writer utilities. - HTTP request/response capture model. -- Test-plan reader and selection helpers in `io.qameta.allure.testfilter`. +- Test-plan reader and selection helpers used internally by the framework adapters. ## What To Expect This module writes result data only when your code or an adapter calls the runtime API. In ordinary test suites, add a framework adapter first and use `Allure.step(...)`, `Allure.attachment(...)`, and metadata annotations for extra report detail. + +## Module Access + +See [Module API boundaries](../docs/module-api.md) for public packages, shared internals, and dependency scopes. diff --git a/allure-java-commons/build.gradle.kts b/allure-java-commons/build.gradle.kts index 4160e0bb8..815ff95b5 100644 --- a/allure-java-commons/build.gradle.kts +++ b/allure-java-commons/build.gradle.kts @@ -4,8 +4,13 @@ plugins { description = "Allure Java Commons" +shadow { + // Only the final JAR, with our descriptor added, belongs in the publication. + addShadowVariantIntoJavaComponent.set(false) +} + dependencies { - api("org.slf4j:slf4j-api") + implementation("org.slf4j:slf4j-api") api(project(":allure-model")) compileOnly("org.aspectj:aspectjrt") internal("com.fasterxml.jackson.core:jackson-databind") @@ -24,11 +29,37 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } +// Compile the descriptor separately: Jackson is relocated into this module in the published JAR. +val compileModuleInfo = tasks.register("compileModuleInfo") { + source("src/main/module/module-info.java") + classpath = configurations.compileClasspath.get() + destinationDirectory.set(layout.buildDirectory.dir("classes/module-info")) + options.release.set(17) + modularity.inferModulePath.set(false) + inputs.files(sourceSets.main.get().output).withPropertyName("patchedClasses") + options.compilerArgumentProviders.add(org.gradle.process.CommandLineArgumentProvider { + listOf("--module-path", classpath.asPath) + }) + dependsOn(tasks.classes) + options.compilerArgs.addAll(listOf( + "--patch-module", "io.qameta.allure.commons=${sourceSets.main.get().output.asPath}" + )) +} + +val modularJar = tasks.register("modularJar") { + archiveClassifier.set("") + from(tasks.shadowJar.map { zipTree(it.archiveFile) }) { + exclude("META-INF/MANIFEST.MF") + } + from(compileModuleInfo) + manifest.from(tasks.shadowJar.get().manifest) +} + configurations.forEach { configuration -> configuration.outgoing.apply { val removed = artifacts.removeAll { it.classifier.isNullOrEmpty() } if (removed) { - artifact(tasks.shadowJar) { + artifact(modularJar) { classifier = "" } } @@ -37,12 +68,12 @@ configurations.forEach { configuration -> tasks { jar { - dependsOn(shadowJar) + dependsOn(modularJar) enabled = false } shadowJar { - archiveClassifier.set("") + archiveClassifier.set("shadow") configurations = project.configurations.runtimeClasspath.map { listOf(it) } relocate("com.fasterxml.jackson", "io.qameta.allure.internal.shadowed.jackson") dependencies { @@ -55,18 +86,21 @@ tasks { attributes(mapOf( "Specification-Title" to project.name, "Implementation-Title" to project.name, - "Implementation-Version" to project.version, - "Automatic-Module-Name" to "io.qameta.allure.commons" + "Implementation-Version" to project.version )) } } assemble { - dependsOn(shadowJar) + dependsOn(modularJar) } test { - dependsOn(shadowJar) + dependsOn(modularJar) useJUnitPlatform() } + + sourcesJar { + from("src/main/module") + } } diff --git a/allure-java-commons/src/main/java/io/qameta/allure/internal/json/JsonSupport.java b/allure-java-commons/src/main/java/io/qameta/allure/internal/json/JsonSupport.java new file mode 100644 index 000000000..e1240044c --- /dev/null +++ b/allure-java-commons/src/main/java/io/qameta/allure/internal/json/JsonSupport.java @@ -0,0 +1,61 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.internal.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import io.qameta.allure.internal.Allure2ModelJackson; + +import java.io.IOException; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_DEFAULT; + +/** + * Internal serialization shared by Allure modules without exposing the bundled Jackson API. + */ +public final class JsonSupport { + + private static final ObjectWriter JSON = new ObjectMapper().writer(); + private static final ObjectWriter MODEL = Allure2ModelJackson.createMapper() + .setSerializationInclusion(NON_DEFAULT) + .writerWithDefaultPrettyPrinter(); + + private JsonSupport() { + throw new IllegalStateException("Do not instance JsonSupport"); + } + + /** + * Serializes JSON values such as maps, lists, strings, numbers, booleans, and null. + * + * @param value the JSON-compatible value + * @return the serialized JSON + * @throws IOException if serialization fails + */ + public static String writeJson(final Object value) throws IOException { + return JSON.writeValueAsString(value); + } + + /** + * Serializes Allure model evidence using the test utilities' compact field inclusion and enum values. + * + * @param value the Allure model value + * @return formatted JSON without default-valued fields + * @throws IOException if serialization fails + */ + public static String writeModel(final Object value) throws IOException { + return MODEL.writeValueAsString(value); + } +} diff --git a/allure-java-commons/src/main/java/io/qameta/allure/util/ServiceLoaderUtils.java b/allure-java-commons/src/main/java/io/qameta/allure/util/ServiceLoaderUtils.java index 7ca753171..87dbf2a9e 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/util/ServiceLoaderUtils.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/util/ServiceLoaderUtils.java @@ -46,6 +46,8 @@ private ServiceLoaderUtils() { * @return loaded implementations. */ public static List load(final Class type, final ClassLoader classLoader) { + // This public helper also accepts service types unknown to the commons module descriptor. + ServiceLoaderUtils.class.getModule().addUses(type); final List loaded = new ArrayList<>(); final Iterator iterator = ServiceLoader.load(type, classLoader).iterator(); while (hasNextSafely(iterator)) { diff --git a/allure-java-commons/src/main/module/module-info.java b/allure-java-commons/src/main/module/module-info.java new file mode 100644 index 000000000..7aa8abc41 --- /dev/null +++ b/allure-java-commons/src/main/module/module-info.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Friend modules depend on commons and cannot be present while this descriptor is compiled. +@SuppressWarnings("module") module io.qameta.allure.commons { + requires transitive io.qameta.allure.model; + requires org.slf4j; + requires static org.aspectj.runtime; + requires java.management; + // Preserve the bundled Jackson implementation's required and optional JDK dependencies. + requires java.logging; + requires static java.desktop; + requires static java.sql; + requires static java.xml; + + exports io.qameta.allure; + exports io.qameta.allure.aspects; + exports io.qameta.allure.http; + exports io.qameta.allure.listener; + exports io.qameta.allure.testfilter to + io.qameta.allure.junitplatform, + io.qameta.allure.junit4, + io.qameta.allure.spock2, + io.qameta.allure.testng; + exports io.qameta.allure.internal.json to + io.qameta.allure.commonstest, + io.qameta.allure.jsonunit; + exports io.qameta.allure.util; + + uses io.qameta.allure.listener.ContainerLifecycleListener; + uses io.qameta.allure.listener.TestLifecycleListener; + uses io.qameta.allure.listener.FixtureLifecycleListener; + uses io.qameta.allure.listener.StepLifecycleListener; +} diff --git a/allure-java-httpclient/build.gradle.kts b/allure-java-httpclient/build.gradle.kts index 7823b68c5..664db34c0 100644 --- a/allure-java-httpclient/build.gradle.kts +++ b/allure-java-httpclient/build.gradle.kts @@ -1,6 +1,7 @@ description = "Allure Java HTTP Client Integration" dependencies { + implementation("org.slf4j:slf4j-api") api(project(":allure-java-commons")) testImplementation("org.wiremock:wiremock") testImplementation("org.assertj:assertj-core") @@ -13,16 +14,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes( - mapOf( - "Automatic-Module-Name" to "io.qameta.allure.javahttpclient" - ) - ) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-java-httpclient/src/main/java/module-info.java b/allure-java-httpclient/src/main/java/module-info.java new file mode 100644 index 000000000..e795c46df --- /dev/null +++ b/allure-java-httpclient/src/main/java/module-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.javahttpclient { + requires transitive io.qameta.allure.commons; + requires transitive java.net.http; + requires org.slf4j; + + exports io.qameta.allure.javahttpclient; +} diff --git a/allure-jax-rs/build.gradle.kts b/allure-jax-rs/build.gradle.kts index e4c72d626..edd420a2a 100644 --- a/allure-jax-rs/build.gradle.kts +++ b/allure-jax-rs/build.gradle.kts @@ -19,14 +19,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.jaxrs" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-jax-rs/src/main/java/module-info.java b/allure-jax-rs/src/main/java/module-info.java new file mode 100644 index 000000000..6f3a0192d --- /dev/null +++ b/allure-jax-rs/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.jaxrs { + requires transitive io.qameta.allure.commons; + requires jakarta.ws.rs; + + exports io.qameta.allure.jaxrs; +} diff --git a/allure-jbehave5/build.gradle.kts b/allure-jbehave5/build.gradle.kts index 69a02c705..17bf720cb 100644 --- a/allure-jbehave5/build.gradle.kts +++ b/allure-jbehave5/build.gradle.kts @@ -17,14 +17,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.jbehave5" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-jbehave5/src/main/java/module-info.java b/allure-jbehave5/src/main/java/module-info.java new file mode 100644 index 000000000..93c72e9b9 --- /dev/null +++ b/allure-jbehave5/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.jbehave5 { + requires transitive io.qameta.allure.commons; + requires jbehave.core; + + exports io.qameta.allure.jbehave5; +} diff --git a/allure-jooq/build.gradle.kts b/allure-jooq/build.gradle.kts index a7378dd84..712259e16 100644 --- a/allure-jooq/build.gradle.kts +++ b/allure-jooq/build.gradle.kts @@ -24,14 +24,6 @@ tasks.compileJava { options.release.set(21) } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.jooq" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-jooq/src/main/java/module-info.java b/allure-jooq/src/main/java/module-info.java new file mode 100644 index 000000000..43874d6cd --- /dev/null +++ b/allure-jooq/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.jooq { + requires transitive io.qameta.allure.commons; + requires org.jooq; + + exports io.qameta.allure.jooq; +} diff --git a/allure-jpms-tests/README.md b/allure-jpms-tests/README.md new file mode 100644 index 000000000..e4482166d --- /dev/null +++ b/allure-jpms-tests/README.md @@ -0,0 +1,46 @@ +# Allure JPMS end-to-end tests + +This is a test-only Gradle project. It has no Maven publication and is excluded from the Allure BOM. +`allure-java-commons-test` remains the reusable test-helper library. + +Each JUnit test creates an isolated consumer in a temporary directory: + +1. Copy a small Java project from `src/test/resources/fixtures` into `src`. +2. Copy the scenario's built Allure JARs and framework dependencies into `lib`. +3. Run `javac` and `java` with argument lists chosen by the Java test. +4. Check process exit codes and the generated Allure JSON, metadata, steps, and attachment contents. + +`JavaConsumer` handles files, subprocess timeouts, and command/output attachments. It uses the JDK +running the tests; the minimal-runtime case uses `jlink` from that JDK and launches the resulting runtime. +Child processes have their own working directory and explicit classpath or module +path; they do not inherit the host test's classpath, JVM options, Allure selection, or result destination. +JUnit cleans up temporary directories, and Allure retains the sources, commands, logs, and checked JSON +in the test report. + +Gradle only builds/resolves the input JARs, copies them into test resources, and runs JUnit. There are +no Gradle tasks for compiling or launching individual consumers and no scenario-specific system +properties. Dependency choices, module-path/classpath flags, `--patch-module`, `--add-opens`, and +AspectJ agent options live in `JpmsIntegrationTest`. + +The consumer cases cover core serialization and service discovery, Jupiter and TestNG on both paths, +JUnit 4, Spock with normal and SPI-off JARs, Kotlin API/context propagation, JDK HTTP client capture, +JsonUnit rendering, modular annotation-processor discovery, denied private-field access, patched tests, +optional Jupiter parameter support, AspectJ weaving, and a runtime without `java.desktop`, `java.sql`, or `java.xml`. +Compilation cases also check that ordinary Jupiter consumers need neither launcher nor engine JARs on their compile path. + +`PublishedModulesTest` additionally inspects all 31 modular published JARs and the four SPI-off JARs. +It checks stable names, explicit descriptors, public exports, and matching public service metadata on both paths. +The jOOQ artifact requires Java 21, so its packaging case is visibly disabled on Java 17 and its JAR is only +built for this suite on Java 21+. Other cases retain the Java 17 baseline. +See [module support](../docs/modules.md) for consumer configuration and scope. + +Run from the repository root: + +```sh +allure agent --goal 'Verify JPMS consumer compatibility' \ + --expect-prefix io.qameta.allure.jpms. \ + -- ./gradlew --no-daemon :allure-jpms-tests:cleanTest :allure-jpms-tests:test +``` + +Set `JAVA_HOME` to test another JDK. The project is also included in the normal Gradle `test` task, +so the existing Java 17 and 25 CI matrix runs these cases without a separate workflow. diff --git a/allure-jpms-tests/build.gradle.kts b/allure-jpms-tests/build.gradle.kts new file mode 100644 index 000000000..0c7e9d5d9 --- /dev/null +++ b/allure-jpms-tests/build.gradle.kts @@ -0,0 +1,116 @@ +description = "Allure JPMS End-to-End Tests" + +// JARs are fixture inputs, separate from the JUnit process that runs the assertions. +val consumerJars = configurations.create("consumerJars") { + isCanBeConsumed = false + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR)) + } +} + +// Verify the packaged artifacts, including metadata contributed by Jar and SPI-off tasks. +val publishedJars = configurations.create("publishedJars") { + isCanBeConsumed = false + isTransitive = false + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR)) + } +} + +dependencies { + publishedJars(project(":allure-assertj")) + publishedJars(project(":allure-awaitility")) + publishedJars(project(":allure-cucumber7-jvm")) + publishedJars(project(":allure-descriptions-javadoc")) + publishedJars(project(":allure-grpc")) + publishedJars(project(":allure-hamcrest")) + publishedJars(project(":allure-httpclient")) + publishedJars(project(":allure-httpclient5")) + publishedJars(project(":allure-java-commons")) + publishedJars(project(":allure-java-commons-test")) + publishedJars(project(":allure-java-httpclient")) + publishedJars(project(":allure-jax-rs")) + publishedJars(project(":allure-jbehave5")) + publishedJars(project(":allure-jsonunit")) + publishedJars(project(":allure-junit-platform")) + publishedJars(project(":allure-junit4")) + publishedJars(project(":allure-junit4-aspect")) + publishedJars(project(":allure-jupiter")) + publishedJars(project(":allure-jupiter-assert")) + publishedJars(project(":allure-kotlin-coroutines")) + publishedJars(project(":allure-kotlin-extensions")) + publishedJars(project(":allure-model")) + publishedJars(project(":allure-okhttp3")) + publishedJars(project(":allure-playwright")) + publishedJars(project(":allure-rest-assured")) + publishedJars(project(":allure-selenium-bidi")) + publishedJars(project(":allure-servlet-api")) + publishedJars(project(":allure-spock2")) + publishedJars(project(":allure-spring-web")) + publishedJars(project(":allure-testng")) + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) { + publishedJars(project(":allure-jooq")) + } + + consumerJars(project(":allure-jupiter")) + consumerJars(project(":allure-testng")) + consumerJars(project(":allure-java-httpclient")) + consumerJars(project(":allure-junit4")) + consumerJars(project(":allure-jsonunit")) + consumerJars(project(":allure-kotlin-coroutines")) + consumerJars(project(":allure-spock2")) + consumerJars(project(":allure-descriptions-javadoc")) + consumerJars("junit:junit") + consumerJars("net.javacrumbs.json-unit:json-unit:5.1.2") + // Exercise JsonUnit with either consumer-selected JSON provider. + consumerJars("com.fasterxml.jackson.core:jackson-databind") + consumerJars("com.google.code.gson:gson:2.11.0") + consumerJars("com.google.errorprone:error_prone_annotations:2.27.0") + consumerJars("org.spockframework:spock-core:2.4-groovy-5.0") + consumerJars("org.apache.groovy:groovy:5.1.2") + consumerJars("org.junit.jupiter:junit-jupiter-engine") + consumerJars("org.junit.jupiter:junit-jupiter-params") + // javac needs these static JUnit dependencies as well as its runtime dependencies. + consumerJars("org.apiguardian:apiguardian-api:1.1.2") + consumerJars("org.jspecify:jspecify:1.0.0") + consumerJars("org.testng:testng:7.12.0") + consumerJars("org.slf4j:slf4j-simple") + consumerJars("org.aspectj:aspectjrt") + consumerJars("org.aspectj:aspectjweaver") + + testImplementation(project(":allure-java-commons")) + testImplementation(project(":allure-assertj")) + testImplementation("com.fasterxml.jackson.core:jackson-databind") + testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.junit.jupiter:junit-jupiter-params") + testRuntimeOnly(project(":allure-junit-platform")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.processTestResources { + from(publishedJars) { + into("published-jars") + } + from(consumerJars) { + into("jars") + } + val adapters = listOf("allure-junit-platform", "allure-jupiter", "allure-testng", "allure-spock2") + dependsOn(adapters.map { ":$it:spiOffJar" }) + from(provider { + adapters.map { project(":$it").tasks.named("spiOffJar").get().archiveFile.get() } + }) { + into("jars") + } +} + +tasks.test { + useJUnitPlatform() +} + +tasks.jar { + enabled = false +} diff --git a/allure-jpms-tests/src/test/java/io/qameta/allure/jpms/JavaConsumer.java b/allure-jpms-tests/src/test/java/io/qameta/allure/jpms/JavaConsumer.java new file mode 100644 index 000000000..0b7df0bd7 --- /dev/null +++ b/allure-jpms-tests/src/test/java/io/qameta/allure/jpms/JavaConsumer.java @@ -0,0 +1,179 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.jpms; + +import io.qameta.allure.Allure; +import io.qameta.allure.Step; + +import java.io.File; +import java.io.IOException; +import java.lang.module.ModuleFinder; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A consumer workspace with its own sources, JARs, classes, results, and process logs. + * Child processes receive only the arguments supplied by the test, without the host test classpath. + */ +final class JavaConsumer { + + private final Path directory; + private int commandIndex; + + JavaConsumer(final Path directory, final String fixture) throws Exception { + this.directory = directory; + copyFixture(fixture); + Files.createDirectories(file("lib")); + Files.createDirectories(results()); + } + + Path file(final String path) { + return directory.resolve(path); + } + + Path results() { + return file("results"); + } + + List sources(final boolean modular) throws IOException { + try (Stream sources = Files.walk(file("src"))) { + return sources.filter(path -> path.toString().endsWith(".java")) + .filter(path -> modular || !path.getFileName().toString().equals("module-info.java")) + .map(directory::relativize).map(Path::toString).sorted().toList(); + } + } + + @Step("Prepare consumer sources: {fixture}") + private void copyFixture(final String fixture) throws Exception { + final Path source = resource("fixtures/" + fixture); + try (Stream paths = Files.walk(source)) { + for (Path input : paths.filter(Files::isRegularFile).sorted().toList()) { + final Path relative = source.relativize(input); + final Path target = file("src").resolve(relative); + Files.createDirectories(target.getParent()); + Files.copy(input, target); + Allure.attachment("src/" + relative, "text/plain", Files.readString(target)); + } + } + } + + @Step("Prepare consumer JARs: {artifacts}") + String jars(final String... artifacts) throws Exception { + final List paths = new ArrayList<>(); + for (String artifact : artifacts) { + final boolean spiOff = artifact.endsWith(":spi-off"); + final String name = artifact.replace(":spi-off", ""); + final Pattern filename = Pattern.compile(Pattern.quote(name) + "-[0-9].*\\.jar"); + final List matches; + try (Stream jars = Files.list(resource("jars"))) { + matches = jars.filter(path -> filename.matcher(path.getFileName().toString()).matches()) + .filter(path -> path.getFileName().toString().endsWith("-spi-off.jar") == spiOff).toList(); + } + assertThat(matches).as("One built JAR for %s", artifact).hasSize(1); + final Path jar = matches.get(0); + assertThat(jar).isRegularFile(); + if (name.startsWith("allure-")) { + final var modules = ModuleFinder.of(jar).findAll(); + assertThat(modules).hasSize(1); + final var descriptor = modules.iterator().next().descriptor(); + assertThat(descriptor.isAutomatic()).as("Explicit module for %s", artifact).isFalse(); + if (spiOff) { + assertThat(descriptor.provides()).as("No service providers in %s", artifact).isEmpty(); + } + } + final Path target = file("lib").resolve(jar.getFileName()); + Files.copy(jar, target, StandardCopyOption.REPLACE_EXISTING); + paths.add(directory.relativize(target).toString()); + } + return String.join(File.pathSeparator, paths); + } + + void javac(final String... arguments) throws Exception { + execute("javac", Path.of(System.getProperty("java.home")), 0, arguments); + } + + String javacFailure(final String... arguments) throws Exception { + return execute("javac", Path.of(System.getProperty("java.home")), 1, arguments); + } + + void jlink(final String... arguments) throws Exception { + execute("jlink", Path.of(System.getProperty("java.home")), 0, arguments); + } + + String java(final String... arguments) throws Exception { + return java(Path.of(System.getProperty("java.home")), arguments); + } + + String java(final Path runtime, final String... arguments) throws Exception { + return execute("java", runtime, 0, arguments); + } + + @Step("Run {tool}") + private String execute(final String tool, final Path runtime, final int expectedExitCode, + final String... arguments) + throws Exception { + final List command = new ArrayList<>(); + command.add(runtime.resolve("bin").resolve(tool).toString()); + command.addAll(List.of(arguments)); + final Path output = file(tool + "-" + ++commandIndex + ".log"); + Allure.attachment( + output.getFileName() + " command", "text/plain", + "Working directory: " + directory + "\nArguments (one per line):\n" + String.join("\n", command) + ); + // Record the actual descriptor, including edits made by the scenario. + if (Files.exists(file("src/module-info.java"))) { + Allure.attachment("module-info.java", "text/plain", Files.readString(file("src/module-info.java"))); + } + final ProcessBuilder builder = new ProcessBuilder(command).directory(directory.toFile()) + .redirectErrorStream(true).redirectOutput(output.toFile()); + // Keep the parent Allure run's selection and result destination out of the child. + builder.environment().remove("ALLURE_TESTPLAN_PATH"); + builder.environment().remove("ALLURE_RESULTS_DIRECTORY"); + // JVM options from the invoking shell must not silently open modules or attach agents. + builder.environment().remove("JDK_JAVA_OPTIONS"); + builder.environment().remove("JAVA_TOOL_OPTIONS"); + builder.environment().remove("_JAVA_OPTIONS"); + builder.environment().remove("CLASSPATH"); + final Process process = builder.start(); + final boolean finished; + try { + finished = process.waitFor(60, TimeUnit.SECONDS); + } finally { + if (process.isAlive()) { + process.destroyForcibly().waitFor(10, TimeUnit.SECONDS); + } + Allure.attachment(output.getFileName().toString(), "text/plain", Files.readString(output)); + } + final String log = Files.readString(output); + assertThat(finished).as("Child process completed: %s", command).isTrue(); + assertThat(process.exitValue()).as("Child process output: %s", log).isEqualTo(expectedExitCode); + return log; + } + + private static Path resource(final String name) throws URISyntaxException { + return Path.of(Objects.requireNonNull(JavaConsumer.class.getResource("/" + name), name).toURI()); + } +} diff --git a/allure-jpms-tests/src/test/java/io/qameta/allure/jpms/JpmsIntegrationTest.java b/allure-jpms-tests/src/test/java/io/qameta/allure/jpms/JpmsIntegrationTest.java new file mode 100644 index 000000000..d87ed3d61 --- /dev/null +++ b/allure-jpms-tests/src/test/java/io/qameta/allure/jpms/JpmsIntegrationTest.java @@ -0,0 +1,650 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.jpms; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.qameta.allure.Allure; +import io.qameta.allure.Description; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +class JpmsIntegrationTest { + + @TempDir + Path directory; + + /** + * A JUnit 4 consumer can register the adapter from a named module and retain test metadata, + * steps, and attachments without opening public test classes for deep reflection. + */ + @Test + @Description + void shouldReportJunit4FromNamedModule() throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "junit4"); + final String dependencies = String.join( + File.pathSeparator, coreDependencies(consumer), + consumer.jars("allure-junit4", "junit", "hamcrest") + ); + runFixture(consumer, dependencies, true); + final JsonNode result = readOnlyResult(consumer.results(), "-result.json"); + assertThat(result.path("status").asText()).isEqualTo("passed"); + assertThat(labels(result)).contains(tuple("owner", "module owner")); + assertThat(result.path("steps").get(0).path("name").asText()).isEqualTo("operation"); + assertAttachment(consumer.results(), result, "JUnit 4 module payload"); + } + + /** + * A named JVM consumer can call the Kotlin extensions and coroutine context API. Restoring the + * captured context on another thread must preserve ownership of its Kotlin step and attachment. + */ + @Test + @Description + void shouldReportThroughKotlinModules() throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "kotlin"); + final String dependencies = String.join( + File.pathSeparator, coreDependencies(consumer), + consumer.jars( + "allure-kotlin-extensions", "allure-kotlin-coroutines", "kotlin-stdlib", + "kotlinx-coroutines-core-jvm" + ) + ); + runFixture(consumer, dependencies, true); + final JsonNode result = readOnlyResult(consumer.results(), "-result.json"); + assertThat(result.path("name").asText()).isEqualTo("Kotlin modules"); + assertThat(result.path("status").asText()).isEqualTo("passed"); + final JsonNode steps = result.path("steps"); + assertThat(steps).hasSize(1); + assertThat(steps.get(0).path("name").asText()).isEqualTo("Kotlin operation"); + assertAttachment(consumer.results(), result, "Kotlin module payload"); + } + + /** + * A named JsonUnit consumer can compare different JSON documents and render its HTML attachment + * through Jackson and FreeMarker, including reflective access to the public difference model. + */ + @Test + @Description + void shouldRenderJsonDifferenceFromNamedModule() throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "jsonunit"); + final String dependencies = String.join( + File.pathSeparator, coreDependencies(consumer), + consumer.jars( + "allure-jsonunit", "json-unit", "json-unit-core", "hamcrest", "opentest4j", + "jackson-annotations", "jackson-core", "jackson-databind", "freemarker" + ) + ); + runFixture(consumer, dependencies, true); + final JsonNode result = readOnlyResult(consumer.results(), "-result.json"); + assertThat(result.path("status").asText()).isEqualTo("passed"); + final List attachments = result.findValues("attachments").stream() + .flatMap(node -> java.util.stream.StreamSupport.stream(node.spliterator(), false)).toList(); + assertThat(attachments).hasSize(1); + final JsonNode attachment = attachments.get(0); + assertThat(attachment.path("name").asText()).isEqualTo("JSON difference"); + assertThat(attachment.path("type").asText()).isEqualTo("text/html"); + final String html = Files.readString(consumer.results().resolve(attachment.path("source").asText())); + Allure.attachment("JSON difference", "text/html", html); + assertThat(html).contains( + "var left = {\"value\":2};", "var right = {\"value\":1};", "var delta = {\"value\":[1,2]};" + ); + } + + /** + * JsonUnit consumers using Gson can render Allure's JSON difference attachment without adding + * an external Jackson dependency. Allure uses the private serializer bundled in commons. + */ + @ParameterizedTest(name = "JsonUnit with Gson and no Jackson, module path = {0}") + @ValueSource(booleans = {true, false}) + @Description + void shouldRenderJsonDifferenceWithoutExternalJackson(final boolean modular) throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "jsonunit"); + final Path descriptor = consumer.file("src/module-info.java"); + Files.writeString( + descriptor, Files.readString(descriptor) + .replace("requires com.fasterxml.jackson.databind;", "requires com.google.gson;") + ); + final String dependencies = String.join( + File.pathSeparator, coreDependencies(consumer), + consumer.jars( + "allure-jsonunit", "json-unit", "json-unit-core", "hamcrest", "opentest4j", + "gson", "error_prone_annotations", "freemarker" + ) + ); + assertThat(dependencies).doesNotContain("jackson-"); + runFixture(consumer, dependencies, modular); + final JsonNode result = readOnlyResult(consumer.results(), "-result.json"); + assertThat(result.path("status").asText()).isEqualTo("passed"); + final List attachments = result.findValues("attachments").stream() + .flatMap(node -> java.util.stream.StreamSupport.stream(node.spliterator(), false)).toList(); + assertThat(attachments).hasSize(1); + final JsonNode attachment = attachments.get(0); + final String html = Files.readString(consumer.results().resolve(attachment.path("source").asText())); + Allure.attachment("JSON difference without external Jackson", "text/html", html); + assertThat(html).contains( + "var left = {\"value\":2};", "var right = {\"value\":1};", "var delta = {\"value\":[1,2]};" + ); + } + + /** + * javac discovers the Javadoc processor on the processor module path using its provides + * declaration, and writes the rendered description for an annotated method in a named module. + */ + @Test + @Description + void shouldDiscoverModularAnnotationProcessor() throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "processor"); + final String dependencies = coreDependencies(consumer); + final String processor = consumer.jars("allure-descriptions-javadoc"); + final List arguments = new ArrayList<>( + List.of( + "--release", "17", "--module-path", dependencies, + "--processor-module-path", processor, "-d", "classes" + ) + ); + arguments.addAll(consumer.sources(true)); + consumer.javac(arguments.toArray(String[]::new)); + final List descriptions; + try (Stream files = Files.list(consumer.file("classes/META-INF/allureDescriptions"))) { + descriptions = files.toList(); + } + assertThat(descriptions).hasSize(1); + final String description = Files.readString(descriptions.get(0)); + Allure.attachment("Generated description", "text/html", description); + assertThat(description).contains("Describes a test compiled with a modular annotation processor."); + } + + /** + * A compiled Groovy specification runs in a named module and is reported through Spock's global + * extension discovery. The SPI-off JAR must run the same feature with no Allure report generated. + */ + @ParameterizedTest(name = "Spock module reporting, SPI off = {0}") + @ValueSource(booleans = {false, true}) + @Description + void shouldReportSpockFromNamedModule(final boolean spiOff) throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "spock"); + final String dependencies = String.join( + File.pathSeparator, coreDependencies(consumer), + consumer.jars(spiOff ? "allure-spock2:spi-off" : "allure-spock2"), + consumer.jars( + "spock-core", "groovy", "junit-platform-engine", "junit-platform-commons", + "junit-platform-launcher", "apiguardian-api", "jspecify", "opentest4j", "hamcrest" + ) + ); + consumer.java( + "--class-path", dependencies, "org.codehaus.groovy.tools.FileSystemCompiler", + "--classpath", dependencies, "-d", "classes", "src/fixture/tests/SampleTest.groovy" + ); + final List arguments = new ArrayList<>( + List.of( + "--release", "17", "--module-path", dependencies, + "--patch-module", "allure.fixture=classes", "-d", "classes" + ) + ); + arguments.addAll(consumer.sources(true)); + consumer.javac(arguments.toArray(String[]::new)); + consumer.java( + "-Dallure.results.directory=results", "--module-path", + "classes" + File.pathSeparator + dependencies, "--module", "allure.fixture/fixture.Main" + ); + if (spiOff) { + assertThat(resultFiles(consumer.results(), "-result.json")).isEmpty(); + } else { + final JsonNode result = readOnlyResult(consumer.results(), "-result.json"); + assertThat(result.path("status").asText()).isEqualTo("passed"); + assertThat(labels(result)).contains(tuple("owner", "module owner")); + assertAttachment(consumer.results(), result, "Spock module payload"); + } + } + + /** + * A named HTTP client consumer needs only the Allure integration in its descriptor. The public + * JDK HTTP API and Allure API remain readable, and a real request produces a structured exchange. + */ + @Test + @Description + void shouldCaptureHttpExchangeFromNamedModule() throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "httpclient"); + final String dependencies = String.join( + File.pathSeparator, coreDependencies(consumer), consumer.jars("allure-java-httpclient") + ); + runFixture(consumer, dependencies, true); + final JsonNode result = readOnlyResult(consumer.results(), "-result.json"); + assertThat(result.path("name").asText()).isEqualTo("modular HTTP"); + assertThat(result.path("status").asText()).isEqualTo("passed"); + final JsonNode steps = result.path("steps"); + assertThat(steps).hasSize(1); + assertThat(steps.get(0).path("name").asText()).isEqualTo("HTTP exchange"); + final JsonNode attachments = steps.get(0).path("attachments"); + assertThat(attachments).hasSize(1); + final JsonNode attachment = attachments.get(0); + assertThat(attachment.path("type").asText()).isEqualTo("application/vnd.allure.http+json"); + final String exchange = Files.readString(consumer.results().resolve(attachment.path("source").asText())); + Allure.attachment("HTTP exchange", "application/vnd.allure.http+json", exchange); + final JsonNode payload = new ObjectMapper().readTree(exchange); + assertThat(payload.path("request").path("method").asText()).isEqualTo("GET"); + assertThat(payload.path("response").path("status").asInt()).isEqualTo(200); + assertThat(payload.path("response").path("body").path("value").asText()).isEqualTo("module response"); + } + + /** + * A named consumer can use the core API without AspectJ or external Jackson. All four lifecycle + * services are discovered from its descriptor and can modify the serialized report artifacts. + */ + @Test + @Description + void shouldReportCoreLifecycleFromNamedModule() throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "core"); + final String modulePath = coreDependencies(consumer); + assertThat(modulePath).doesNotContain("aspectj", "jackson-"); + runFixture(consumer, modulePath, true); + final Path results = consumer.results(); + + final JsonNode result = readOnlyResult(results, "-result.json"); + assertThat(result.path("name").asText()).isEqualTo("core"); + assertThat(result.path("status").asText()).isEqualTo("passed"); + assertThat(result.path("steps").get(0).path("name").asText()).isEqualTo("step listener: operation"); + assertThat(labels(result)).contains(tuple("listener", "test"), tuple("custom service", "loaded")); + assertAttachment(results, result, "module payload"); + final JsonNode container = readOnlyResult(results, "-container.json"); + assertThat(container.path("name").asText()).isEqualTo("container listener"); + assertThat(container.path("befores").get(0).path("description").asText()).isEqualTo("fixture listener"); + } + + /** + * A minimal Java runtime can write Allure results, steps, and attachments without the desktop, + * SQL, or XML modules used only by optional features of the bundled Jackson implementation. + */ + @Test + @Description + void shouldReportWithoutOptionalJdkModules() throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "core"); + final String dependencies = coreDependencies(consumer); + compileFixture(consumer, dependencies, true); + consumer.jlink("--add-modules", "java.base,java.logging,java.management", "--output", "runtime"); + final Path runtime = consumer.file("runtime"); + final String modules = consumer.java(runtime, "--list-modules"); + assertThat(modules.lines().map(line -> line.split("@")[0]).toList()) + .contains("java.base", "java.logging", "java.management") + .doesNotContain("java.desktop", "java.sql", "java.xml"); + + consumer.java( + runtime, "-Dallure.results.directory=results", + "--module-path", "classes" + File.pathSeparator + dependencies, + "--module", "allure.fixture/fixture.Main" + ); + final Path results = consumer.results(); + final JsonNode result = readOnlyResult(results, "-result.json"); + assertThat(result.path("name").asText()).isEqualTo("core"); + assertThat(result.path("status").asText()).isEqualTo("passed"); + assertThat(result.path("steps").get(0).path("name").asText()).isEqualTo("step listener: operation"); + assertAttachment(results, result, "module payload"); + } + + /** + * Jupiter and the Allure extension discover one parameterized test through their normal SPIs. + * Both launch modes preserve parameters, repeated custom annotations, steps, and attachments. + */ + @ParameterizedTest(name = "Jupiter reporting, module path = {0}") + @ValueSource(booleans = {true, false}) + @Description + void shouldReportJupiter(final boolean modular) throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "jupiter"); + runFixture(consumer, jupiterDependencies(consumer, false), modular); + final Path results = consumer.results(); + final JsonNode result = readOnlyResult(results, "-result.json"); + + assertThat(result.path("status").asText()).isEqualTo("passed"); + assertThat(labels(result)).contains( + tuple("owner", "module owner"), tuple("team", "first team"), tuple("team", "second team") + ); + assertThat(result.path("parameters").findValuesAsText("value")).contains("argument value"); + assertThat(result.path("steps").get(0).path("name").asText()).isEqualTo("operation"); + assertAttachment(results, result, "argument value"); + } + + /** + * Ordinary consumers can compile tests using the Jupiter extension without the launcher or + * engine on their compile path. Those dependencies belong to the runner or a custom listener. + */ + @ParameterizedTest(name = "Jupiter API without launcher, module path = {0}") + @ValueSource(booleans = {true, false}) + @Description + void shouldCompileJupiterConsumerWithoutLauncher(final boolean modular) throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "jupiter-api"); + final String dependencies = String.join( + File.pathSeparator, coreDependencies(consumer), + consumer.jars( + "allure-jupiter", "allure-junit-platform", "junit-jupiter-api", + "junit-platform-commons", "apiguardian-api", "opentest4j", "jspecify" + ) + ); + compileFixture(consumer, dependencies, modular); + assertThat(consumer.file("classes/fixture/SampleTest.class")).isRegularFile(); + } + + /** + * Applications can use the commons runtime API supplied by a reporter without putting the + * reporter's framework, runner, or logging implementation on their compile module path. + */ + @ParameterizedTest(name = "Commons API through {0} without framework dependencies") + @ValueSource(strings = {"junit-platform", "jupiter", "junit4", "testng", "spock2"}) + @Description + void shouldCompileRuntimeApiWithoutFrameworkDependencies(final String adapter) throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "consumer-api"); + final Path descriptor = consumer.file("src/module-info.java"); + Files.writeString( + descriptor, Files.readString(descriptor).replace( + "io.qameta.allure.commons", "io.qameta.allure." + adapter.replace("-", "") + ) + ); + final String dependencies = consumer.jars("allure-model", "allure-java-commons", "allure-" + adapter); + compileFixture(consumer, dependencies, true); + assertThat(consumer.file("classes/fixture/Main.class")).isRegularFile(); + } + + /** + * Requiring Allure must not grant ordinary consumers access to adapter implementation packages. + * The compiler must reject the internal type even when its owning module and dependencies exist. + */ + @ParameterizedTest(name = "Customer cannot import {0}.{1}") + @CsvSource( + { + "io.qameta.allure.testfilter, TestPlan", + "io.qameta.allure.testng.config, AllureTestNgConfig", + "io.qameta.allure.internal.json, JsonSupport" + } + ) + @Description + void shouldHideInternalPackages(final String packageName, final String type) throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "consumer-api"); + Files.writeString( + consumer.file("src/module-info.java"), + "module allure.fixture { requires io.qameta.allure.testng; }" + ); + final String source = "package fixture; public class Main { private " + packageName + "." + type + " value; }"; + Files.writeString(consumer.file("src/fixture/Main.java"), source); + Allure.attachment("Customer source", "text/plain", source); + final String dependencies = testNgDependencies(consumer, false); + final String diagnostic = consumer.javacFailure( + "--release", "17", "--module-path", dependencies, "-d", "classes", + "src/module-info.java", "src/fixture/Main.java" + ); + assertThat(diagnostic).contains(packageName, "does not export"); + } + + /** + * TestNG discovers Allure through its listener SPI on either path. A private instance parameter + * is included when the test package permits the adapter to access it. + */ + @ParameterizedTest(name = "TestNG reporting, module path = {0}") + @ValueSource(booleans = {true, false}) + @Description + void shouldReportTestNg(final boolean modular) throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "testng"); + runFixture(consumer, testNgDependencies(consumer, false), modular); + final Path results = consumer.results(); + final JsonNode result = readOnlyResult(results, "-result.json"); + + assertThat(result.path("status").asText()).isEqualTo("passed"); + assertThat(labels(result)).contains(tuple("owner", "module owner")); + assertThat(result.path("parameters").findValuesAsText("value")).contains("instance value"); + assertThat(result.path("steps").get(0).path("name").asText()).isEqualTo("operation"); + assertAttachment(results, result, "module payload"); + } + + /** + * An inaccessible private instance parameter must not prevent TestNG from running the test or + * Allure from writing its result. The diagnostic must identify the package that needs opening. + */ + @Test + @Description + void shouldReportTestNgWithoutAccessToPrivateParameter() throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "testng"); + final Path descriptor = consumer.file("src/module-info.java"); + Files.writeString( + descriptor, Files.readString(descriptor) + .replace("org.testng, io.qameta.allure.testng", "org.testng") + ); + final String output = runFixture(consumer, testNgDependencies(consumer, false), true); + final Path results = consumer.results(); + final JsonNode result = readOnlyResult(results, "-result.json"); + + assertThat(result.path("status").asText()).isEqualTo("passed"); + assertThat(result.path("parameters").findValuesAsText("value")).doesNotContain("instance value"); + assertThat(output) + .contains("fixture.tests", "io.qameta.allure.testng"); + } + + /** + * The SPI-off variants remain explicit named modules but do not automatically register Allure + * listeners or extensions. The frameworks still execute their selected tests successfully. + */ + @ParameterizedTest + @ValueSource(strings = {"jupiter", "testng"}) + @Description + void shouldDisableAutomaticRegistration(final String framework) throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, framework); + final String dependencies = "jupiter".equals(framework) + ? jupiterDependencies(consumer, true) + : testNgDependencies(consumer, true); + runFixture(consumer, dependencies, true); + final Path results = consumer.results(); + + assertThat(resultFiles(results, "-result.json")).isEmpty(); + } + + /** + * Tests patched into an application module can use the same adapters without a second descriptor. + * Runtime opens grant only the access needed by Jupiter and Allure. + */ + @Test + @Description + void shouldReportTestsPatchedIntoAnApplicationModule() throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "jupiter"); + final String dependencies = jupiterDependencies(consumer, false); + final Path descriptor = consumer.file("src/module-info.java"); + Files.writeString( + descriptor, Files.readString(descriptor) + .replace(" opens fixture.tests to org.junit.platform.commons, io.qameta.allure.commons, io.qameta.allure.jupiter;", "") + ); + consumer.javac( + "--release", "17", "-d", "application", "--module-path", dependencies, + "src/module-info.java", "src/fixture/Main.java" + ); + consumer.javac( + "--release", "17", "-parameters", "-d", "tests", + "--module-path", "application" + File.pathSeparator + dependencies, + "--patch-module", "allure.fixture=src/fixture/tests", "src/fixture/tests/SampleTest.java" + ); + consumer.java( + "-Dallure.results.directory=results", + "--module-path", "application" + File.pathSeparator + dependencies, + "--patch-module", "allure.fixture=tests", + "--add-opens", "allure.fixture/fixture.tests=org.junit.platform.commons,io.qameta.allure.commons,io.qameta.allure.jupiter", + "--module", "allure.fixture/fixture.Main" + ); + final Path results = consumer.results(); + + final JsonNode result = readOnlyResult(results, "-result.json"); + assertThat(result.path("status").asText()).isEqualTo("passed"); + assertThat(result.path("parameters").findValuesAsText("value")).contains("argument value"); + assertThat(labels(result)).contains(tuple("team", "first team"), tuple("team", "second team")); + assertAttachment(results, result, "argument value"); + } + + /** + * AspectJ can weave an explicit consumer module. Allure resolves a private argument field from + * an opened package and writes the annotated step and attachment through its bundled serializer. + */ + @Test + @Description + void shouldWeaveStepsAndAttachmentsOnModulePath() throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "weaving"); + final String dependencies = coreDependencies(consumer) + File.pathSeparator + consumer.jars("aspectjrt"); + compileFixture(consumer, dependencies, true); + Files.createDirectories(consumer.file("classes/META-INF")); + Files.copy(consumer.file("src/META-INF/aop.xml"), consumer.file("classes/META-INF/aop.xml")); + consumer.java( + "-Dallure.results.directory=results", "-javaagent:" + consumer.jars("aspectjweaver"), + "--add-modules", "jdk.unsupported,java.sql", + "--module-path", "classes" + File.pathSeparator + dependencies, + "--module", "allure.fixture/fixture.Main" + ); + final Path results = consumer.results(); + final JsonNode result = readOnlyResult(results, "-result.json"); + + assertThat(result.path("status").asText()).isEqualTo("passed"); + final JsonNode steps = result.path("steps"); + assertThat(steps).as("Woven steps reported by the AspectJ agent").isNotEmpty(); + assertThat(steps.get(0).path("name").asText()).isEqualTo("operation for module user"); + assertAttachment(results, result, "woven payload"); + } + + /** + * Ordinary Jupiter tests do not need junit-jupiter-params on either path. The Allure extension + * still captures before fixtures when the optional parameter support module is absent. + */ + @Test + @Description + void shouldRunJupiterWithoutOptionalParameterModule() throws Exception { + final JavaConsumer consumer = new JavaConsumer(directory, "jupiter-plain"); + final String dependencies = Arrays.stream(jupiterDependencies(consumer, false).split(File.pathSeparator)) + .filter(path -> !Path.of(path).getFileName().toString().startsWith("junit-jupiter-params-")) + .collect(Collectors.joining(File.pathSeparator)); + runFixture(consumer, dependencies, true); + final Path results = consumer.results(); + final JsonNode result = readOnlyResult(results, "-result.json"); + + assertThat(result.path("status").asText()).isEqualTo("passed"); + assertThat(result.path("parameters")).isEmpty(); + assertAttachment(results, result, "module payload"); + final List beforeSteps = new ArrayList<>(); + for (Path container : resultFiles(results, "-container.json")) { + final String json = Files.readString(container); + Allure.attachment(container.getFileName().toString(), "application/json", json); + beforeSteps.addAll(new ObjectMapper().readTree(json).path("befores").findValuesAsText("name")); + } + assertThat(beforeSteps).contains("prepare operation"); + } + + private static String runFixture(final JavaConsumer consumer, final String dependencies, + final boolean modular) + throws Exception { + compileFixture(consumer, dependencies, modular); + final List arguments = new ArrayList<>( + List.of( + "-Dallure.results.directory=results", + modular ? "--module-path" : "--class-path", "classes" + File.pathSeparator + dependencies + ) + ); + if (modular) { + arguments.addAll(List.of("--module", "allure.fixture/fixture.Main")); + } else { + arguments.add("fixture.Main"); + } + return consumer.java(arguments.toArray(String[]::new)); + } + + private static void compileFixture(final JavaConsumer consumer, final String dependencies, + final boolean modular) + throws Exception { + final List arguments = new ArrayList<>( + List.of( + "--release", "17", "-g", "-parameters", "-d", "classes", + modular ? "--module-path" : "--class-path", dependencies + ) + ); + arguments.addAll(consumer.sources(modular)); + consumer.javac(arguments.toArray(String[]::new)); + } + + private static String coreDependencies(final JavaConsumer consumer) throws Exception { + return consumer.jars("allure-model", "allure-java-commons", "slf4j-api"); + } + + private static String jupiterDependencies(final JavaConsumer consumer, final boolean spiOff) throws Exception { + final String classifier = spiOff ? ":spi-off" : ""; + return String.join( + File.pathSeparator, + coreDependencies(consumer), + consumer.jars("allure-jupiter" + classifier, "allure-junit-platform" + classifier), + consumer.jars( + "junit-jupiter-api", "junit-jupiter-engine", "junit-jupiter-params", + "junit-platform-commons", "junit-platform-engine", "junit-platform-launcher", + "apiguardian-api", "opentest4j", "jspecify" + ) + ); + } + + private static String testNgDependencies(final JavaConsumer consumer, final boolean spiOff) throws Exception { + return String.join( + File.pathSeparator, + coreDependencies(consumer), + consumer.jars(spiOff ? "allure-testng:spi-off" : "allure-testng"), + consumer.jars("testng", "jcommander", "jquery", "slf4j-simple") + ); + } + + private static List resultFiles(final Path results, final String suffix) throws IOException { + try (Stream files = Files.list(results)) { + return files.filter(path -> path.getFileName().toString().endsWith(suffix)) + .collect(Collectors.toList()); + } + } + + private static JsonNode readOnlyResult(final Path results, final String suffix) throws IOException { + final List files = resultFiles(results, suffix); + assertThat(files).as("Written Allure artifacts ending in %s", suffix).hasSize(1); + final String json = Files.readString(files.get(0)); + Allure.attachment(files.get(0).getFileName().toString(), "application/json", json); + return new ObjectMapper().readTree(json); + } + + private static List labels(final JsonNode result) { + final List labels = new ArrayList<>(); + result.path("labels").forEach(label -> labels.add(tuple(label.path("name").asText(), label.path("value").asText()))); + return labels; + } + + private static void assertAttachment(final Path results, final JsonNode result, + final String content) + throws IOException { + final List attachments = result.findValues("attachments").stream() + .flatMap(node -> java.util.stream.StreamSupport.stream(node.spliterator(), false)) + .collect(Collectors.toList()); + assertThat(attachments).hasSize(1); + final JsonNode attachment = attachments.get(0); + assertThat(attachment.path("name").asText()).isEqualTo("payload"); + assertThat(Files.readString(results.resolve(attachment.path("source").asText()))).isEqualTo(content); + } +} diff --git a/allure-jpms-tests/src/test/java/io/qameta/allure/jpms/PublishedModulesTest.java b/allure-jpms-tests/src/test/java/io/qameta/allure/jpms/PublishedModulesTest.java new file mode 100644 index 000000000..17a908132 --- /dev/null +++ b/allure-jpms-tests/src/test/java/io/qameta/allure/jpms/PublishedModulesTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.jpms; + +import io.qameta.allure.Allure; +import io.qameta.allure.Description; +import io.qameta.allure.Step; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleFinder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.jar.JarFile; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class PublishedModulesTest { + + /** + * Every migrated library must publish a real descriptor under its established module name. + * Public service providers must agree with classpath metadata, so switching launch modes preserves discovery. + */ + @ParameterizedTest(name = "{0} publishes {1}") + @CsvSource( + { + "allure-assertj, io.qameta.allure.assertj", + "allure-awaitility, io.qameta.allure.awaitility", + "allure-cucumber7-jvm, io.qameta.allure.cucumber7jvm", + "allure-descriptions-javadoc, io.qameta.allure.description", + "allure-grpc, io.qameta.allure.grpc", + "allure-hamcrest, io.qameta.allure.hamcrest", + "allure-httpclient, io.qameta.allure.httpclient", + "allure-httpclient5, io.qameta.allure.httpclient5", + "allure-java-commons, io.qameta.allure.commons", + "allure-java-commons-test, io.qameta.allure.commonstest", + "allure-java-httpclient, io.qameta.allure.javahttpclient", + "allure-jax-rs, io.qameta.allure.jaxrs", + "allure-jbehave5, io.qameta.allure.jbehave5", + "allure-jsonunit, io.qameta.allure.jsonunit", + "allure-junit-platform, io.qameta.allure.junitplatform", + "allure-junit4, io.qameta.allure.junit4", + "allure-junit4-aspect, io.qameta.allure.junit4aspect", + "allure-jupiter, io.qameta.allure.jupiter", + "allure-jupiter-assert, io.qameta.allure.jupiterassert", + "allure-kotlin-coroutines, io.qameta.allure.kotlin.coroutines", + "allure-kotlin-extensions, io.qameta.allure.kotlin.extensions", + "allure-model, io.qameta.allure.model", + "allure-okhttp3, io.qameta.allure.okhttp3", + "allure-playwright, io.qameta.allure.playwright", + "allure-rest-assured, io.qameta.allure.restassured", + "allure-selenium-bidi, io.qameta.allure.seleniumbidi", + "allure-servlet-api, io.qameta.allure.servletapi", + "allure-spock2, io.qameta.allure.spock2", + "allure-spring-web, io.qameta.allure.springweb", + "allure-testng, io.qameta.allure.testng" + } + ) + @Description + void shouldPublishExplicitModule(final String artifact, final String moduleName) throws Exception { + verifyDescriptor(artifact, moduleName, "published-jars", false); + } + + /** + * The jOOQ adapter retains its Java 21 baseline and publishes a descriptor with its established name. + */ + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + @Description + void shouldPublishJooqModule() throws Exception { + verifyDescriptor("allure-jooq", "io.qameta.allure.jooq", "published-jars", false); + } + + /** + * Alternate adapter JARs must keep the same named-module API while disabling service registration + * on both paths, without accidentally inheriting the normal JAR's provides declarations. + */ + @ParameterizedTest + @ValueSource(strings = {"junit-platform", "jupiter", "testng", "spock2"}) + @Description + void shouldPublishSpiOffModule(final String adapter) throws Exception { + verifyDescriptor("allure-" + adapter, "io.qameta.allure." + adapter.replace("-", ""), "jars", true); + } + + @Step("Inspect published module: {artifact}") + private static void verifyDescriptor(final String artifact, final String moduleName, + final String resources, final boolean spiOff) + throws Exception { + final Path directory = Path.of( + Objects.requireNonNull( + PublishedModulesTest.class.getResource("/" + resources) + ).toURI() + ); + final Pattern name = Pattern.compile(Pattern.quote(artifact) + "-[0-9].*\\.jar"); + final List matches; + try (Stream files = Files.list(directory)) { + matches = files.filter(path -> name.matcher(path.getFileName().toString()).matches()) + .filter(path -> path.getFileName().toString().endsWith("-spi-off.jar") == spiOff).toList(); + } + assertThat(matches).as("Published JAR for %s", artifact).hasSize(1); + final Path jarPath = matches.get(0); + final var modules = ModuleFinder.of(jarPath).findAll(); + assertThat(modules).hasSize(1); + final ModuleDescriptor descriptor = modules.iterator().next().descriptor(); + Allure.attachment("Module descriptor", "text/plain", descriptor.toString()); + assertThat(descriptor.name()).isEqualTo(moduleName); + assertThat(descriptor.isAutomatic()).isFalse(); + assertThat(descriptor.exports()).as("Public API exports").isNotEmpty(); + final Map> classpathProviders = new HashMap<>(); + try (JarFile jar = new JarFile(jarPath.toFile())) { + final String automaticName = jar.getManifest().getMainAttributes().getValue("Automatic-Module-Name"); + assertThat(automaticName).as("The descriptor replaces the automatic name").isNull(); + final var services = jar.stream() + .filter(entry -> !entry.isDirectory() && entry.getName().startsWith("META-INF/services/")) + // Bundled Jackson's relocated SPI is encapsulated and is not an Allure extension point. + .filter(entry -> !entry.getName().startsWith("META-INF/services/io.qameta.allure.internal.shadowed.")) + .toList(); + for (var service : services) { + final String content; + try (var input = jar.getInputStream(service)) { + content = new String(input.readAllBytes(), StandardCharsets.UTF_8); + } + Allure.attachment(service.getName(), "text/plain", content); + classpathProviders.put( + service.getName().substring("META-INF/services/".length()), + content.lines().map(line -> line.split("#", 2)[0].trim()) + .filter(line -> !line.isEmpty()).sorted().toList() + ); + } + } + final Map> moduleProviders = descriptor.provides().stream().collect( + Collectors.toMap( + ModuleDescriptor.Provides::service, provider -> provider.providers().stream().sorted().toList() + ) + ); + assertThat(moduleProviders).as("Consistent module-path and classpath service discovery") + .isEqualTo(classpathProviders); + if (spiOff) { + assertThat(moduleProviders).isEmpty(); + } + } +} diff --git a/allure-jpms-tests/src/test/resources/allure.properties b/allure-jpms-tests/src/test/resources/allure.properties new file mode 100644 index 000000000..b169c422a --- /dev/null +++ b/allure-jpms-tests/src/test/resources/allure.properties @@ -0,0 +1,3 @@ +allure.results.directory=build/allure-results +allure.label.epic=#project.description# +allure.label.module=allure-jpms-tests diff --git a/allure-jpms-tests/src/test/resources/fixtures/consumer-api/fixture/Main.java b/allure-jpms-tests/src/test/resources/fixtures/consumer-api/fixture/Main.java new file mode 100644 index 000000000..a056f8b8a --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/consumer-api/fixture/Main.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import io.qameta.allure.Allure; +import io.qameta.allure.AllureLifecycle; +import io.qameta.allure.model.Label; + +public class Main { + public AllureLifecycle lifecycle() { + return Allure.getLifecycle(); + } + + public Label owner() { + return new Label().setName("owner").setValue("module user"); + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/consumer-api/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/consumer-api/module-info.java new file mode 100644 index 000000000..8d18f284a --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/consumer-api/module-info.java @@ -0,0 +1,3 @@ +module allure.fixture { + requires io.qameta.allure.commons; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/core/fixture/Main.java b/allure-jpms-tests/src/test/resources/fixtures/core/fixture/Main.java new file mode 100644 index 000000000..179791ba3 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/core/fixture/Main.java @@ -0,0 +1,90 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import fixture.spi.Greeting; +import io.qameta.allure.Allure; +import io.qameta.allure.AllureExternalKey; +import io.qameta.allure.AllureLifecycle; +import io.qameta.allure.listener.ContainerLifecycleListener; +import io.qameta.allure.listener.FixtureLifecycleListener; +import io.qameta.allure.listener.StepLifecycleListener; +import io.qameta.allure.listener.TestLifecycleListener; +import io.qameta.allure.model.FixtureResult; +import io.qameta.allure.model.Label; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; +import io.qameta.allure.model.TestResultContainer; +import io.qameta.allure.util.ServiceLoaderUtils; + +import java.util.List; + +public class Main + implements + TestLifecycleListener, + ContainerLifecycleListener, + FixtureLifecycleListener, + StepLifecycleListener, + Greeting { + + public static void main(final String[] args) { + final AllureLifecycle lifecycle = Allure.getLifecycle(); + final AllureExternalKey scope = AllureExternalKey.of(Main.class, "scope"); + final AllureExternalKey fixture = AllureExternalKey.of(Main.class, "fixture"); + final AllureExternalKey test = AllureExternalKey.of(Main.class, "test"); + lifecycle.registerScope(scope); + lifecycle.startBeforeFixture(scope, fixture, new FixtureResult().setName("setup")); + lifecycle.stopFixture(fixture); + lifecycle.scheduleTest(List.of(scope), test, new TestResult().setUuid("test").setName("core")); + lifecycle.startTest(test); + Allure.label( + "custom service", ServiceLoaderUtils.load(Greeting.class, Main.class.getClassLoader()) + .get(0).value() + ); + Allure.step("operation"); + Allure.attachment("payload", "text/plain", "module payload"); + lifecycle.updateTest(result -> result.setStatus(Status.PASSED)); + lifecycle.stopTest(test); + lifecycle.writeTest(test); + lifecycle.writeScope(scope); + } + + @Override + public String value() { + return "loaded"; + } + + @Override + public void beforeTestWrite(final TestResult result) { + result.getLabels().add(new Label().setName("listener").setValue("test")); + } + + @Override + public void beforeContainerWrite(final TestResultContainer result) { + result.setName("container listener"); + } + + @Override + public void beforeFixtureStop(final FixtureResult result) { + result.setDescription("fixture listener"); + } + + @Override + public void beforeStepStart(final StepResult result) { + result.setName("step listener: " + result.getName()); + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/core/fixture/spi/Greeting.java b/allure-jpms-tests/src/test/resources/fixtures/core/fixture/spi/Greeting.java new file mode 100644 index 000000000..f389d5e66 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/core/fixture/spi/Greeting.java @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture.spi; + +public interface Greeting { + String value(); +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/core/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/core/module-info.java new file mode 100644 index 000000000..c83cd90af --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/core/module-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module allure.fixture { + requires io.qameta.allure.commons; + exports fixture.spi to io.qameta.allure.commons; + provides fixture.spi.Greeting with fixture.Main; + + provides io.qameta.allure.listener.TestLifecycleListener with fixture.Main; + provides io.qameta.allure.listener.ContainerLifecycleListener with fixture.Main; + provides io.qameta.allure.listener.FixtureLifecycleListener with fixture.Main; + provides io.qameta.allure.listener.StepLifecycleListener with fixture.Main; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/httpclient/fixture/Main.java b/allure-jpms-tests/src/test/resources/fixtures/httpclient/fixture/Main.java new file mode 100644 index 000000000..228d95ca5 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/httpclient/fixture/Main.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import com.sun.net.httpserver.HttpServer; +import io.qameta.allure.Allure; +import io.qameta.allure.AllureExternalKey; +import io.qameta.allure.javahttpclient.AllureHttpClient; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.TestResult; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public final class Main { + + public static void main(final String[] args) throws Exception { + final HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/module", exchange -> { + final byte[] body = "module response".getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, body.length); + try (var output = exchange.getResponseBody()) { + output.write(body); + } + }); + server.start(); + try { + final AllureExternalKey test = AllureExternalKey.of(Main.class, "http"); + Allure.getLifecycle().scheduleTest(List.of(), test, new TestResult().setName("modular HTTP")); + Allure.getLifecycle().startTest(test); + final HttpClient client = new AllureHttpClient(HttpClient.newHttpClient()); + final URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/module"); + final HttpResponse response = client.send( + HttpRequest.newBuilder(uri).GET().build(), HttpResponse.BodyHandlers.ofString() + ); + if (response.statusCode() != 200 || !"module response".equals(response.body())) { + throw new AssertionError("Unexpected HTTP response: " + response); + } + Allure.getLifecycle().updateTest(result -> result.setStatus(Status.PASSED)); + Allure.getLifecycle().stopTest(test); + Allure.getLifecycle().writeTest(test); + } finally { + server.stop(0); + } + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/httpclient/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/httpclient/module-info.java new file mode 100644 index 000000000..ed0a86b51 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/httpclient/module-info.java @@ -0,0 +1,4 @@ +module allure.fixture { + requires io.qameta.allure.javahttpclient; + requires jdk.httpserver; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/jsonunit/fixture/Main.java b/allure-jpms-tests/src/test/resources/fixtures/jsonunit/fixture/Main.java new file mode 100644 index 000000000..ea29076bd --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/jsonunit/fixture/Main.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import io.qameta.allure.Allure; +import io.qameta.allure.AllureExternalKey; +import io.qameta.allure.jsonunit.JsonPatchMatcher; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.TestResult; +import org.hamcrest.StringDescription; + +public final class Main { + public static void main(final String[] args) { + final var lifecycle = Allure.getLifecycle(); + final AllureExternalKey test = AllureExternalKey.of(Main.class, "jsonunit"); + lifecycle.scheduleTest(test, new TestResult().setName("JSON module")); + lifecycle.startTest(test); + final var matcher = JsonPatchMatcher.jsonEquals("{\"value\":1}"); + if (matcher.matches("{\"value\":2}")) { + throw new AssertionError("Expected a JSON difference"); + } + matcher.describeMismatch("{\"value\":2}", new StringDescription()); + lifecycle.updateTest(result -> result.setStatus(Status.PASSED)); + lifecycle.stopTest(test); + lifecycle.writeTest(test); + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/jsonunit/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/jsonunit/module-info.java new file mode 100644 index 000000000..f2d765982 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/jsonunit/module-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module allure.fixture { + requires io.qameta.allure.jsonunit; + requires com.fasterxml.jackson.databind; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/junit4/fixture/Main.java b/allure-jpms-tests/src/test/resources/fixtures/junit4/fixture/Main.java new file mode 100644 index 000000000..42e2ecb56 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/junit4/fixture/Main.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import io.qameta.allure.junit4.AllureJunit4; +import org.junit.runner.JUnitCore; + +public final class Main { + public static void main(final String[] args) { + final JUnitCore runner = new JUnitCore(); + runner.addListener(new AllureJunit4()); + final var result = runner.run(fixture.tests.SampleTest.class); + if (result.getRunCount() != 1 || !result.wasSuccessful()) { + throw new AssertionError("Expected one successful JUnit 4 test: " + result.getFailures()); + } + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/junit4/fixture/tests/SampleTest.java b/allure-jpms-tests/src/test/resources/fixtures/junit4/fixture/tests/SampleTest.java new file mode 100644 index 000000000..e0364bf51 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/junit4/fixture/tests/SampleTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture.tests; + +import io.qameta.allure.Allure; +import io.qameta.allure.Owner; +import org.junit.Test; + +public class SampleTest { + @Test + @Owner("module owner") + public void sample() { + Allure.step("operation"); + Allure.attachment("payload", "text/plain", "JUnit 4 module payload"); + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/junit4/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/junit4/module-info.java new file mode 100644 index 000000000..a2d8a5f5b --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/junit4/module-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module allure.fixture { + requires io.qameta.allure.junit4; + requires junit; + exports fixture.tests; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/jupiter-api/fixture/SampleTest.java b/allure-jpms-tests/src/test/resources/fixtures/jupiter-api/fixture/SampleTest.java new file mode 100644 index 000000000..e9a129fcc --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/jupiter-api/fixture/SampleTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import io.qameta.allure.Allure; +import io.qameta.allure.jupiter.AllureJupiter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(AllureJupiter.class) +class SampleTest { + + @Test + void reports() { + Allure.step("operation"); + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/jupiter-api/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/jupiter-api/module-info.java new file mode 100644 index 000000000..7b8dc1886 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/jupiter-api/module-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module allure.fixture { + requires io.qameta.allure.jupiter; + requires org.junit.jupiter.api; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/jupiter-plain/fixture/Main.java b/allure-jpms-tests/src/test/resources/fixtures/jupiter-plain/fixture/Main.java new file mode 100644 index 000000000..a8c42e21a --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/jupiter-plain/fixture/Main.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; + +public final class Main { + + private Main() { + } + + public static void main(final String[] args) { + final SummaryGeneratingListener summary = new SummaryGeneratingListener(); + final var launcher = LauncherFactory.create(); + launcher.registerTestExecutionListeners(summary); + launcher.execute( + LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectClass("fixture.tests.SampleTest")) + .configurationParameter("junit.jupiter.extensions.autodetection.enabled", "true") + .build() + ); + if (summary.getSummary().getTestsSucceededCount() != 1) { + throw new IllegalStateException( + "Expected one successful Jupiter test: " + + summary.getSummary().getFailures() + ); + } + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/jupiter-plain/fixture/tests/SampleTest.java b/allure-jpms-tests/src/test/resources/fixtures/jupiter-plain/fixture/tests/SampleTest.java new file mode 100644 index 000000000..715303479 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/jupiter-plain/fixture/tests/SampleTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture.tests; + +import io.qameta.allure.Allure; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SampleTest { + + @BeforeEach + void prepare() { + Allure.step("prepare operation"); + } + + @Test + void reports() { + Allure.step("operation"); + Allure.attachment("payload", "text/plain", "module payload"); + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/jupiter-plain/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/jupiter-plain/module-info.java new file mode 100644 index 000000000..d515365fe --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/jupiter-plain/module-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module allure.fixture { + requires io.qameta.allure.commons; + requires org.junit.jupiter.api; + requires org.junit.platform.launcher; + + opens fixture.tests to org.junit.platform.commons, io.qameta.allure.commons, io.qameta.allure.jupiter; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/jupiter/fixture/Main.java b/allure-jpms-tests/src/test/resources/fixtures/jupiter/fixture/Main.java new file mode 100644 index 000000000..a8c42e21a --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/jupiter/fixture/Main.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; + +public final class Main { + + private Main() { + } + + public static void main(final String[] args) { + final SummaryGeneratingListener summary = new SummaryGeneratingListener(); + final var launcher = LauncherFactory.create(); + launcher.registerTestExecutionListeners(summary); + launcher.execute( + LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectClass("fixture.tests.SampleTest")) + .configurationParameter("junit.jupiter.extensions.autodetection.enabled", "true") + .build() + ); + if (summary.getSummary().getTestsSucceededCount() != 1) { + throw new IllegalStateException( + "Expected one successful Jupiter test: " + + summary.getSummary().getFailures() + ); + } + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/jupiter/fixture/tests/SampleTest.java b/allure-jpms-tests/src/test/resources/fixtures/jupiter/fixture/tests/SampleTest.java new file mode 100644 index 000000000..0def514d8 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/jupiter/fixture/tests/SampleTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture.tests; + +import io.qameta.allure.Allure; +import io.qameta.allure.LabelAnnotation; +import io.qameta.allure.Owner; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Owner("module owner") +class SampleTest { + + @ParameterizedTest + @ValueSource(strings = "argument value") + @Team("first team") + @Team("second team") + void reports(final String input) { + Allure.step("operation"); + Allure.attachment("payload", "text/plain", input); + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(Teams.class) + @LabelAnnotation(name = "team") + public @interface Team { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface Teams { + Team[] value(); + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/jupiter/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/jupiter/module-info.java new file mode 100644 index 000000000..a6f80f660 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/jupiter/module-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module allure.fixture { + requires io.qameta.allure.commons; + requires org.junit.jupiter.api; + requires org.junit.platform.launcher; + requires org.junit.jupiter.params; + + opens fixture.tests to org.junit.platform.commons, io.qameta.allure.commons, io.qameta.allure.jupiter; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/kotlin/fixture/Main.java b/allure-jpms-tests/src/test/resources/fixtures/kotlin/fixture/Main.java new file mode 100644 index 000000000..66ec0521c --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/kotlin/fixture/Main.java @@ -0,0 +1,54 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import io.qameta.allure.AllureExternalKey; +import io.qameta.allure.AllureThreadBinding; +import io.qameta.allure.kotlin.coroutines.AllureCoroutines; +import io.qameta.allure.kotlin.extensions.AllureKotlin; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.TestResult; +import kotlin.Unit; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlinx.coroutines.ThreadContextElement; + +public final class Main { + public static void main(final String[] args) throws Exception { + final var lifecycle = AllureKotlin.getLifecycle(); + final AllureExternalKey test = AllureExternalKey.of(Main.class, "kotlin"); + lifecycle.scheduleTest(test, new TestResult().setName("Kotlin modules")); + lifecycle.startTest(test); + final var context = AllureCoroutines.allureContext(); + final var worker = new Thread(() -> { + @SuppressWarnings("unchecked") + final var element = (ThreadContextElement) context; + final var previous = element.updateThreadContext(EmptyCoroutineContext.INSTANCE); + try { + AllureKotlin.step("Kotlin operation", scope -> { + AllureKotlin.attachment("payload", "Kotlin module payload", "text/plain", null); + return Unit.INSTANCE; + }); + } finally { + element.restoreThreadContext(EmptyCoroutineContext.INSTANCE, previous); + } + }); + worker.start(); + worker.join(); + lifecycle.updateTest(result -> result.setStatus(Status.PASSED)); + lifecycle.stopTest(test); + lifecycle.writeTest(test); + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/kotlin/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/kotlin/module-info.java new file mode 100644 index 000000000..493431b76 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/kotlin/module-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module allure.fixture { + requires io.qameta.allure.kotlin.coroutines; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/processor/fixture/Sample.java b/allure-jpms-tests/src/test/resources/fixtures/processor/fixture/Sample.java new file mode 100644 index 000000000..3078af6c2 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/processor/fixture/Sample.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import io.qameta.allure.Description; + +public final class Sample { + /** Describes a test compiled with a modular annotation processor. */ + @Description + public void sample() { + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/processor/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/processor/module-info.java new file mode 100644 index 000000000..61dec9bf8 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/processor/module-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module allure.fixture { + requires io.qameta.allure.commons; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/spock/fixture/Main.java b/allure-jpms-tests/src/test/resources/fixtures/spock/fixture/Main.java new file mode 100644 index 000000000..5bc23535e --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/spock/fixture/Main.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; + +public final class Main { + + private Main() { + } + + public static void main(final String[] args) { + final SummaryGeneratingListener summary = new SummaryGeneratingListener(); + final var launcher = LauncherFactory.create(); + launcher.registerTestExecutionListeners(summary); + launcher.execute( + LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectClass("fixture.tests.SampleTest")) + .configurationParameter("junit.jupiter.extensions.autodetection.enabled", "true") + .build() + ); + if (summary.getSummary().getTestsSucceededCount() != 1) { + throw new IllegalStateException( + "Expected one successful Spock feature: " + + summary.getSummary().getFailures() + ); + } + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/spock/fixture/tests/SampleTest.groovy b/allure-jpms-tests/src/test/resources/fixtures/spock/fixture/tests/SampleTest.groovy new file mode 100644 index 000000000..07823a599 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/spock/fixture/tests/SampleTest.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture.tests + +import io.qameta.allure.Allure +import io.qameta.allure.Owner +import spock.lang.Specification + +class SampleTest extends Specification { + @Owner("module owner") + def "module feature"() { + when: + Allure.step("operation") + Allure.attachment("payload", "text/plain", "Spock module payload") + + then: + 2 + 2 == 4 + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/spock/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/spock/module-info.java new file mode 100644 index 000000000..c45febe9f --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/spock/module-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module allure.fixture { + requires io.qameta.allure.spock2; + requires org.spockframework.core; + requires org.apache.groovy; + requires org.junit.platform.launcher; + requires java.desktop; + opens fixture.tests to org.spockframework.core, org.apache.groovy, + io.qameta.allure.commons, io.qameta.allure.spock2; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/testng/fixture/Main.java b/allure-jpms-tests/src/test/resources/fixtures/testng/fixture/Main.java new file mode 100644 index 000000000..0a16b6316 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/testng/fixture/Main.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import org.testng.TestListenerAdapter; +import org.testng.TestNG; + +public final class Main { + + private Main() { + } + + public static void main(final String[] args) throws ClassNotFoundException { + final TestNG testng = new TestNG(); + final TestListenerAdapter summary = new TestListenerAdapter(); + testng.addListener(summary); + testng.setUseDefaultListeners(false); + testng.setTestClasses(new Class[]{Class.forName("fixture.tests.SampleTest")}); + testng.run(); + if (testng.hasFailure() || summary.getPassedTests().size() != 1) { + throw new IllegalStateException( + "Expected one successful TestNG test", + summary.getFailedTests().isEmpty() ? null : summary.getFailedTests().get(0).getThrowable() + ); + } + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/testng/fixture/tests/SampleTest.java b/allure-jpms-tests/src/test/resources/fixtures/testng/fixture/tests/SampleTest.java new file mode 100644 index 000000000..4c4c881c7 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/testng/fixture/tests/SampleTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture.tests; + +import io.qameta.allure.Allure; +import io.qameta.allure.Owner; +import io.qameta.allure.testng.TestInstanceParameter; +import org.testng.annotations.Test; + +@Owner("module owner") +public class SampleTest { + + @TestInstanceParameter + private final String instance = "instance value"; + + @Test + public void reports() { + Allure.step("operation"); + Allure.attachment("payload", "text/plain", "module payload"); + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/testng/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/testng/module-info.java new file mode 100644 index 000000000..5199d3a7e --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/testng/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module allure.fixture { + requires io.qameta.allure.testng; + requires org.testng; + + opens fixture.tests to org.testng, io.qameta.allure.testng; +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/weaving/META-INF/aop.xml b/allure-jpms-tests/src/test/resources/fixtures/weaving/META-INF/aop.xml new file mode 100644 index 000000000..2688c86b8 --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/weaving/META-INF/aop.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/allure-jpms-tests/src/test/resources/fixtures/weaving/fixture/Main.java b/allure-jpms-tests/src/test/resources/fixtures/weaving/fixture/Main.java new file mode 100644 index 000000000..00f6e990b --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/weaving/fixture/Main.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixture; + +import io.qameta.allure.Allure; +import io.qameta.allure.AllureExternalKey; +import io.qameta.allure.Attachment; +import io.qameta.allure.Step; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.TestResult; + +public final class Main { + + private Main() { + } + + public static void main(final String[] args) { + final AllureExternalKey test = AllureExternalKey.of(Main.class, "test"); + Allure.getLifecycle().scheduleTest(test, new TestResult().setName("woven test")); + Allure.getLifecycle().startTest(test); + operation(new User()); + Allure.getLifecycle().updateTest(result -> result.setStatus(Status.PASSED)); + Allure.getLifecycle().stopTest(test); + Allure.getLifecycle().writeTest(test); + } + + @Step("operation for {user.name}") + private static void operation(final User user) { + payload(); + } + + @Attachment( + value = "payload", + type = "text/plain" + ) + private static String payload() { + return "woven payload"; + } + + private static final class User { + private final String name = "module user"; + } +} diff --git a/allure-jpms-tests/src/test/resources/fixtures/weaving/module-info.java b/allure-jpms-tests/src/test/resources/fixtures/weaving/module-info.java new file mode 100644 index 000000000..ac12b472c --- /dev/null +++ b/allure-jpms-tests/src/test/resources/fixtures/weaving/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module allure.fixture { + requires io.qameta.allure.commons; + requires org.aspectj.runtime; + + opens fixture to io.qameta.allure.commons; +} diff --git a/allure-jsonunit/build.gradle.kts b/allure-jsonunit/build.gradle.kts index 3a9b71fa1..a5ac9b03a 100644 --- a/allure-jsonunit/build.gradle.kts +++ b/allure-jsonunit/build.gradle.kts @@ -3,9 +3,9 @@ description = "Allure JsonUnit Integration" val jsonUnitVersion = "5.1.2" dependencies { + testImplementation("com.fasterxml.jackson.core:jackson-databind") api(project(":allure-java-commons")) compileOnly("net.javacrumbs.json-unit:json-unit:$jsonUnitVersion") - implementation("com.fasterxml.jackson.core:jackson-databind") implementation("org.freemarker:freemarker") testImplementation("net.javacrumbs.json-unit:json-unit:$jsonUnitVersion") testImplementation("org.assertj:assertj-core") @@ -19,14 +19,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.jsonunit" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-jsonunit/src/main/java/io/qameta/allure/jsonunit/JsonPatchListener.java b/allure-jsonunit/src/main/java/io/qameta/allure/jsonunit/JsonPatchListener.java index 33702603c..03a7d3636 100644 --- a/allure-jsonunit/src/main/java/io/qameta/allure/jsonunit/JsonPatchListener.java +++ b/allure-jsonunit/src/main/java/io/qameta/allure/jsonunit/JsonPatchListener.java @@ -15,12 +15,12 @@ */ package io.qameta.allure.jsonunit; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.qameta.allure.internal.json.JsonSupport; import net.javacrumbs.jsonunit.core.listener.Difference; import net.javacrumbs.jsonunit.core.listener.DifferenceContext; import net.javacrumbs.jsonunit.core.listener.DifferenceListener; +import java.io.IOException; import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.HashMap; @@ -35,7 +35,6 @@ public class JsonPatchListener implements DifferenceListener { private static final String UNKNOWN_TYPE_ERROR = "Difference has unknown type"; - private static final ObjectMapper MAPPER = new ObjectMapper(); private final List differences = new ArrayList<>(); private DifferenceContext context; @@ -134,10 +133,9 @@ public String getJsonPatch() { final Difference difference = getDifferences().get(0); final String field = getPath(difference); if (field.isEmpty()) { - final ObjectMapper mapper = new ObjectMapper(); try { - return mapper.writeValueAsString(getPatch(difference)); - } catch (JsonProcessingException e) { + return JsonSupport.writeJson(getPatch(difference)); + } catch (IOException e) { throw new IllegalStateException("Could not process patch json", e); } } @@ -179,18 +177,17 @@ public String getJsonPatch() { left = right + 1; } }); - final ObjectMapper mapper = new ObjectMapper(); try { - return mapper.writeValueAsString(jsonDiffPatch); - } catch (JsonProcessingException e) { + return JsonSupport.writeJson(jsonDiffPatch); + } catch (IOException e) { throw new IllegalStateException("Could not process patch json", e); } } private static String writeAsString(final Object object, final String failDescription) { try { - return MAPPER.writeValueAsString(object); - } catch (JsonProcessingException e) { + return JsonSupport.writeJson(object); + } catch (IOException e) { throw new UncheckedIOException(String.format("Could not process %s json", failDescription), e); } } diff --git a/allure-jsonunit/src/main/java/io/qameta/allure/jsonunit/JsonPatchMatcher.java b/allure-jsonunit/src/main/java/io/qameta/allure/jsonunit/JsonPatchMatcher.java index 898571b52..690d0d986 100644 --- a/allure-jsonunit/src/main/java/io/qameta/allure/jsonunit/JsonPatchMatcher.java +++ b/allure-jsonunit/src/main/java/io/qameta/allure/jsonunit/JsonPatchMatcher.java @@ -15,6 +15,7 @@ */ package io.qameta.allure.jsonunit; +import freemarker.cache.URLTemplateLoader; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; @@ -27,6 +28,7 @@ import java.io.IOException; import java.io.StringWriter; import java.io.Writer; +import java.net.URL; import java.util.Map; /** @@ -105,7 +107,13 @@ private static Configuration configuration() { configuration.setLocalizedLookup(false); configuration.setTemplateUpdateDelayMilliseconds(0); configuration.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER); - configuration.setClassLoaderForTemplateLoading(JsonPatchMatcher.class.getClassLoader(), "tpl"); + configuration.setTemplateLoader(new URLTemplateLoader() { + @Override + protected URL getURL(final String name) { + // Resolve from the owning module, including resources hidden from ClassLoader lookup. + return JsonPatchMatcher.class.getResource("/tpl/" + name); + } + }); return configuration; } } diff --git a/allure-jsonunit/src/main/java/module-info.java b/allure-jsonunit/src/main/java/module-info.java new file mode 100644 index 000000000..fb8ecafff --- /dev/null +++ b/allure-jsonunit/src/main/java/module-info.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.jsonunit { + requires transitive io.qameta.allure.commons; + requires transitive net.javacrumbs.jsonunit.classic; + requires transitive net.javacrumbs.jsonunit.core; + requires transitive org.hamcrest; + requires freemarker; + // JsonUnit is automatic and cannot resolve its explicit assertion dependency itself. + requires org.opentest4j; + + exports io.qameta.allure.jsonunit; +} diff --git a/allure-junit-platform/build.gradle.kts b/allure-junit-platform/build.gradle.kts index 114f09b35..10326b49c 100644 --- a/allure-junit-platform/build.gradle.kts +++ b/allure-junit-platform/build.gradle.kts @@ -1,6 +1,7 @@ description = "Allure JUnit Platform Integration" dependencies { + implementation("org.slf4j:slf4j-api") api(project(":allure-java-commons")) implementation("org.junit.jupiter:junit-jupiter-api") implementation("org.junit.platform:junit-platform-launcher") @@ -16,11 +17,6 @@ dependencies { } tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.junitplatform" - )) - } from("src/main/services") { into("META-INF/services") } @@ -42,8 +38,32 @@ tasks.named("pmdMain") { dependsOn(tasks.jar) } -val spiOffJar: Jar by tasks.creating(Jar::class) { - from(sourceSets.getByName("main").output) +tasks.javadoc { + dependsOn(tasks.jar) +} + +val compileSpiOffModuleInfo = tasks.register("compileSpiOffModuleInfo") { + source("src/spi-off/java/module-info.java") + classpath = configurations.compileClasspath.get() + destinationDirectory.set(layout.buildDirectory.dir("classes/spi-off-module-info")) + options.release.set(17) + modularity.inferModulePath.set(false) + inputs.files(sourceSets.main.get().output).withPropertyName("patchedClasses") + options.compilerArgumentProviders.add(org.gradle.process.CommandLineArgumentProvider { + listOf("--module-path", classpath.asPath) + }) + dependsOn(tasks.jar) + options.compilerArgs.addAll(listOf( + "--patch-module", "io.qameta.allure.junitplatform=${sourceSets.main.get().output.asPath}" + )) +} + +val spiOffJar = tasks.register("spiOffJar") { + from(sourceSets.main.get().output) { + exclude("module-info.class") + } + from(compileSpiOffModuleInfo) + manifest.from(tasks.jar.get().manifest) archiveClassifier.set("spi-off") } diff --git a/allure-junit-platform/src/main/java/module-info.java b/allure-junit-platform/src/main/java/module-info.java new file mode 100644 index 000000000..769ee4a1a --- /dev/null +++ b/allure-junit-platform/src/main/java/module-info.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.junitplatform { + requires transitive io.qameta.allure.commons; + requires org.junit.platform.launcher; + requires org.junit.jupiter.api; + requires org.slf4j; + + exports io.qameta.allure.junitplatform; + + provides org.junit.platform.launcher.TestExecutionListener + with io.qameta.allure.junitplatform.AllureJunitPlatform; + provides org.junit.platform.launcher.PostDiscoveryFilter + with io.qameta.allure.junitplatform.AllurePostDiscoveryFilter; +} diff --git a/allure-junit-platform/src/spi-off/java/module-info.java b/allure-junit-platform/src/spi-off/java/module-info.java new file mode 100644 index 000000000..5df1abdda --- /dev/null +++ b/allure-junit-platform/src/spi-off/java/module-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.junitplatform { + requires transitive io.qameta.allure.commons; + requires org.junit.platform.launcher; + requires org.junit.jupiter.api; + requires org.slf4j; + + exports io.qameta.allure.junitplatform; +} diff --git a/allure-junit4-aspect/build.gradle.kts b/allure-junit4-aspect/build.gradle.kts index c40dbcaaa..78e459e15 100644 --- a/allure-junit4-aspect/build.gradle.kts +++ b/allure-junit4-aspect/build.gradle.kts @@ -18,14 +18,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.junit4aspect" - )) - } -} - publishing { publications.withType().configureEach { pom { diff --git a/allure-junit4-aspect/src/main/java/module-info.java b/allure-junit4-aspect/src/main/java/module-info.java new file mode 100644 index 000000000..a17b280a0 --- /dev/null +++ b/allure-junit4-aspect/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.junit4aspect { + requires transitive io.qameta.allure.junit4; + requires org.aspectj.runtime; + + exports io.qameta.allure.junit4.aspect; +} diff --git a/allure-junit4/build.gradle.kts b/allure-junit4/build.gradle.kts index 54dc2fb42..5fc83e7bb 100644 --- a/allure-junit4/build.gradle.kts +++ b/allure-junit4/build.gradle.kts @@ -17,14 +17,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.junit4" - )) - } -} - tasks.test { useJUnitPlatform() exclude("**/samples/*", "SampleTestInDefaultPackage.java") diff --git a/allure-junit4/src/main/java/module-info.java b/allure-junit4/src/main/java/module-info.java new file mode 100644 index 000000000..cd40a2b32 --- /dev/null +++ b/allure-junit4/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.junit4 { + requires transitive io.qameta.allure.commons; + requires junit; + + exports io.qameta.allure.junit4; +} diff --git a/allure-jupiter-assert/build.gradle.kts b/allure-jupiter-assert/build.gradle.kts index fe63eb8d3..2cfb53eae 100644 --- a/allure-jupiter-assert/build.gradle.kts +++ b/allure-jupiter-assert/build.gradle.kts @@ -4,7 +4,9 @@ description = "Allure Jupiter Assertions Integration" val missingDependency by sourceSets.creating dependencies { - api(project(":allure-jupiter")) + implementation("org.slf4j:slf4j-api") + api(project(":allure-java-commons")) + implementation(project(":allure-jupiter")) compileOnly("org.aspectj:aspectjrt") compileOnly("org.junit.jupiter:junit-jupiter-api") testImplementation("org.aspectj:aspectjweaver") @@ -18,14 +20,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.jupiterassert" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-jupiter-assert/src/main/java/module-info.java b/allure-jupiter-assert/src/main/java/module-info.java new file mode 100644 index 000000000..fdd5ac41f --- /dev/null +++ b/allure-jupiter-assert/src/main/java/module-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.jupiterassert { + requires transitive io.qameta.allure.commons; + requires io.qameta.allure.jupiter; + requires org.aspectj.runtime; + requires org.slf4j; + + exports io.qameta.allure.jupiterassert; +} diff --git a/allure-jupiter/build.gradle.kts b/allure-jupiter/build.gradle.kts index bd3bcae04..482b6d878 100644 --- a/allure-jupiter/build.gradle.kts +++ b/allure-jupiter/build.gradle.kts @@ -1,7 +1,8 @@ description = "Allure Jupiter Integration" dependencies { - api(project(":allure-junit-platform")) + api(project(":allure-java-commons")) + implementation(project(":allure-junit-platform")) compileOnly("org.junit.jupiter:junit-jupiter-api") compileOnly("org.junit.jupiter:junit-jupiter-params") compileOnly("org.junit.platform:junit-platform-launcher") @@ -17,11 +18,6 @@ dependencies { } tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.jupiter" - )) - } from("src/main/services") { into("META-INF/services") } @@ -33,8 +29,28 @@ tasks.test { exclude("**/features/*") } -val spiOffJar: Jar by tasks.creating(Jar::class) { - from(sourceSets.getByName("main").output) +val compileSpiOffModuleInfo = tasks.register("compileSpiOffModuleInfo") { + source("src/spi-off/java/module-info.java") + classpath = configurations.compileClasspath.get() + destinationDirectory.set(layout.buildDirectory.dir("classes/spi-off-module-info")) + options.release.set(17) + modularity.inferModulePath.set(false) + inputs.files(sourceSets.main.get().output).withPropertyName("patchedClasses") + options.compilerArgumentProviders.add(org.gradle.process.CommandLineArgumentProvider { + listOf("--module-path", classpath.asPath) + }) + dependsOn(tasks.jar) + options.compilerArgs.addAll(listOf( + "--patch-module", "io.qameta.allure.jupiter=${sourceSets.main.get().output.asPath}" + )) +} + +val spiOffJar = tasks.register("spiOffJar") { + from(sourceSets.main.get().output) { + exclude("module-info.class") + } + from(compileSpiOffModuleInfo) + manifest.from(tasks.jar.get().manifest) archiveClassifier.set("spi-off") } diff --git a/allure-jupiter/src/main/java/module-info.java b/allure-jupiter/src/main/java/module-info.java new file mode 100644 index 000000000..428439ec0 --- /dev/null +++ b/allure-jupiter/src/main/java/module-info.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.jupiter { + requires transitive io.qameta.allure.commons; + requires io.qameta.allure.junitplatform; + requires org.junit.jupiter.api; + requires static org.junit.jupiter.params; + requires static org.junit.platform.launcher; + + exports io.qameta.allure.jupiter; + + provides org.junit.jupiter.api.extension.Extension + with io.qameta.allure.jupiter.AllureJupiter; +} diff --git a/allure-jupiter/src/spi-off/java/module-info.java b/allure-jupiter/src/spi-off/java/module-info.java new file mode 100644 index 000000000..f94ddfefd --- /dev/null +++ b/allure-jupiter/src/spi-off/java/module-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.jupiter { + requires transitive io.qameta.allure.commons; + requires io.qameta.allure.junitplatform; + requires org.junit.jupiter.api; + requires static org.junit.jupiter.params; + requires static org.junit.platform.launcher; + + exports io.qameta.allure.jupiter; +} diff --git a/allure-karate/build.gradle.kts b/allure-karate/build.gradle.kts index 763a4b48b..1b015bead 100644 --- a/allure-karate/build.gradle.kts +++ b/allure-karate/build.gradle.kts @@ -9,6 +9,7 @@ configurations { } dependencies { + implementation("org.slf4j:slf4j-api") api(project(":allure-java-commons")) compileOnly("io.karatelabs:karate-core:${karateVersion}") testAnnotationProcessor("org.slf4j:slf4j-simple") diff --git a/allure-kotlin-coroutines/build.gradle.kts b/allure-kotlin-coroutines/build.gradle.kts index be0f0a1d4..19c232867 100644 --- a/allure-kotlin-coroutines/build.gradle.kts +++ b/allure-kotlin-coroutines/build.gradle.kts @@ -45,14 +45,15 @@ spotless { } } -tasks.jar { - manifest { - attributes( - mapOf( - "Automatic-Module-Name" to "io.qameta.allure.kotlin.coroutines", - ), - ) - } +tasks.compileJava { + options.compilerArgumentProviders.add(CommandLineArgumentProvider { + listOf("--patch-module", "io.qameta.allure.kotlin.coroutines=${sourceSets.main.get().output.asPath}") + }) +} + +tasks.javadoc { + // This library's API is Kotlin; the descriptor is its only Java source. + exclude("module-info.java") } tasks.compileTestKotlin { diff --git a/allure-kotlin-coroutines/src/main/java/module-info.java b/allure-kotlin-coroutines/src/main/java/module-info.java new file mode 100644 index 000000000..57cc3b4a7 --- /dev/null +++ b/allure-kotlin-coroutines/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.kotlin.coroutines { + requires transitive io.qameta.allure.kotlin.extensions; + requires transitive kotlinx.coroutines.core; + + exports io.qameta.allure.kotlin.coroutines; +} diff --git a/allure-kotlin-extensions/build.gradle.kts b/allure-kotlin-extensions/build.gradle.kts index 60f88934a..002b5a9fa 100644 --- a/allure-kotlin-extensions/build.gradle.kts +++ b/allure-kotlin-extensions/build.gradle.kts @@ -45,14 +45,15 @@ spotless { } } -tasks.jar { - manifest { - attributes( - mapOf( - "Automatic-Module-Name" to "io.qameta.allure.kotlin.extensions", - ), - ) - } +tasks.compileJava { + options.compilerArgumentProviders.add(CommandLineArgumentProvider { + listOf("--patch-module", "io.qameta.allure.kotlin.extensions=${sourceSets.main.get().output.asPath}") + }) +} + +tasks.javadoc { + // This library's API is Kotlin; the descriptor is its only Java source. + exclude("module-info.java") } tasks.compileTestKotlin { diff --git a/allure-kotlin-extensions/src/main/java/module-info.java b/allure-kotlin-extensions/src/main/java/module-info.java new file mode 100644 index 000000000..8ca090512 --- /dev/null +++ b/allure-kotlin-extensions/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.kotlin.extensions { + requires transitive io.qameta.allure.commons; + requires transitive kotlin.stdlib; + + exports io.qameta.allure.kotlin.extensions; +} diff --git a/allure-model/build.gradle.kts b/allure-model/build.gradle.kts index ff9b3d2e1..d71c1d72a 100644 --- a/allure-model/build.gradle.kts +++ b/allure-model/build.gradle.kts @@ -12,14 +12,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.model" - )) - } -} - tasks.test { // The Gradle-added Allure JUnit Platform adapter resolves allure-model back to this project. dependsOn(tasks.jar) diff --git a/allure-model/src/main/java/module-info.java b/allure-model/src/main/java/module-info.java new file mode 100644 index 000000000..4129ff4da --- /dev/null +++ b/allure-model/src/main/java/module-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.model { + exports io.qameta.allure.model; +} diff --git a/allure-okhttp3/build.gradle.kts b/allure-okhttp3/build.gradle.kts index 366b25747..edd63f907 100644 --- a/allure-okhttp3/build.gradle.kts +++ b/allure-okhttp3/build.gradle.kts @@ -21,14 +21,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.okhttp3" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-okhttp3/src/main/java/module-info.java b/allure-okhttp3/src/main/java/module-info.java new file mode 100644 index 000000000..07c1ead10 --- /dev/null +++ b/allure-okhttp3/src/main/java/module-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.okhttp3 { + requires transitive io.qameta.allure.commons; + requires transitive okhttp3; + requires transitive okhttp3.sse; + + exports io.qameta.allure.okhttp3; +} diff --git a/allure-playwright/build.gradle.kts b/allure-playwright/build.gradle.kts index 62bc5493f..c2be27ea6 100644 --- a/allure-playwright/build.gradle.kts +++ b/allure-playwright/build.gradle.kts @@ -5,6 +5,7 @@ val agent: Configuration by configurations.creating val playwrightVersion = "1.62.0" dependencies { + implementation("org.slf4j:slf4j-api") agent("org.aspectj:aspectjweaver") api(project(":allure-java-commons")) compileOnly("com.microsoft.playwright:playwright:$playwrightVersion") @@ -20,14 +21,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.playwright" - )) - } -} - tasks.test { useJUnitPlatform() doFirst { diff --git a/allure-playwright/src/main/java/module-info.java b/allure-playwright/src/main/java/module-info.java new file mode 100644 index 000000000..01202eb4d --- /dev/null +++ b/allure-playwright/src/main/java/module-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.playwright { + requires transitive io.qameta.allure.commons; + requires playwright; + requires static org.aspectj.runtime; + requires org.slf4j; + + exports io.qameta.allure.playwright; + + provides io.qameta.allure.listener.TestLifecycleListener with io.qameta.allure.playwright.AllurePlaywrightLifecycle; +} diff --git a/allure-rest-assured/build.gradle.kts b/allure-rest-assured/build.gradle.kts index d5a72759d..2f5c7c122 100644 --- a/allure-rest-assured/build.gradle.kts +++ b/allure-rest-assured/build.gradle.kts @@ -20,14 +20,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.restassured" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-rest-assured/src/main/java/module-info.java b/allure-rest-assured/src/main/java/module-info.java new file mode 100644 index 000000000..443483ea0 --- /dev/null +++ b/allure-rest-assured/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.restassured { + requires transitive io.qameta.allure.commons; + requires rest.assured; + + exports io.qameta.allure.restassured; +} diff --git a/allure-selenide/build.gradle.kts b/allure-selenide/build.gradle.kts index 7dcdf1c91..ea0820f43 100644 --- a/allure-selenide/build.gradle.kts +++ b/allure-selenide/build.gradle.kts @@ -3,6 +3,7 @@ description = "Allure Selenide Integration" val selenideVersion = "7.18.1" dependencies { + implementation("org.slf4j:slf4j-api") api(project(":allure-java-commons")) compileOnly("com.codeborne:selenide:$selenideVersion") testImplementation("com.codeborne:selenide:$selenideVersion") diff --git a/allure-selenium-bidi/build.gradle.kts b/allure-selenium-bidi/build.gradle.kts index 13cb8dfc4..fc1cf8aa7 100644 --- a/allure-selenium-bidi/build.gradle.kts +++ b/allure-selenium-bidi/build.gradle.kts @@ -6,6 +6,7 @@ val seleniumVersion = "4.48.0" val testcontainersVersion = "1.21.4" dependencies { + implementation("org.slf4j:slf4j-api") agent("org.aspectj:aspectjweaver") api(project(":allure-java-commons")) compileOnly("org.seleniumhq.selenium:selenium-java:$seleniumVersion") @@ -29,15 +30,6 @@ tasks { compileTestJava { options.release.set(17) } - jar { - manifest { - attributes( - mapOf( - "Automatic-Module-Name" to "io.qameta.allure.seleniumbidi" - ) - ) - } - } test { useJUnitPlatform() jvmArgs("-javaagent:${agent.singleFile}") diff --git a/allure-selenium-bidi/src/main/java/module-info.java b/allure-selenium-bidi/src/main/java/module-info.java new file mode 100644 index 000000000..8f04b1287 --- /dev/null +++ b/allure-selenium-bidi/src/main/java/module-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.seleniumbidi { + requires transitive io.qameta.allure.commons; + requires org.seleniumhq.selenium.remote_driver; + requires org.seleniumhq.selenium.support; + requires org.slf4j; + + exports io.qameta.allure.seleniumbidi; +} diff --git a/allure-servlet-api/build.gradle.kts b/allure-servlet-api/build.gradle.kts index 814160aee..4dbe32c30 100644 --- a/allure-servlet-api/build.gradle.kts +++ b/allure-servlet-api/build.gradle.kts @@ -3,6 +3,7 @@ description = "Allure Jakarta Servlet API Integration" val jakartaServletApiVersion = "6.1.0" dependencies { + implementation("org.slf4j:slf4j-api") api(project(":allure-java-commons")) compileOnly("jakarta.servlet:jakarta.servlet-api:$jakartaServletApiVersion") testImplementation("org.assertj:assertj-core") @@ -16,14 +17,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.servletapi" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-servlet-api/src/main/java/module-info.java b/allure-servlet-api/src/main/java/module-info.java new file mode 100644 index 000000000..9885ac64f --- /dev/null +++ b/allure-servlet-api/src/main/java/module-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.servletapi { + requires transitive io.qameta.allure.commons; + requires jakarta.servlet; + requires org.slf4j; + + exports io.qameta.allure.servletapi; +} diff --git a/allure-spock2/build.gradle.kts b/allure-spock2/build.gradle.kts index 8e4081893..543c80e3b 100644 --- a/allure-spock2/build.gradle.kts +++ b/allure-spock2/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { compileOnly("org.spockframework:spock-core:$spockFrameworkVersion") testAnnotationProcessor("org.slf4j:slf4j-simple") testImplementation("io.github.glytching:junit-extensions") + testImplementation("org.apache.commons:commons-lang3") testImplementation("org.assertj:assertj-core") testImplementation("org.apache.groovy:groovy:${groovyVersion}") testImplementation("org.junit.jupiter:junit-jupiter-api") @@ -28,12 +29,6 @@ dependencies { } tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.spock2" - )) - } - from("src/main/services") { into("META-INF/services") } @@ -45,8 +40,28 @@ tasks.test { exclude("**/samples/*") } -val spiOffJar: Jar by tasks.creating(Jar::class) { - from(sourceSets.getByName("main").output) +val compileSpiOffModuleInfo = tasks.register("compileSpiOffModuleInfo") { + source("src/spi-off/java/module-info.java") + classpath = configurations.compileClasspath.get() + destinationDirectory.set(layout.buildDirectory.dir("classes/spi-off-module-info")) + options.release.set(17) + modularity.inferModulePath.set(false) + inputs.files(sourceSets.main.get().output).withPropertyName("patchedClasses") + options.compilerArgumentProviders.add(CommandLineArgumentProvider { + listOf("--module-path", classpath.asPath) + }) + dependsOn(tasks.jar) + options.compilerArgs.addAll(listOf( + "--patch-module", "io.qameta.allure.spock2=${sourceSets.main.get().output.asPath}" + )) +} + +val spiOffJar = tasks.register("spiOffJar") { + from(sourceSets.main.get().output) { + exclude("module-info.class") + } + from(compileSpiOffModuleInfo) + manifest.from(tasks.jar.get().manifest) archiveClassifier.set("spi-off") } diff --git a/allure-spock2/src/main/java/module-info.java b/allure-spock2/src/main/java/module-info.java new file mode 100644 index 000000000..014b647d2 --- /dev/null +++ b/allure-spock2/src/main/java/module-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.spock2 { + requires transitive io.qameta.allure.commons; + requires org.spockframework.core; + + exports io.qameta.allure.spock2; + + provides org.spockframework.runtime.extension.IGlobalExtension with io.qameta.allure.spock2.AllureSpock2; +} diff --git a/allure-spock2/src/spi-off/java/module-info.java b/allure-spock2/src/spi-off/java/module-info.java new file mode 100644 index 000000000..2c111a172 --- /dev/null +++ b/allure-spock2/src/spi-off/java/module-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.spock2 { + requires transitive io.qameta.allure.commons; + requires org.spockframework.core; + + exports io.qameta.allure.spock2; +} diff --git a/allure-spring-web/build.gradle.kts b/allure-spring-web/build.gradle.kts index bc5a5acee..f9ec7d812 100644 --- a/allure-spring-web/build.gradle.kts +++ b/allure-spring-web/build.gradle.kts @@ -3,6 +3,7 @@ description = "Allure Spring Web Integration" val springWebVersion = "7.0.9" dependencies { + testImplementation("com.fasterxml.jackson.core:jackson-databind") api(project(":allure-java-commons")) compileOnly("org.springframework:spring-web:$springWebVersion") testImplementation("org.wiremock:wiremock") @@ -20,14 +21,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.springweb" - )) - } -} - tasks.test { useJUnitPlatform() } diff --git a/allure-spring-web/src/main/java/module-info.java b/allure-spring-web/src/main/java/module-info.java new file mode 100644 index 000000000..dcb92f30c --- /dev/null +++ b/allure-spring-web/src/main/java/module-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.springweb { + requires transitive io.qameta.allure.commons; + requires transitive spring.web; + requires spring.core; + + exports io.qameta.allure.springweb; +} diff --git a/allure-testng/build.gradle.kts b/allure-testng/build.gradle.kts index 8e0980cc5..d4532c14e 100644 --- a/allure-testng/build.gradle.kts +++ b/allure-testng/build.gradle.kts @@ -3,6 +3,7 @@ description = "Allure TestNG 7 Integration" val testNgVersion = "7.12.0" dependencies { + implementation("org.slf4j:slf4j-api") api(project(":allure-java-commons")) compileOnly("org.testng:testng:$testNgVersion") testAnnotationProcessor("org.slf4j:slf4j-simple") @@ -21,11 +22,6 @@ dependencies { } tasks.jar { - manifest { - attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.testng" - )) - } from("src/main/services") { into("META-INF/services") } @@ -36,8 +32,28 @@ tasks.test { exclude("**/samples/*") } -val spiOffJar: Jar by tasks.creating(Jar::class) { - from(sourceSets.getByName("main").output) +val compileSpiOffModuleInfo = tasks.register("compileSpiOffModuleInfo") { + source("src/spi-off/java/module-info.java") + classpath = configurations.compileClasspath.get() + destinationDirectory.set(layout.buildDirectory.dir("classes/spi-off-module-info")) + options.release.set(17) + modularity.inferModulePath.set(false) + inputs.files(sourceSets.main.get().output).withPropertyName("patchedClasses") + options.compilerArgumentProviders.add(org.gradle.process.CommandLineArgumentProvider { + listOf("--module-path", classpath.asPath) + }) + dependsOn(tasks.jar) + options.compilerArgs.addAll(listOf( + "--patch-module", "io.qameta.allure.testng=${sourceSets.main.get().output.asPath}" + )) +} + +val spiOffJar = tasks.register("spiOffJar") { + from(sourceSets.main.get().output) { + exclude("module-info.class") + } + from(compileSpiOffModuleInfo) + manifest.from(tasks.jar.get().manifest) archiveClassifier.set("spi-off") } diff --git a/allure-testng/src/main/java/io/qameta/allure/testng/AllureTestNg.java b/allure-testng/src/main/java/io/qameta/allure/testng/AllureTestNg.java index 87bfefb71..c66503863 100644 --- a/allure-testng/src/main/java/io/qameta/allure/testng/AllureTestNg.java +++ b/allure-testng/src/main/java/io/qameta/allure/testng/AllureTestNg.java @@ -1128,7 +1128,7 @@ private String getUniqueUuid(final IAttributes suite) { return Objects.toString(suite.getAttribute(ALLURE_UUID)); } - @SuppressWarnings({"PMD.AvoidAccessibilityAlteration", "PMD.CognitiveComplexity"}) + @SuppressWarnings("PMD.CognitiveComplexity") private List getParameters(final ITestContext context, final ITestNGMethod method, final Object... parameters) { @@ -1146,7 +1146,16 @@ private List getParameters(final ITestContext context, .filter(s -> !s.isEmpty()) .orElseGet(field::getName); try { - field.setAccessible(true); + if (!field.trySetAccessible()) { + final Module adapterModule = AllureTestNg.class.getModule(); + LOGGER.warn( + "Could not read test instance parameter {}. Open package {} to {}.", + field, + field.getDeclaringClass().getPackageName(), + adapterModule.isNamed() ? adapterModule.getName() : "ALL-UNNAMED" + ); + return; + } final String value = ObjectUtils.toString(field.get(instance)); result.put( name, createParameter( diff --git a/allure-testng/src/main/java/module-info.java b/allure-testng/src/main/java/module-info.java new file mode 100644 index 000000000..c7e469700 --- /dev/null +++ b/allure-testng/src/main/java/module-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.testng { + requires transitive io.qameta.allure.commons; + requires org.testng; + requires org.slf4j; + + exports io.qameta.allure.testng; + + provides org.testng.ITestNGListener with io.qameta.allure.testng.AllureTestNg; +} diff --git a/allure-testng/src/spi-off/java/module-info.java b/allure-testng/src/spi-off/java/module-info.java new file mode 100644 index 000000000..3093e7ae2 --- /dev/null +++ b/allure-testng/src/spi-off/java/module-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module io.qameta.allure.testng { + requires transitive io.qameta.allure.commons; + requires org.testng; + requires org.slf4j; + + exports io.qameta.allure.testng; +} diff --git a/build.gradle.kts b/build.gradle.kts index 5208d4444..988e39cc6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,8 +4,9 @@ val gradleScriptDir by extra("${rootProject.projectDir}/gradle") val qualityConfigsDir by extra("$gradleScriptDir/quality-configs") val spotlessDtr by extra("$qualityConfigsDir/spotless") -val libs = subprojects.filterNot { it.name in "allure-bom" } -val standardJavaLibs = libs.filterNot { it.name == "allure-scalatest" } +val publishedProjects = subprojects.filterNot { it.name == "allure-jpms-tests" } +val javaProjects = subprojects.filterNot { it.name == "allure-bom" } +val standardJavaLibs = publishedProjects.filterNot { it.name in setOf("allure-bom", "allure-scalatest") } val javadocDescriptionProcessorExclusions = setOf( "allure-descriptions-javadoc", "allure-java-commons", @@ -54,6 +55,13 @@ configure(subprojects) { group = "io.qameta.allure" version = version + repositories { + mavenLocal() + mavenCentral() + } +} + +configure(publishedProjects) { apply(plugin = "signing") apply(plugin = "maven-publish") @@ -124,14 +132,9 @@ configure(subprojects) { tasks.withType { enabled = false } - - repositories { - mavenLocal() - mavenCentral() - } } -configure(libs) { +configure(javaProjects) { val project = this apply(plugin = "checkstyle") apply(plugin = "pmd") @@ -206,6 +209,27 @@ configure(libs) { configurations.testCompileClasspath.get().extendsFrom(internal) configurations.testRuntimeClasspath.get().extendsFrom(internal) + if (file("src/main/java/module-info.java").exists()) { + // Some framework JARs have only a filename-derived module name, which Gradle does not infer. + configurations.compileClasspath { + attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR)) + } + tasks.compileJava { + modularity.inferModulePath.set(false) + options.compilerArgumentProviders.add(CommandLineArgumentProvider { + listOf("--module-path", classpath.asPath) + }) + } + tasks.javadoc { + modularity.inferModulePath.set(false) + doFirst { + (options as StandardJavadocDocletOptions).addStringOption( + "p", configurations.compileClasspath.get().asPath + ) + } + } + } + tasks { compileJava { options.compilerArgs.add("-Xlint:-options") @@ -289,6 +313,8 @@ configure(libs) { } fun checkstyleMainJavaSources(): FileTree = mainJavaSources().matching { + // Checkstyle's Java parser does not support module descriptors; javac validates them. + exclude("module-info.java") exclude("cucumber/runtime/formatter/**") } diff --git a/docs/allure-agent-mode.md b/docs/allure-agent-mode.md index 671d7ac36..59d1d616d 100644 --- a/docs/allure-agent-mode.md +++ b/docs/allure-agent-mode.md @@ -35,6 +35,7 @@ Refresh this section when Allure, Gradle, test runners, Allure results paths, re - Project wrapper: `./gradlew`. - Test runner surface: Gradle `Test` tasks. The root build configures library modules to use JUnit Platform with the Allure Gradle adapter; adapter autodetection is disabled. - Test roots: module `src/test/java`, `src/test/groovy`, `src/test/kotlin`, `src/test/scala`, and `src/test/resources`; `allure-citrus/examples/**` has additional example builds and result directories when those example builds are executed. +- JPMS end-to-end tests: the non-published `allure-jpms-tests` project runs JUnit tests that copy Java consumers and JARs into temporary directories and launch `javac`/`java`; one case builds a minimal runtime with `jlink`. Run `:allure-jpms-tests:cleanTest :allure-jpms-tests:test` through `allure agent`; expect 31 consumer cases under `io.qameta.allure.jpms.JpmsIntegrationTest` plus 35 artifact cases under `PublishedModulesTest` (the jOOQ artifact case requires Java 21 and is skipped on Java 17). Its consumer sources are fixture resources, not Gradle source sets. Child result JSON and process logs are attached to the outer test results in `allure-jpms-tests/build/allure-results`. - Main test frameworks and fixtures present in the repo: JUnit Platform/Jupiter, JUnit 4, TestNG, Spock 2, ScalaTest, Cucumber 7 JVM, JBehave 5, Karate, Citrus, Awaitility, AssertJ, Hamcrest, JsonUnit, REST Assured, HTTP client integrations, gRPC, Playwright, Selenide, Selenium BiDi, servlet API, Spring Web, and jOOQ. - Allure results paths: module-local `build/allure-results` from `src/test/resources/allure.properties`. - Result cleanup: local Gradle runs do NOT clean `build/allure-results` between runs, so it accumulates. CI uses `cleanTest`. Before a scoped local `allure agent inspect`, clean the target first (`rm -rf /build/allure-results`, or run `::cleanTest :test`) so the inspected signal is fresh and scoped. diff --git a/settings.gradle.kts b/settings.gradle.kts index 210645603..04542f251 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,7 @@ include("allure-java-commons-test") include("allure-jax-rs") include("allure-jbehave5") include("allure-jooq") +include("allure-jpms-tests") include("allure-jsonunit") include("allure-junit-platform") include("allure-junit4")