diff --git a/allure-junit-platform/src/main/java/io/qameta/allure/junitplatform/AllureJunitPlatform.java b/allure-junit-platform/src/main/java/io/qameta/allure/junitplatform/AllureJunitPlatform.java index 9a053a09..0f9b652c 100644 --- a/allure-junit-platform/src/main/java/io/qameta/allure/junitplatform/AllureJunitPlatform.java +++ b/allure-junit-platform/src/main/java/io/qameta/allure/junitplatform/AllureJunitPlatform.java @@ -140,8 +140,12 @@ public class AllureJunitPlatform implements TestExecutionListener { private static final boolean HAS_CUCUMBERJVM7_IN_CLASSPATH = isClassAvailableOnClasspath("io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm"); + private static final boolean HAS_KARATE_IN_CLASSPATH = isClassAvailableOnClasspath("io.qameta.allure.karate.AllureKarate"); + private static final String ENGINE_SPOCK2 = "spock"; private static final String ENGINE_CUCUMBER = "cucumber"; + private static final String KARATE_JUNIT5_TEST = "com.intuit.karate.junit5.Karate$Test"; + private static final String KARATE_JUNIT6_TEST = "io.karatelabs.junit6.Karate$Test"; private final ThreadLocal testPlanStorage = new InheritableThreadLocal<>(); @@ -217,7 +221,25 @@ private boolean shouldSkipReportingFor(final TestIdentifier testIdentifier) { final String engine = maybeEngine.get(); return HAS_SPOCK2_IN_CLASSPATH && ENGINE_SPOCK2.equals(engine) - || HAS_CUCUMBERJVM7_IN_CLASSPATH && ENGINE_CUCUMBER.equals(engine); + || HAS_CUCUMBERJVM7_IN_CLASSPATH && ENGINE_CUCUMBER.equals(engine) + || HAS_KARATE_IN_CLASSPATH && isKarateTest(testIdentifier); + } + + private boolean isKarateTest(final TestIdentifier testIdentifier) { + return getParents(testIdentifier).stream() + .map(TestIdentifier::getSource) + .filter(Optional::isPresent) + .map(Optional::get) + .map(AllureJunitPlatformUtils::getTestMethod) + .filter(Optional::isPresent) + .map(Optional::get) + .flatMap(method -> Stream.of(method.getAnnotations())) + .map(Annotation::annotationType) + .map(Class::getName) + .anyMatch( + annotation -> KARATE_JUNIT5_TEST.equals(annotation) + || KARATE_JUNIT6_TEST.equals(annotation) + ); } private Optional getEngine(final TestIdentifier testIdentifier) { @@ -240,7 +262,7 @@ private static boolean isClassAvailableOnClasspath(final String clazz) { try { AllureJunitPlatform.class.getClassLoader().loadClass(clazz); return true; - } catch (Exception ignored) { + } catch (LinkageError | Exception ignored) { return false; } } diff --git a/allure-junit-platform/src/test/java/com/intuit/karate/junit5/Karate.java b/allure-junit-platform/src/test/java/com/intuit/karate/junit5/Karate.java new file mode 100644 index 00000000..f371b724 --- /dev/null +++ b/allure-junit-platform/src/test/java/com/intuit/karate/junit5/Karate.java @@ -0,0 +1,55 @@ +/* + * 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 com.intuit.karate.junit5; + +import org.junit.jupiter.api.DynamicContainer; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Iterator; +import java.util.List; + +/** + * Minimal test fixture that reproduces Karate's JUnit dynamic-node shape without introducing a Java 21 dependency. + */ +public final class Karate implements Iterable { + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @TestFactory + public @interface Test { + } + + private Karate() { + } + + public static Karate run() { + return new Karate(); + } + + @Override + public Iterator iterator() { + final DynamicTest scenario = DynamicTest.dynamicTest("[1:3] My scenario", () -> { + }); + final DynamicContainer feature = DynamicContainer.dynamicContainer("Testing web page", List.of(scenario)); + return List.of(feature).iterator(); + } +} diff --git a/allure-junit-platform/src/test/java/io/karatelabs/core/RunListener.java b/allure-junit-platform/src/test/java/io/karatelabs/core/RunListener.java new file mode 100644 index 00000000..ae8014cd --- /dev/null +++ b/allure-junit-platform/src/test/java/io/karatelabs/core/RunListener.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. + */ +package io.karatelabs.core; + +/** + * Test-only stand-in for the optional Karate listener contract. + */ +public interface RunListener { +} diff --git a/allure-junit-platform/src/test/java/io/karatelabs/junit6/Karate.java b/allure-junit-platform/src/test/java/io/karatelabs/junit6/Karate.java new file mode 100644 index 00000000..6f3171fd --- /dev/null +++ b/allure-junit-platform/src/test/java/io/karatelabs/junit6/Karate.java @@ -0,0 +1,55 @@ +/* + * 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.karatelabs.junit6; + +import org.junit.jupiter.api.DynamicContainer; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Iterator; +import java.util.List; + +/** + * Minimal test fixture for the current Karate JUnit package. + */ +public final class Karate implements Iterable { + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @TestFactory + public @interface Test { + } + + private Karate() { + } + + public static Karate run() { + return new Karate(); + } + + @Override + public Iterator iterator() { + final DynamicTest scenario = DynamicTest.dynamicTest("[1:3] Current scenario", () -> { + }); + final DynamicContainer feature = DynamicContainer.dynamicContainer("Current web page", List.of(scenario)); + return List.of(feature).iterator(); + } +} diff --git a/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformClasspathTest.java b/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformClasspathTest.java new file mode 100644 index 00000000..72b12e6e --- /dev/null +++ b/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformClasspathTest.java @@ -0,0 +1,87 @@ +/* + * 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.junitplatform; + +import io.qameta.allure.Description; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThatCode; + +class AllureJunitPlatformClasspathTest { + + private static final String KARATE_ADAPTER = "io.qameta.allure.karate.AllureKarate"; + private static final String KARATE_RUN_LISTENER = "io.karatelabs.core.RunListener"; + private static final Set CHILD_FIRST_CLASSES = Set.of( + AllureJunitPlatform.class.getName(), + KARATE_ADAPTER + ); + + /** + * Keeps JUnit Platform reporting available when an optional adapter links to a missing framework version. + */ + @Test + @Description + void shouldIgnoreMissingOptionalFrameworkDependencyDuringClasspathProbe() { + final ClassLoader classLoader = new MissingOptionalDependencyClassLoader( + AllureJunitPlatformClasspathTest.class.getClassLoader() + ); + + assertThatCode(() -> Class.forName(AllureJunitPlatform.class.getName(), true, classLoader)) + .doesNotThrowAnyException(); + } + + private static final class MissingOptionalDependencyClassLoader extends ClassLoader { + + MissingOptionalDependencyClassLoader(final ClassLoader parent) { + super(parent); + } + + @Override + protected Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException { + if (KARATE_RUN_LISTENER.equals(name)) { + throw new ClassNotFoundException(name); + } + if (!CHILD_FIRST_CLASSES.contains(name)) { + return super.loadClass(name, resolve); + } + synchronized (getClassLoadingLock(name)) { + final Class loadedClass = findLoadedClass(name); + final Class result = loadedClass == null ? defineClassFromParent(name) : loadedClass; + if (resolve) { + resolveClass(result); + } + return result; + } + } + + private Class defineClassFromParent(final String name) throws ClassNotFoundException { + final String resourceName = name.replace('.', '/') + ".class"; + try (InputStream input = getParent().getResourceAsStream(resourceName)) { + if (input == null) { + throw new ClassNotFoundException(name); + } + final byte[] bytes = input.readAllBytes(); + return defineClass(name, bytes, 0, bytes.length); + } catch (IOException e) { + throw new ClassNotFoundException(name, e); + } + } + } +} diff --git a/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformTest.java b/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformTest.java index 5b6ab8b2..275a5833 100644 --- a/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformTest.java +++ b/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformTest.java @@ -16,6 +16,7 @@ package io.qameta.allure.junitplatform; import io.github.glytching.junit.extension.system.SystemProperty; +import io.qameta.allure.Description; import io.qameta.allure.Issue; import io.qameta.allure.junitplatform.features.ActualExpectedStatusDetailsTests; import io.qameta.allure.junitplatform.features.AllureIdAnnotationSupport; @@ -29,6 +30,7 @@ import io.qameta.allure.junitplatform.features.FailedTests; import io.qameta.allure.junitplatform.features.FlakyMutedTest; import io.qameta.allure.junitplatform.features.JupiterUniqueIdTest; +import io.qameta.allure.junitplatform.features.KarateTests; import io.qameta.allure.junitplatform.features.MarkerAnnotationSupport; import io.qameta.allure.junitplatform.features.MetaAnnotationTest; import io.qameta.allure.junitplatform.features.NestedDisplayNameTests; @@ -377,6 +379,21 @@ void shouldProcessDynamicTests() { .containsExactlyInAnyOrder("testA", "testB", "testC"); } + /** + * Ensures the dedicated Karate adapter remains the only reporter for Karate scenarios while ordinary Jupiter + * tests from the same class are still reported by the JUnit Platform integration. + */ + @Test + @Issue("1127") + @Description + void shouldSkipKarateDynamicTestsWhenAllureKarateIsPresent() { + final AllureResults results = runClasses(KarateTests.class); + + assertThat(results.getTestResults()) + .extracting(TestResult::getName) + .containsExactly("ordinaryJupiterTest()"); + } + @Test @AllureFeatures.Parameters void shouldProcessParametrisedTests() { diff --git a/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/features/KarateTests.java b/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/features/KarateTests.java new file mode 100644 index 00000000..86e46def --- /dev/null +++ b/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/features/KarateTests.java @@ -0,0 +1,36 @@ +/* + * 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.junitplatform.features; + +import com.intuit.karate.junit5.Karate; +import org.junit.jupiter.api.Test; + +public class KarateTests { + + @Karate.Test + Karate karateScenarios() { + return Karate.run(); + } + + @io.karatelabs.junit6.Karate.Test + io.karatelabs.junit6.Karate currentKarateScenarios() { + return io.karatelabs.junit6.Karate.run(); + } + + @Test + void ordinaryJupiterTest() { + } +} diff --git a/allure-junit-platform/src/test/java/io/qameta/allure/karate/AllureKarate.java b/allure-junit-platform/src/test/java/io/qameta/allure/karate/AllureKarate.java new file mode 100644 index 00000000..e6e7fd72 --- /dev/null +++ b/allure-junit-platform/src/test/java/io/qameta/allure/karate/AllureKarate.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. + */ +package io.qameta.allure.karate; + +import io.karatelabs.core.RunListener; + +/** + * Classpath marker that represents the dedicated Karate adapter in JUnit Platform integration tests. + */ +public final class AllureKarate implements RunListener { + + private AllureKarate() { + } +} diff --git a/allure-karate/README.md b/allure-karate/README.md index 57c87fa3..814fbb91 100644 --- a/allure-karate/README.md +++ b/allure-karate/README.md @@ -44,6 +44,41 @@ Runner.builder() ## Report Output -- Karate features, scenarios, and steps. +- One Allure test result for each top-level Karate scenario. +- Karate steps, including `call` and `callonce`. +- Called scenarios represented as nested steps under the calling step, including nested embeds and HTTP traffic. - Tags mapped to Allure labels and links where supported. -- Runtime attachments produced by Karate steps. +- Runtime attachments produced by Karate steps, including attachments emitted by a failed step. +- Request and response data in Allure's rich HTTP exchange format. +- Karate 2 multipart embeds. Image comparison parts (`baseline`, `current`, and `diff`) use Allure's image-diff format; other inline parts, URL references, and metadata are preserved as attachments. + +The listener is safe to reuse with Karate's parallel runner. Evidence remains associated with the scenario and step that produced it. + +## Sensitive Output + +The integration follows Karate's reporting privacy settings: + +- A scenario tagged `@report=false` still produces its top-level Allure result and status, but its description, example parameters, steps, embeds, HTTP exchanges, and raw failure details are omitted. A failed scenario uses Karate's redacted failure message. +- Allure's standard HTTP redaction protects common authentication and cookie fields. +- A Karate `configure logging = { mask: ... }` configuration is also applied to HTTP exchange attachments. Header rules, JSON paths, regex patterns, custom replacements, and `enableForUri` are honored before the attachment is written. + +For example: + +```gherkin +* configure logging = + """ + { + mask: { + headers: ['X-Api-Key'], + jsonPaths: ['$.credentials.secret'], + replacement: '***' + } + } + """ +``` + +## JUnit Platform + +When `allure-junit-platform` and `allure-karate` are both present, the JUnit Platform listener ignores the dynamic nodes produced by Karate's legacy JUnit 5 annotation and the current `io.karatelabs.junit6.Karate.Test` annotation. The dedicated Karate listener remains the single source of Karate results, while ordinary Jupiter tests continue to be reported. + +This duplicate suppression does not register the Karate runtime listener. Continue to configure `AllureKarate` through `Runner.builder().listener(...)` as shown above, or through an equivalent Karate runner configuration. diff --git a/allure-karate/build.gradle.kts b/allure-karate/build.gradle.kts index 3b5f0ee9..55fb4a10 100644 --- a/allure-karate/build.gradle.kts +++ b/allure-karate/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { compileOnly("io.karatelabs:karate-core:${karateVersion}") testAnnotationProcessor("org.slf4j:slf4j-simple") testImplementation("io.karatelabs:karate-core:${karateVersion}") + testImplementation("io.karatelabs:karate-junit6:${karateVersion}") testImplementation("io.github.glytching:junit-extensions") testImplementation("org.assertj:assertj-core") testImplementation(project(":allure-assertj")) @@ -21,8 +22,8 @@ dependencies { testImplementation("org.slf4j:slf4j-simple") testImplementation(project(":allure-junit-platform")) testImplementation(project(":allure-java-commons-test")) + testImplementation("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") } tasks.jar { diff --git a/allure-karate/src/main/java/io/qameta/allure/karate/AllureKarate.java b/allure-karate/src/main/java/io/qameta/allure/karate/AllureKarate.java index 44c48534..f554fc7f 100644 --- a/allure-karate/src/main/java/io/qameta/allure/karate/AllureKarate.java +++ b/allure-karate/src/main/java/io/qameta/allure/karate/AllureKarate.java @@ -15,6 +15,8 @@ */ package io.qameta.allure.karate; +import io.karatelabs.common.Json; +import io.karatelabs.core.HttpRunEvent; import io.karatelabs.core.RunEvent; import io.karatelabs.core.RunListener; import io.karatelabs.core.ScenarioResult; @@ -26,10 +28,19 @@ import io.karatelabs.gherkin.Scenario; import io.karatelabs.gherkin.Step; import io.karatelabs.gherkin.Tag; +import io.karatelabs.http.HttpRequest; +import io.karatelabs.http.HttpResponse; +import io.karatelabs.output.LogMask; import io.qameta.allure.Allure; import io.qameta.allure.AllureExternalKey; import io.qameta.allure.AllureLifecycle; import io.qameta.allure.AttachmentOptions; +import io.qameta.allure.http.HttpExchange; +import io.qameta.allure.http.HttpExchangeBody; +import io.qameta.allure.http.HttpExchangeNameValue; +import io.qameta.allure.http.HttpExchangeRequest; +import io.qameta.allure.http.HttpExchangeResponse; +import io.qameta.allure.http.HttpExchangeSerializer; import io.qameta.allure.model.Label; import io.qameta.allure.model.Link; import io.qameta.allure.model.Parameter; @@ -41,15 +52,20 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import java.util.stream.Stream; import static io.qameta.allure.util.ResultsUtils.createLabel; import static io.qameta.allure.util.ResultsUtils.createLink; @@ -64,16 +80,24 @@ *

Register this listener with Karate so features, scenarios, steps, and attachments are converted into Allure * results. The listener uses the Allure lifecycle to write standard result files.

*/ -@SuppressWarnings({"MultipleStringLiterals", "PMD.GodClass"}) +@SuppressWarnings({"MultipleStringLiterals", "PMD.GodClass", "PMD.TooManyMethods"}) public class AllureKarate implements RunListener { private static final Logger LOGGER = LoggerFactory.getLogger(AllureKarate.class); private static final String BUILD_RESOURCES = "build/resources/"; + private static final String HTTP_EXCHANGE_ATTACHMENT = "HTTP exchange"; + private static final String IMAGE_DIFF_CONTENT_TYPE = "application/vnd.allure.image.diff"; + private static final String JSON_CONTENT_TYPE = "application/json"; + private static final String URI_LIST_CONTENT_TYPE = "text/uri-list"; + private static final String BINARY_CONTENT_TYPE = "application/octet-stream"; private final AllureLifecycle lifecycle; - private final Map testCaseUuids = new ConcurrentHashMap<>(); + private final Map scenarioContexts = new ConcurrentHashMap<>(); + private final Map activeStepKeys = new ConcurrentHashMap<>(); + private final Set redactedStepKeys = ConcurrentHashMap.newKeySet(); + private final Set redactedScenarioFailures = ConcurrentHashMap.newKeySet(); /** * Creates an Allure karate with default configuration. @@ -107,19 +131,25 @@ public boolean onEvent(final RunEvent event) { case STEP_EXIT: afterStep((StepRunEvent) event); return true; + case HTTP_EXIT: + afterHttp((HttpRunEvent) event); + return true; default: return true; } } private boolean beforeScenario(final ScenarioRuntime sr) { + if (sr.getFeatureRuntime().isCalled()) { + return beforeCalledScenario(sr); + } + final Scenario scenario = sr.getScenario(); final Feature feature = scenario.getFeature(); final String featureName = feature.getName(); final String featureNameQualified = getFeatureNameQualified(feature); final String uuid = UUID.randomUUID().toString(); - testCaseUuids.put(sr, uuid); final String nameOrLine = getName(scenario, String.valueOf(scenario.getLine())); final String testCaseId = md5(String.format("%s:%s", featureNameQualified, nameOrLine)); @@ -130,7 +160,7 @@ private boolean beforeScenario(final ScenarioRuntime sr) { .setUuid(uuid) .setFullName(fullName) .setName(getName(scenario, fullName)) - .setDescription(getDescription(scenario)) + .setDescription(sr.isReportDisabled() ? null : getDescription(scenario)) .setTestCaseId(testCaseId) .setTitlePath(titlePath); @@ -146,6 +176,59 @@ private boolean beforeScenario(final ScenarioRuntime sr) { lifecycle.scheduleTest(testKey, result); lifecycle.addDefaultLabels(testKey, List.of(ResultsUtils.createFeatureLabel(featureName))); lifecycle.startTest(testKey); + scenarioContexts.put( + sr, + new ScenarioContext(uuid, testKey, null, null, null, true, sr.isReportDisabled()) + ); + return true; + } + + private boolean beforeCalledScenario(final ScenarioRuntime sr) { + final ScenarioRuntime caller = sr.getFeatureRuntime().getCallerScenario(); + if (Objects.isNull(caller)) { + return true; + } + final ScenarioContext callerContext = scenarioContexts.get(caller); + if (Objects.isNull(callerContext)) { + return true; + } + + final AllureExternalKey parentStepKey = activeStepKeys.get(caller); + final AllureExternalKey parentKey = Objects.requireNonNullElse(parentStepKey, callerContext.ownerKey()); + if (sr.isReportDisabled()) { + scenarioContexts.put( + sr, + new ScenarioContext( + callerContext.testUuid(), + parentKey, + null, + parentStepKey, + caller, + false, + true + ) + ); + return true; + } + + final AllureExternalKey scenarioKey = AllureExternalKey.random(AllureKarate.class); + lifecycle.startStep( + parentKey, + scenarioKey, + new io.qameta.allure.model.StepResult().setName(getCalledScenarioName(sr.getScenario())) + ); + scenarioContexts.put( + sr, + new ScenarioContext( + callerContext.testUuid(), + scenarioKey, + scenarioKey, + parentStepKey, + caller, + false, + false + ) + ); return true; } @@ -153,8 +236,12 @@ private static AllureExternalKey testKey(final String scenarioUuid) { return AllureExternalKey.of(AllureKarate.class, "test", scenarioUuid); } - private static AllureExternalKey stepKey(final String scenarioUuid, final int stepIndex) { - return AllureExternalKey.of(AllureKarate.class, "step", scenarioUuid, stepIndex); + private static String getCalledScenarioName(final Scenario scenario) { + final Feature feature = scenario.getFeature(); + final String featureName = Objects.isNull(feature.getName()) || feature.getName().isBlank() + ? getFeatureNameQualified(feature) + : feature.getName().trim(); + return featureName + ": " + getName(scenario, String.valueOf(scenario.getLine())); } private static String getName(final Scenario scenario, final String defaultValue) { @@ -189,35 +276,35 @@ private static String getFeatureNameQualified(final Feature feature) { private void afterScenario(final ScenarioRunEvent event) { final ScenarioRuntime sr = event.source(); - final String uuid = testCaseUuids.remove(sr); - if (Objects.isNull(uuid)) { + final ScenarioContext context = scenarioContexts.remove(sr); + activeStepKeys.remove(sr); + if (Objects.isNull(context)) { return; } final Optional maybeResult = Optional.ofNullable(event.result()); - final Status status = maybeResult - .filter(result -> !result.isFailed()) - .isPresent() - ? Status.PASSED - : maybeResult - .map(ScenarioResult::getError) - .flatMap(ResultsUtils::getStatus) - .orElse(null); + final boolean failed = maybeResult.map(ScenarioResult::isFailed).orElse(true); + final Throwable error = maybeResult.map(ScenarioResult::getError).orElse(null); + final boolean redactFailure = context.reportDisabled() || redactedScenarioFailures.remove(sr); + final Status status = getStatus(failed, error); + final StatusDetails statusDetails = getStatusDetails(failed, error, redactFailure); - final StatusDetails statusDetails = maybeResult - .map(ScenarioResult::getError) - .flatMap(ResultsUtils::getStatusDetails) - .orElse(null); + if (!context.topLevel()) { + finishCalledScenario(context, failed, redactFailure, status, statusDetails); + return; + } final List list = new ArrayList<>(); - if (event.result() != null && event.result().getScenario().getExampleIndex() > -1) { + if (!context.reportDisabled() + && event.result() != null + && event.result().getScenario().getExampleIndex() > -1) { final Map data = event.result().getScenario().getExampleData(); for (Map.Entry entry : data.entrySet()) { list.add(createParameter(entry.getKey(), entry.getValue())); } } - final AllureExternalKey testKey = testKey(uuid); + final AllureExternalKey testKey = testKey(context.testUuid()); lifecycle.updateTest(testKey, tr -> { tr.setStatus(status); tr.setStatusDetails(statusDetails); @@ -228,50 +315,59 @@ private void afterScenario(final ScenarioRunEvent event) { lifecycle.writeTest(testKey); } - private boolean beforeStep(final StepRunEvent event) { - final Step step = event.step(); - final String parentUuid = testCaseUuids.get(event.scenarioRuntime()); - if (Objects.isNull(parentUuid)) { - return true; + private void finishCalledScenario(final ScenarioContext context, + final boolean failed, + final boolean redactFailure, + final Status status, + final StatusDetails statusDetails) { + if (failed && redactFailure) { + if (Objects.nonNull(context.parentStepKey())) { + redactedStepKeys.add(context.parentStepKey()); + } + if (Objects.nonNull(context.callerRuntime())) { + redactedScenarioFailures.add(context.callerRuntime()); + } } + if (Objects.isNull(context.scenarioKey())) { + return; + } + lifecycle.updateStep(context.scenarioKey(), step -> { + step.setStatus(status); + step.setStatusDetails(statusDetails); + }); + lifecycle.stopStep(context.scenarioKey()); + } - if (isCallStep(step)) { + private boolean beforeStep(final StepRunEvent event) { + final ScenarioRuntime scenarioRuntime = event.scenarioRuntime(); + final ScenarioContext context = scenarioContexts.get(scenarioRuntime); + if (Objects.isNull(context) || context.reportDisabled()) { return true; } + final Step step = event.step(); + final AllureExternalKey stepKey = AllureExternalKey.random(AllureKarate.class); final io.qameta.allure.model.StepResult stepResult = new io.qameta.allure.model.StepResult() .setName(getStepName(step)); - lifecycle.startStep(testKey(parentUuid), stepKey(parentUuid, step.getIndex()), stepResult); + lifecycle.startStep(context.ownerKey(), stepKey, stepResult); + activeStepKeys.put(scenarioRuntime, stepKey); return true; } private void afterStep(final StepRunEvent event) { final StepResult result = event.result(); - final String parentUuid = testCaseUuids.get(event.scenarioRuntime()); - if (Objects.isNull(parentUuid)) { - return; - } - - final Step step = result.getStep(); - if (isCallStep(step)) { + final ScenarioRuntime scenarioRuntime = event.scenarioRuntime(); + final ScenarioContext context = scenarioContexts.get(scenarioRuntime); + final AllureExternalKey stepKey = activeStepKeys.remove(scenarioRuntime); + if (Objects.isNull(context) || context.reportDisabled() || Objects.isNull(stepKey)) { return; } - final AllureExternalKey stepKey = stepKey(parentUuid, step.getIndex()); - - final Status status = !result.isFailed() - ? Status.PASSED - : Optional.of(result) - .map(StepResult::getError) - .flatMap(ResultsUtils::getStatus) - .orElse(null); - - final StatusDetails statusDetails = Optional.of(result) - .map(StepResult::getError) - .flatMap(ResultsUtils::getStatusDetails) - .orElse(null); + final boolean redactFailure = redactedStepKeys.remove(stepKey); + final Status status = getStatus(result.isFailed(), result.getError()); + final StatusDetails statusDetails = getStatusDetails(result.isFailed(), result.getError(), redactFailure); lifecycle.updateStep(stepKey, s -> { s.setStatus(status); @@ -279,31 +375,262 @@ private void afterStep(final StepRunEvent event) { }); if (Objects.nonNull(result.getEmbeds())) { - result.getEmbeds().forEach(embed -> { - final byte[] data = embed.getData(); - if (data == null) { - return; - } - try { - lifecycle.addAttachment( - stepKey, - embed.getName(), - embed.getMimeType(), - new ByteArrayInputStream(data), - AttachmentOptions.empty() - ); - } catch (RuntimeException e) { - LOGGER.warn("could not save embedding", e); - } - }); + result.getEmbeds().forEach(embed -> addEmbed(stepKey, embed)); } lifecycle.stopStep(stepKey); } - private static boolean isCallStep(final Step step) { - return "call".equals(step.getKeyword()) || "callonce".equals(step.getKeyword()); + private void afterHttp(final HttpRunEvent event) { + final ScenarioContext context = scenarioContexts.get(event.scenarioRuntime()); + final AllureExternalKey stepKey = activeStepKeys.get(event.scenarioRuntime()); + final HttpRequest request = event.request(); + if (Objects.isNull(context) + || context.reportDisabled() + || Objects.isNull(stepKey) + || Objects.isNull(request)) { + return; + } + + final LogMask mask = getActiveMask(event.scenarioRuntime(), request.getUrlAndPath()); + final HttpResponse response = event.response(); + final HttpExchange.Builder exchange = HttpExchange.builder(toHttpExchangeRequest(request, mask)) + .setStart(Objects.isNull(response) ? null : response.getStartTime()) + .setStop(event.getTimeStamp()); + + if (Objects.nonNull(response)) { + exchange.setResponse(toHttpExchangeResponse(response, mask)); + } + + try { + lifecycle.addAttachment( + stepKey, + HTTP_EXCHANGE_ATTACHMENT, + HttpExchange.CONTENT_TYPE, + new ByteArrayInputStream(HttpExchangeSerializer.toJsonBytes(exchange.build())), + AttachmentOptions.empty() + ); + } catch (RuntimeException e) { + LOGGER.warn("could not save HTTP exchange", e); + } + } + + private static LogMask getActiveMask(final ScenarioRuntime scenarioRuntime, final String uri) { + final LogMask mask = scenarioRuntime.getConfig().getCompiledMask(); + return Objects.nonNull(mask) && mask.enabledForUri(uri) ? mask : null; + } + + private static HttpExchangeRequest toHttpExchangeRequest(final HttpRequest request, final LogMask mask) { + final HttpExchangeRequest.Builder builder = HttpExchangeRequest + .builder(request.getMethod(), request.getUrlAndPath()) + .addHeaders(toNameValues(request.getHeaders(), mask)) + .setBody( + toHttpExchangeBody( + request.getContentType(), + request.getResourceType() != null && request.getResourceType().isBinary(), + request.getBody(), + request.getBodyString(), + mask + ) + ); + + toNameValues(request.getParams(), null) + .forEach(parameter -> builder.addQuery(parameter.name(), parameter.value())); + + return builder.build(); + } + + private static HttpExchangeResponse toHttpExchangeResponse(final HttpResponse response, final LogMask mask) { + return HttpExchangeResponse.builder() + .setStatus(response.getStatus()) + .setStatusText(response.getStatusText()) + .addHeaders(toNameValues(response.getHeaders(), mask)) + .setBody( + toHttpExchangeBody( + response.getContentType(), + response.getResourceType() != null && response.getResourceType().isBinary(), + response.getBodyBytes(), + response.getBodyString(), + mask + ) + ) + .build(); + } + + private static HttpExchangeBody toHttpExchangeBody(final String contentType, + final boolean binary, + final byte[] data, + final String text, + final LogMask mask) { + if (Objects.isNull(data)) { + return null; + } + final String encoding = binary ? "base64" : "utf8"; + final String originalValue = Objects.nonNull(text) + ? text + : new String(data, StandardCharsets.UTF_8); + final String value = binary + ? Base64.getEncoder().encodeToString(data) + : Objects.isNull(mask) ? originalValue : mask.maskBody(originalValue); + return new HttpExchangeBody( + contentType, + encoding, + value, + (long) data.length, + false, + null, + null, + null + ); + } + + private static List toNameValues(final Map> values, + final LogMask mask) { + if (Objects.isNull(values)) { + return List.of(); + } + final List result = new ArrayList<>(); + values.forEach((name, items) -> { + if (Objects.nonNull(items)) { + items.forEach( + value -> result.add( + new HttpExchangeNameValue( + name, + Objects.isNull(mask) ? value : mask.maskHeader(name, value) + ) + ) + ); + } + }); + return result; + } + + private void addEmbed(final AllureExternalKey stepKey, final StepResult.Embed embed) { + try { + if (isImageDiff(embed)) { + addImageDiff(stepKey, embed); + } else { + addEmbedParts(stepKey, embed); + } + addEmbedMetadata(stepKey, embed); + } catch (RuntimeException e) { + LOGGER.warn("could not save embedding", e); + } + } + + private void addImageDiff(final AllureExternalKey stepKey, final StepResult.Embed embed) { + final Map parts = embed.getParts().stream() + .collect(Collectors.toMap(StepResult.Part::getRole, part -> part)); + final Map imageDiff = new LinkedHashMap<>(); + imageDiff.put("expected", toDataUrl(parts.get("baseline"))); + imageDiff.put("actual", toDataUrl(parts.get("current"))); + imageDiff.put("diff", toDataUrl(parts.get("diff"))); + addAttachment( + stepKey, + getEmbedName(embed), + IMAGE_DIFF_CONTENT_TYPE, + Json.toBytes(imageDiff) + ); + } + + private void addEmbedParts(final AllureExternalKey stepKey, final StepResult.Embed embed) { + final List parts = embed.getParts(); + for (int index = 0; index < parts.size(); index++) { + final StepResult.Part part = parts.get(index); + final String role = Objects.isNull(part.getRole()) || part.getRole().isBlank() + ? "part-" + (index + 1) + : part.getRole(); + final boolean primary = parts.size() == 1 && "primary".equals(role); + final String name = primary ? getEmbedName(embed) : getEmbedName(embed) + " [" + role + "]"; + if (Objects.nonNull(part.getData())) { + addAttachment( + stepKey, + name, + Objects.requireNonNullElse(part.getMime(), BINARY_CONTENT_TYPE), + part.getData() + ); + } else if (Objects.nonNull(part.getUrl())) { + addAttachment( + stepKey, + name, + URI_LIST_CONTENT_TYPE, + (part.getUrl() + "\n").getBytes(StandardCharsets.UTF_8) + ); + } + } + } + + private void addEmbedMetadata(final AllureExternalKey stepKey, final StepResult.Embed embed) { + if (Objects.isNull(embed.getMeta()) || embed.getMeta().isEmpty()) { + return; + } + addAttachment( + stepKey, + getEmbedName(embed) + " metadata", + JSON_CONTENT_TYPE, + Json.toBytes(embed.getMeta()) + ); + } + + private void addAttachment(final AllureExternalKey stepKey, + final String name, + final String type, + final byte[] data) { + lifecycle.addAttachment( + stepKey, + name, + type, + new ByteArrayInputStream(data), + AttachmentOptions.empty() + ); + } + + private static boolean isImageDiff(final StepResult.Embed embed) { + if (embed.getParts().size() != 3) { + return false; + } + final Map parts = embed.getParts().stream() + .filter(part -> Objects.nonNull(part.getRole())) + .collect( + Collectors.toMap( + StepResult.Part::getRole, + part -> part, + (first, second) -> first + ) + ); + return Stream.of("baseline", "current", "diff") + .map(parts::get) + .allMatch(part -> Objects.nonNull(part) && Objects.nonNull(part.getData())); + } + + private static String toDataUrl(final StepResult.Part part) { + final String mime = Objects.requireNonNullElse(part.getMime(), BINARY_CONTENT_TYPE); + return "data:" + mime + ";base64," + Base64.getEncoder().encodeToString(part.getData()); + } + + private static String getEmbedName(final StepResult.Embed embed) { + return Objects.isNull(embed.getName()) || embed.getName().isBlank() + ? "attachment" + : embed.getName(); + } + + private static Status getStatus(final boolean failed, final Throwable error) { + return failed + ? Optional.ofNullable(error).flatMap(ResultsUtils::getStatus).orElse(null) + : Status.PASSED; + } + + private static StatusDetails getStatusDetails(final boolean failed, + final Throwable error, + final boolean redact) { + if (!failed) { + return null; + } + if (redact) { + return new StatusDetails().setMessage(ScenarioResult.SUPPRESSED_FAILURE_MESSAGE); + } + return Optional.ofNullable(error).flatMap(ResultsUtils::getStatusDetails).orElse(null); } private static String getStepName(final Step step) { @@ -368,4 +695,14 @@ private List getLinks(final List labels) { } return allureLinks; } + + private record ScenarioContext( + String testUuid, + AllureExternalKey ownerKey, + AllureExternalKey scenarioKey, + AllureExternalKey parentStepKey, + ScenarioRuntime callerRuntime, + boolean topLevel, + boolean reportDisabled) { + } } diff --git a/allure-karate/src/test/java/io/qameta/allure/karate/AllureKarateAdvancedTest.java b/allure-karate/src/test/java/io/qameta/allure/karate/AllureKarateAdvancedTest.java new file mode 100644 index 00000000..9a5edabc --- /dev/null +++ b/allure-karate/src/test/java/io/qameta/allure/karate/AllureKarateAdvancedTest.java @@ -0,0 +1,318 @@ +/* + * 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.karate; + +import io.karatelabs.common.Json; +import io.karatelabs.core.ScenarioResult; +import io.qameta.allure.Description; +import io.qameta.allure.http.HttpExchange; +import io.qameta.allure.junitplatform.AllureJunitPlatform; +import io.qameta.allure.karate.features.KarateJunit6Tests; +import io.qameta.allure.model.Attachment; +import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; +import io.qameta.allure.test.AllureFeatures; +import io.qameta.allure.test.AllureResults; +import io.qameta.allure.test.RunUtils; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.core.LauncherConfig; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +import static io.qameta.allure.model.Status.BROKEN; +import static io.qameta.allure.model.Status.PASSED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +@SuppressWarnings({"MultipleStringLiterals", "PMD.AvoidDuplicateLiterals", "PMD.JUnitTestContainsTooManyAsserts"}) +class AllureKarateAdvancedTest extends TestRunner { + + /** + * Protects Karate's privacy contract: a suppressed scenario keeps only its outcome and safe identity. + */ + @Test + @Description + void shouldSuppressReportDisabledScenarioDetails() { + final AllureResults results = runApi("classpath:testdata/report-disabled.feature"); + final TestResult testResult = results.getTestResultByName("Suppressed failure"); + + assertThat(results.getTestResults()) + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactly(tuple("Suppressed failure", BROKEN)); + assertThat(testResult.getStatus()).isEqualTo(BROKEN); + assertThat(testResult.getDescription()).isNull(); + assertThat(testResult.getSteps()).isEmpty(); + assertThat(testResult.getParameters()).isEmpty(); + assertThat(testResult.getStatusDetails()) + .extracting(details -> details.getMessage(), details -> details.getTrace()) + .containsExactly(ScenarioResult.SUPPRESSED_FAILURE_MESSAGE, null); + assertThat(results.getAttachments()).isEmpty(); + } + + /** + * Ensures an explicitly suppressed callee cannot leak its failure through a visible caller's result or call step. + */ + @Test + @Description + void shouldRedactReportDisabledCalledFailureFromCaller() { + final AllureResults results = run("classpath:testdata/report-disabled-caller.feature"); + final TestResult caller = results.getTestResultByName("Caller of report-disabled feature"); + final StepResult call = directStep( + caller, + "call read('classpath:testdata/called-report-disabled.feature')" + ); + + assertThat(caller.getStatus()).isEqualTo(BROKEN); + assertThat(caller.getStatusDetails()) + .extracting(details -> details.getMessage(), details -> details.getTrace()) + .containsExactly(ScenarioResult.SUPPRESSED_FAILURE_MESSAGE, null); + assertThat(call.getStatus()).isEqualTo(BROKEN); + assertThat(call.getStatusDetails()) + .extracting(details -> details.getMessage(), details -> details.getTrace()) + .containsExactly(ScenarioResult.SUPPRESSED_FAILURE_MESSAGE, null); + assertThat(call.getSteps()).isEmpty(); + assertThat(results.getAttachments()).isEmpty(); + } + + /** + * Ensures Allure's rich HTTP artifact applies Karate's complete configured mask before serialization. + */ + @Test + @AllureFeatures.Attachments + @Description + void shouldApplyConfiguredKarateMaskToHttpExchange() { + final AllureResults results = runApi("classpath:testdata/http-masking.feature"); + final Attachment attachment = findStep( + results.getTestResultByName("Configured HTTP mask"), + "method post" + ).getAttachments().get(0); + + final String exchange = results.getAttachmentContentAsString(attachment); + assertThat(exchange) + .contains("\"name\":\"X-Api-Key\",\"value\":\"[MASKED]\"") + .contains("MASKED-NAME") + .contains("[MASKED]") + .doesNotContain("private-api-key") + .doesNotContain("private-body-value") + .doesNotContain("Soul"); + } + + /** + * Protects the result-count fix without sacrificing evidence produced by called and nested features. + */ + @Test + @AllureFeatures.Attachments + @Description + void shouldKeepCalledFeatureEvidenceUnderCallerResult() { + final AllureResults results = runApi("classpath:testdata/call-callonce.feature"); + + assertThat(results.getTestResults()) + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactly(tuple("Main Scenario with a call", PASSED)); + + final TestResult caller = results.getTestResults().get(0); + final StepResult call = directStep(caller, "call read('classpath:testdata/call-target.feature')"); + final StepResult calledScenario = directStep(call, "Called feature: Called scenario"); + assertThat(call.getStatus()).isEqualTo(PASSED); + assertThat(calledScenario.getSteps()) + .extracting(StepResult::getName) + .containsExactly( + "eval", + "url karate.properties['mock.server.url']", + "path '/called'", + "method get", + "status 200", + "call read('classpath:testdata/nested-call-target.feature')" + ); + assertAttachment(results, findStep(calledScenario, "eval"), "called.txt", "called evidence"); + assertThat( + results.getAttachmentContentAsString( + findStep(calledScenario, "method get").getAttachments().get(0) + ) + ).contains("/called"); + + final StepResult nestedCall = directStep( + calledScenario, + "call read('classpath:testdata/nested-call-target.feature')" + ); + final StepResult nestedScenario = directStep(nestedCall, "Nested called feature: Nested called scenario"); + assertAttachment(results, findStep(nestedScenario, "eval"), "nested.txt", "nested evidence"); + + final StepResult callonce = directStep( + caller, + "callonce read('classpath:testdata/callonce-target.feature')" + ); + final StepResult callonceScenario = directStep(callonce, "Callonce feature: Callonce scenario"); + assertAttachment(results, findStep(callonceScenario, "eval"), "callonce.txt", "callonce evidence"); + } + + /** + * Exercises the listener with four concurrently running scenarios and unique evidence in each result. + */ + @Test + @AllureFeatures.Attachments + @Description + void shouldKeepParallelScenarioEvidenceIsolated() { + final AllureResults results = runApi(4, "classpath:testdata/parallel-evidence.feature"); + + assertThat(results.getTestResults()) + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactlyInAnyOrder( + tuple("Parallel one", PASSED), + tuple("Parallel two", PASSED), + tuple("Parallel three", PASSED), + tuple("Parallel four", PASSED) + ); + assertParallelEvidence(results, "Parallel one", "one"); + assertParallelEvidence(results, "Parallel two", "two"); + assertParallelEvidence(results, "Parallel three", "three"); + assertParallelEvidence(results, "Parallel four", "four"); + } + + /** + * Protects Karate 2's multi-asset embed contract and Allure's rich image-diff rendering. + */ + @Test + @AllureFeatures.Attachments + @Description + void shouldPreserveMultipartEmbeds() { + final AllureResults results = run("classpath:testdata/multipart-attachments.feature"); + final List attachments = findStep( + results.getTestResultByName("Rich multipart attachments"), + "eval" + ).getAttachments(); + + assertThat(attachments) + .extracting(Attachment::getName, Attachment::getType) + .containsExactly( + tuple("visual comparison", "application/vnd.allure.image.diff"), + tuple("visual comparison metadata", "application/json"), + tuple("multi evidence [request]", "text/plain"), + tuple("multi evidence [reference]", "text/uri-list"), + tuple("multi evidence metadata", "application/json") + ); + + final Map imageDiff = Json.of( + Json.parseStrict(results.getAttachmentContentAsString(attachments.get(0))) + ).asMap(); + assertThat(imageDiff) + .containsEntry("expected", "data:image/png;base64,YmFzZWxpbmUtYnl0ZXM=") + .containsEntry("actual", "data:image/png;base64,Y3VycmVudC1ieXRlcw==") + .containsEntry("diff", "data:image/png;base64,ZGlmZi1ieXRlcw=="); + assertThat(results.getAttachmentContentAsString(attachments.get(1))).contains("\"threshold\":0.1"); + assertThat(results.getAttachmentContentAsString(attachments.get(2))).isEqualTo("request evidence"); + assertThat(results.getAttachmentContentAsString(attachments.get(3))) + .isEqualTo("ext/image/reference.png\n"); + assertThat(results.getAttachmentContentAsString(attachments.get(4))) + .contains("\"source\":\"karate extension\""); + } + + /** + * Cross-module smoke coverage with the actual Karate JUnit 6 implementation and both Allure listeners. + */ + @Test + @Description + void shouldAvoidDuplicateResultsForRealKarateJunit6Launch() { + final AllureResults results = RunUtils.runTests(lifecycle -> { + final LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectClass(KarateJunit6Tests.class)) + .build(); + final LauncherConfig config = LauncherConfig.builder() + .enableTestExecutionListenerAutoRegistration(false) + .enablePostDiscoveryFilterAutoRegistration(false) + .addTestExecutionListeners(new AllureJunitPlatform(lifecycle)) + .build(); + final Launcher launcher = LauncherFactory.create(config); + launcher.execute(request); + }); + + assertThat(results.getTestResults()) + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactlyInAnyOrder( + tuple("Karate JUnit 6 smoke scenario", PASSED), + tuple("ordinaryJupiterTest()", PASSED) + ); + } + + private static void assertParallelEvidence(final AllureResults results, + final String scenarioName, + final String id) { + final TestResult testResult = results.getTestResultByName(scenarioName); + final StepResult embedStep = testResult.getSteps().stream() + .filter(step -> step.getName().startsWith("eval karate.embed")) + .findFirst() + .orElseThrow(); + assertAttachment(results, embedStep, "parallel-" + id + ".txt", "payload-" + id); + + final StepResult method = findStep(testResult, "method get"); + assertThat(method.getAttachments()) + .extracting(Attachment::getName, Attachment::getType) + .containsExactly(tuple("HTTP exchange", HttpExchange.CONTENT_TYPE)); + assertThat(results.getAttachmentContentAsString(method.getAttachments().get(0))) + .contains("/parallel/" + id); + } + + private static void assertAttachment(final AllureResults results, + final StepResult step, + final String name, + final String content) { + assertThat(step.getAttachments()) + .extracting(Attachment::getName, Attachment::getType) + .containsExactly(tuple(name, "text/plain")); + assertThat(results.getAttachmentContentAsString(step.getAttachments().get(0))).isEqualTo(content); + } + + private static StepResult directStep(final TestResult result, final String name) { + return result.getSteps().stream() + .filter(step -> Objects.equals(step.getName(), name)) + .findFirst() + .orElseThrow(); + } + + private static StepResult directStep(final StepResult result, final String name) { + return result.getSteps().stream() + .filter(step -> Objects.equals(step.getName(), name)) + .findFirst() + .orElseThrow(); + } + + private static StepResult findStep(final TestResult result, final String name) { + return result.getSteps().stream() + .flatMap(AllureKarateAdvancedTest::flatten) + .filter(step -> Objects.equals(step.getName(), name)) + .findFirst() + .orElseThrow(); + } + + private static StepResult findStep(final StepResult result, final String name) { + return flatten(result) + .filter(step -> Objects.equals(step.getName(), name)) + .findFirst() + .orElseThrow(); + } + + private static Stream flatten(final StepResult step) { + return Stream.concat(Stream.of(step), step.getSteps().stream().flatMap(AllureKarateAdvancedTest::flatten)); + } +} diff --git a/allure-karate/src/test/java/io/qameta/allure/karate/AllureKarateTest.java b/allure-karate/src/test/java/io/qameta/allure/karate/AllureKarateTest.java index 2165a892..33de3eb8 100644 --- a/allure-karate/src/test/java/io/qameta/allure/karate/AllureKarateTest.java +++ b/allure-karate/src/test/java/io/qameta/allure/karate/AllureKarateTest.java @@ -19,17 +19,20 @@ import io.qameta.allure.Allure; import io.qameta.allure.AllureLifecycle; import io.qameta.allure.FileSystemResultsWriter; +import io.qameta.allure.http.HttpExchange; +import io.qameta.allure.model.Attachment; import io.qameta.allure.model.Label; import io.qameta.allure.model.Link; import io.qameta.allure.model.Parameter; import io.qameta.allure.model.Stage; import io.qameta.allure.model.StepResult; import io.qameta.allure.model.TestResult; +import io.qameta.allure.test.AllureFeatures; import io.qameta.allure.test.AllureResults; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.nio.file.Path; +import java.util.List; import static io.qameta.allure.model.Status.BROKEN; import static io.qameta.allure.model.Status.FAILED; @@ -320,51 +323,85 @@ void shouldCreateStepsStatuses() { ); } + @AllureFeatures.Attachments @Test - @Disabled - @SuppressWarnings("PMD.JUnitTestContainsTooManyAsserts") void shouldCreateAttachmentForFailedStep() { - final AllureResults results = run("classpath:testdata/screenshot.feature"); + final AllureResults results = run("classpath:testdata/failed-attachment.feature"); + final TestResult testResult = results.getTestResultByName("Failed step attachment"); - assertThat(results.getTestResults().get(0).getAttachments().get(0).getName()).contains("screenshot_1"); - assertThat(results.getTestResults().get(1).getAttachments().get(0).getName()).contains("screenshot_2"); + assertThat(testResult.getSteps()) + .extracting(StepResult::getName, StepResult::getStatus) + .containsExactly(tuple("eval", BROKEN)); + + final List attachments = testResult.getSteps().get(0).getAttachments(); + assertThat(attachments) + .extracting(Attachment::getName, Attachment::getType) + .containsExactly(tuple("failure-context.txt", "text/plain")); + + final Attachment attachment = attachments.get(0); + assertThat(attachment.getSource()).endsWith(".txt"); + assertThat(results.getAttachmentContentAsString(attachment)).isEqualTo("failure context"); } + @AllureFeatures.Attachments @Test - @Disabled - @SuppressWarnings("PMD.JUnitTestContainsTooManyAsserts") void shouldCreateAttachments() { - final AllureResults results = run("classpath:testdata/web.feature"); + final AllureResults results = run("classpath:testdata/attachments.feature"); + final TestResult testResult = results.getTestResultByName("Named attachments"); - assertThat(results.getTestResults().get(0).getAttachments().size()).isEqualTo(2); + assertThat(testResult.getSteps()) + .extracting(StepResult::getName, StepResult::getStatus) + .containsExactly(tuple("eval", PASSED)); - final String firstAttachment = results.getTestResults().get(0).getAttachments().get(0).getName(); - final String secondAttachment = results.getTestResults().get(0).getAttachments().get(1).getName(); + final List attachments = testResult.getSteps().get(0).getAttachments(); - assertThat(firstAttachment).contains("web_1"); - assertThat(secondAttachment).contains("web_1"); + assertThat(attachments) + .extracting(Attachment::getName, Attachment::getType) + .containsExactly( + tuple("notes.txt", "text/plain"), + tuple("payload.json", "application/json") + ); - final String firstAttachmentDateCreated = firstAttachment.substring( - firstAttachment.lastIndexOf('_') + 1, - firstAttachment.lastIndexOf('.') - ); - final String secondAttachmentDateCreated = secondAttachment.substring( - secondAttachment.lastIndexOf('_') + 1, - secondAttachment.lastIndexOf('.') - ); + assertThat(attachments) + .extracting(Attachment::getSource) + .allSatisfy(source -> assertThat(results.getAttachments()).containsKey(source)); - assertThat(Long.parseLong(secondAttachmentDateCreated)) - .isGreaterThan(Long.parseLong(firstAttachmentDateCreated)); + final List attachmentContents = attachments.stream() + .map(results::getAttachmentContentAsString) + .toList(); + assertThat(attachmentContents) + .containsExactly("plain attachment", "{\"status\":\"ok\"}"); } + @AllureFeatures.Attachments @Test - void shouldSkipCallAndCallOnceStepsInBeforeStep() { - final AllureResults results = runApi("classpath:testdata/call-callonce.feature"); - - assertThat(results.getTestResults()) - .flatExtracting(TestResult::getSteps) - .extracting(StepResult::getName) - .doesNotContain("call", "callonce"); + void shouldCreateHttpRequestAndResponseAttachment() { + final AllureResults results = runApi("classpath:testdata/http-attachments.feature"); + final TestResult testResult = results.getTestResultByName("HTTP request and response attachment"); + final StepResult methodStep = testResult.getSteps().stream() + .filter(step -> "method post".equals(step.getName())) + .findFirst() + .orElseThrow(); + + assertThat(methodStep.getAttachments()) + .extracting(Attachment::getName, Attachment::getType) + .containsExactly(tuple("HTTP exchange", HttpExchange.CONTENT_TYPE)); + + final Attachment attachment = methodStep.getAttachments().get(0); + assertThat(attachment.getSource()).endsWith(HttpExchange.FILE_EXTENSION); + assertThat(results.getAttachments()).containsKey(attachment.getSource()); + + final String exchange = results.getAttachmentContentAsString(attachment); + assertThat(exchange) + .contains("\"schemaVersion\":1") + .contains("\"method\":\"POST\"") + .contains("/users/login") + .contains("\"name\":\"X-Request-Id\",\"value\":\"karate-http-attachment\"") + .contains("\\\"username\\\":\\\"Soul\\\"") + .contains("\"status\":200") + .contains("\\\"message\\\":\\\"User logged in\\\"") + .contains(HttpExchange.REDACTED_VALUE) + .doesNotContain("Bearer secret"); } @Test diff --git a/allure-karate/src/test/java/io/qameta/allure/karate/TestRunner.java b/allure-karate/src/test/java/io/qameta/allure/karate/TestRunner.java index 867b3e22..5842e069 100644 --- a/allure-karate/src/test/java/io/qameta/allure/karate/TestRunner.java +++ b/allure-karate/src/test/java/io/qameta/allure/karate/TestRunner.java @@ -48,11 +48,19 @@ public class TestRunner { protected Path temp; AllureResults runApi(final String... featurePath) { + return runApi(1, featurePath); + } + + AllureResults runApi(final int threads, final String... featurePath) { startServer(); - return run(featurePath); + return run(threads, featurePath); } AllureResults run(final String... path) { + return run(1, path); + } + + AllureResults run(final int threads, final String... path) { return Allure.step("Run Karate features and collect Allure results", () -> RunUtils.runTests(lifecycle -> { final AllureKarate allureKarate = new AllureKarate(lifecycle); @@ -66,7 +74,7 @@ AllureResults run(final String... path) { .outputJunitXml(false) .outputCucumberJson(false) .outputHtmlReport(false) - .parallel(1); + .parallel(threads); } finally { stopServer(); } @@ -136,8 +144,12 @@ private void handle(final Socket socket) throws IOException { private MockResponse getResponse(final String method, final String path) { final String normalizedMethod = method.toUpperCase(Locale.ROOT); + if ("GET".equals(normalizedMethod) && path.startsWith("/parallel/")) { + final String id = path.substring("/parallel/".length()); + return new MockResponse(200, "OK", "{\"id\":\"" + id + "\"}"); + } return switch (normalizedMethod + " " + path) { - case "GET /", "GET /login" -> new MockResponse(200, "OK", ""); + case "GET /", "GET /login", "GET /called" -> new MockResponse(200, "OK", ""); case "POST /login" -> new MockResponse(401, "Unauthorized", "[{\"message\":\"No access\"}]"); case "GET /user" -> new MockResponse(301, "Moved Permanently", ""); case "GET /pages" -> new MockResponse(404, "Not Found", ""); diff --git a/allure-karate/src/test/java/io/qameta/allure/karate/features/KarateJunit6Tests.java b/allure-karate/src/test/java/io/qameta/allure/karate/features/KarateJunit6Tests.java new file mode 100644 index 00000000..001b6851 --- /dev/null +++ b/allure-karate/src/test/java/io/qameta/allure/karate/features/KarateJunit6Tests.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 io.qameta.allure.karate.features; + +import io.karatelabs.core.Runner; +import io.karatelabs.junit6.Karate; +import io.qameta.allure.karate.AllureKarate; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; + +/** + * Real Karate JUnit 6 fixture launched programmatically by the integration smoke test. + */ +public class KarateJunit6Tests { + + @Karate.Test + Iterable karateScenarios() { + final Karate karate = Karate.run("classpath:testdata/junit6-smoke.feature") + .outputHtmlReport(false) + .outputJunitXml(false) + .outputCucumberJson(false); + getDelegate(karate).listener(new AllureKarate()); + return karate; + } + + @Test + void ordinaryJupiterTest() { + } + + private static Runner.Builder getDelegate(final Karate karate) { + try { + final Field field = Karate.class.getDeclaredField("delegate"); + field.setAccessible(true); + return (Runner.Builder) field.get(karate); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Could not register the Allure listener with Karate JUnit 6", e); + } + } +} diff --git a/allure-karate/src/test/resources/testdata/apiResponse.feature b/allure-karate/src/test/resources/testdata/apiResponse.feature deleted file mode 100644 index 440a4bb4..00000000 --- a/allure-karate/src/test/resources/testdata/apiResponse.feature +++ /dev/null @@ -1,15 +0,0 @@ -Feature: API tests - - Scenario: Get request with response body - Given url karate.properties['mock.server.url'] - And path '/users' - When method get - Then status 200 - And match response == [{ id: '1', name: 'Soul' }, { id: '2', name: 'Kate' }] - - Scenario: Post request with response body - Given url karate.properties['mock.server.url'] - And path '/users/login' - When method post - Then status 200 - And match response == { message: 'User logged in', error: null } diff --git a/allure-karate/src/test/resources/testdata/attachments.feature b/allure-karate/src/test/resources/testdata/attachments.feature new file mode 100644 index 00000000..268e9751 --- /dev/null +++ b/allure-karate/src/test/resources/testdata/attachments.feature @@ -0,0 +1,8 @@ +Feature: embedded attachments + + Scenario: Named attachments + * eval + """ + karate.embed('plain attachment', 'text/plain', 'notes.txt'); + karate.embed('{"status":"ok"}', 'application/json', 'payload.json'); + """ diff --git a/allure-karate/src/test/resources/testdata/call-callonce.feature b/allure-karate/src/test/resources/testdata/call-callonce.feature index 5fa194dd..7f5efce7 100644 --- a/allure-karate/src/test/resources/testdata/call-callonce.feature +++ b/allure-karate/src/test/resources/testdata/call-callonce.feature @@ -1,13 +1,9 @@ Feature: Call & Call once Feature - This feature calls another feature and demonstrates Allure reporting issue. + This feature calls other passing features and keeps their evidence under one result. @smoke Scenario: Main Scenario with a call - Given url karate.properties['mock.server.url'] - When method GET - Then status 200 - - * call read('classpath:testdata/apiResponse.feature') - * callonce read('classpath:testdata/api.feature') - - Then print 'Main scenario completed.' + * match 1 == 1 + * call read('classpath:testdata/call-target.feature') + * callonce read('classpath:testdata/callonce-target.feature') + * print 'Main scenario completed.' diff --git a/allure-karate/src/test/resources/testdata/call-target.feature b/allure-karate/src/test/resources/testdata/call-target.feature new file mode 100644 index 00000000..b22dc09b --- /dev/null +++ b/allure-karate/src/test/resources/testdata/call-target.feature @@ -0,0 +1,12 @@ +Feature: Called feature + + Scenario: Called scenario + * eval + """ + karate.embed('called evidence', 'text/plain', 'called.txt') + """ + * url karate.properties['mock.server.url'] + * path '/called' + * method get + * status 200 + * call read('classpath:testdata/nested-call-target.feature') diff --git a/allure-karate/src/test/resources/testdata/called-report-disabled.feature b/allure-karate/src/test/resources/testdata/called-report-disabled.feature new file mode 100644 index 00000000..42ae5269 --- /dev/null +++ b/allure-karate/src/test/resources/testdata/called-report-disabled.feature @@ -0,0 +1,6 @@ +Feature: explicitly suppressed called feature + + @report=false + Scenario: Confidential called failure + * eval karate.embed('called private attachment', 'text/plain', 'called-private.txt') + * match 'called-private-failure-secret' == 'different' diff --git a/allure-karate/src/test/resources/testdata/callonce-target.feature b/allure-karate/src/test/resources/testdata/callonce-target.feature new file mode 100644 index 00000000..c4343f95 --- /dev/null +++ b/allure-karate/src/test/resources/testdata/callonce-target.feature @@ -0,0 +1,8 @@ +Feature: Callonce feature + + Scenario: Callonce scenario + * eval + """ + karate.embed('callonce evidence', 'text/plain', 'callonce.txt') + """ + * match 3 == 3 diff --git a/allure-karate/src/test/resources/testdata/failed-attachment.feature b/allure-karate/src/test/resources/testdata/failed-attachment.feature new file mode 100644 index 00000000..c07735c8 --- /dev/null +++ b/allure-karate/src/test/resources/testdata/failed-attachment.feature @@ -0,0 +1,8 @@ +Feature: failed-step attachment + + Scenario: Failed step attachment + * eval + """ + karate.embed('failure context', 'text/plain', 'failure-context.txt'); + throw new Error('expected failure'); + """ diff --git a/allure-karate/src/test/resources/testdata/http-attachments.feature b/allure-karate/src/test/resources/testdata/http-attachments.feature new file mode 100644 index 00000000..7b0398a6 --- /dev/null +++ b/allure-karate/src/test/resources/testdata/http-attachments.feature @@ -0,0 +1,11 @@ +Feature: HTTP attachments + + Scenario: HTTP request and response attachment + * url karate.properties['mock.server.url'] + * path '/users/login' + * header X-Request-Id = 'karate-http-attachment' + * header Authorization = 'Bearer secret' + * request { username: 'Soul' } + When method post + Then status 200 + And match response == { message: 'User logged in', error: null } diff --git a/allure-karate/src/test/resources/testdata/http-masking.feature b/allure-karate/src/test/resources/testdata/http-masking.feature new file mode 100644 index 00000000..725154a8 --- /dev/null +++ b/allure-karate/src/test/resources/testdata/http-masking.feature @@ -0,0 +1,21 @@ +Feature: HTTP attachment masking + + Scenario: Configured HTTP mask + * configure logging = + """ + { + mask: { + headers: ['X-Api-Key'], + jsonPaths: ['$.customSecret'], + patterns: [{ regex: 'Soul', replacement: 'MASKED-NAME' }], + replacement: '[MASKED]', + enableForUri: function(uri) { return uri.indexOf('/users/login') > -1 } + } + } + """ + * url karate.properties['mock.server.url'] + * path '/users/login' + * header X-Api-Key = 'private-api-key' + * request { username: 'Soul', customSecret: 'private-body-value' } + * method post + * status 200 diff --git a/allure-karate/src/test/resources/testdata/junit6-smoke.feature b/allure-karate/src/test/resources/testdata/junit6-smoke.feature new file mode 100644 index 00000000..9af1429e --- /dev/null +++ b/allure-karate/src/test/resources/testdata/junit6-smoke.feature @@ -0,0 +1,4 @@ +Feature: Karate JUnit 6 smoke + + Scenario: Karate JUnit 6 smoke scenario + * match 1 == 1 diff --git a/allure-karate/src/test/resources/testdata/multipart-attachments.feature b/allure-karate/src/test/resources/testdata/multipart-attachments.feature new file mode 100644 index 00000000..1ffd432c --- /dev/null +++ b/allure-karate/src/test/resources/testdata/multipart-attachments.feature @@ -0,0 +1,23 @@ +Feature: multipart attachments + + Scenario: Rich multipart attachments + * eval + """ + karate.embed({ + name: 'visual comparison', + parts: [ + { role: 'baseline', mime: 'image/png', data: 'baseline-bytes' }, + { role: 'current', mime: 'image/png', data: 'current-bytes' }, + { role: 'diff', mime: 'image/png', data: 'diff-bytes' } + ], + meta: { threshold: 0.1 } + }); + karate.embed({ + name: 'multi evidence', + parts: [ + { role: 'request', mime: 'text/plain', data: 'request evidence' }, + { role: 'reference', mime: 'image/png', url: 'ext/image/reference.png' } + ], + meta: { source: 'karate extension' } + }); + """ diff --git a/allure-karate/src/test/resources/testdata/nested-call-target.feature b/allure-karate/src/test/resources/testdata/nested-call-target.feature new file mode 100644 index 00000000..e24086ec --- /dev/null +++ b/allure-karate/src/test/resources/testdata/nested-call-target.feature @@ -0,0 +1,8 @@ +Feature: Nested called feature + + Scenario: Nested called scenario + * eval + """ + karate.embed('nested evidence', 'text/plain', 'nested.txt') + """ + * match 2 == 2 diff --git a/allure-karate/src/test/resources/testdata/parallel-evidence.feature b/allure-karate/src/test/resources/testdata/parallel-evidence.feature new file mode 100644 index 00000000..6eff2ebd --- /dev/null +++ b/allure-karate/src/test/resources/testdata/parallel-evidence.feature @@ -0,0 +1,29 @@ +Feature: parallel evidence + + Scenario: Parallel one + * eval karate.embed('payload-one', 'text/plain', 'parallel-one.txt') + * url karate.properties['mock.server.url'] + * path '/parallel/one' + * method get + * status 200 + + Scenario: Parallel two + * eval karate.embed('payload-two', 'text/plain', 'parallel-two.txt') + * url karate.properties['mock.server.url'] + * path '/parallel/two' + * method get + * status 200 + + Scenario: Parallel three + * eval karate.embed('payload-three', 'text/plain', 'parallel-three.txt') + * url karate.properties['mock.server.url'] + * path '/parallel/three' + * method get + * status 200 + + Scenario: Parallel four + * eval karate.embed('payload-four', 'text/plain', 'parallel-four.txt') + * url karate.properties['mock.server.url'] + * path '/parallel/four' + * method get + * status 200 diff --git a/allure-karate/src/test/resources/testdata/report-disabled-caller.feature b/allure-karate/src/test/resources/testdata/report-disabled-caller.feature new file mode 100644 index 00000000..85857af7 --- /dev/null +++ b/allure-karate/src/test/resources/testdata/report-disabled-caller.feature @@ -0,0 +1,4 @@ +Feature: caller of suppressed feature + + Scenario: Caller of report-disabled feature + * call read('classpath:testdata/called-report-disabled.feature') diff --git a/allure-karate/src/test/resources/testdata/report-disabled-child.feature b/allure-karate/src/test/resources/testdata/report-disabled-child.feature new file mode 100644 index 00000000..658f1fbd --- /dev/null +++ b/allure-karate/src/test/resources/testdata/report-disabled-child.feature @@ -0,0 +1,5 @@ +Feature: inherited reporting suppression + + Scenario: Confidential inherited child + * eval karate.embed('private child attachment', 'text/plain', 'private-child.txt') + * match 'private-child-failure-secret' == 'different' diff --git a/allure-karate/src/test/resources/testdata/report-disabled.feature b/allure-karate/src/test/resources/testdata/report-disabled.feature new file mode 100644 index 00000000..8a247962 --- /dev/null +++ b/allure-karate/src/test/resources/testdata/report-disabled.feature @@ -0,0 +1,12 @@ +Feature: reporting suppression + + @report=false + Scenario: Suppressed failure + This confidential description must not be reported. + * eval karate.embed('private attachment', 'text/plain', 'private.txt') + * url karate.properties['mock.server.url'] + * path '/users/login' + * header X-Private = 'private-header-secret' + * request { privateValue: 'private-body-secret' } + * method post + * call read('classpath:testdata/report-disabled-child.feature') diff --git a/allure-karate/src/test/resources/testdata/screenshot.feature b/allure-karate/src/test/resources/testdata/screenshot.feature deleted file mode 100644 index b105e272..00000000 --- a/allure-karate/src/test/resources/testdata/screenshot.feature +++ /dev/null @@ -1,12 +0,0 @@ -Feature: attachments - - Background: - * configure driver = { type: 'chrome', timeout: 5000, screenshotOnFailure: true, showDriverLog: true } - - Scenario: Screenshot attachment - Given driver 'https://docs.qameta.io/allure-testops/' - Then match true == false - - Scenario: Screenshot attachment - Given driver 'https://docs.qameta.io/allure-testops/' - Then match false == true \ No newline at end of file diff --git a/allure-karate/src/test/resources/testdata/web.feature b/allure-karate/src/test/resources/testdata/web.feature deleted file mode 100644 index 1a4737b6..00000000 --- a/allure-karate/src/test/resources/testdata/web.feature +++ /dev/null @@ -1,20 +0,0 @@ -Feature: browser automation 1 - - Background: - * configure driver = { type: 'chrome' } - # * configure driverTarget = { docker: 'justinribeiro/chrome-headless', showDriverLog: true } - # * configure driverTarget = { docker: 'ptrthomas/karate-chrome', showDriverLog: true } - # * configure driver = { type: 'chromedriver', showDriverLog: true } - # * configure driver = { type: 'geckodriver', showDriverLog: true } - # * configure driver = { type: 'safaridriver', showDriverLog: true } - # * configure driver = { type: 'iedriver', showDriverLog: true, httpConfig: { readTimeout: 120000 } } - - Scenario: try to login to github - - Given driver 'https://github.com/login' - And screenshot() - And input('#login_field', 'dummy') - And input('#password', 'world') - And screenshot() - When submit().click("input[name=commit]") - Then match html('#js-flash-container') contains 'Incorrect username or password.'