NAVIGATION_METHODS = new HashSet<>(
+ Arrays.asList(
+ "asBase64Decoded",
+ "asBoolean",
+ "asByte",
+ "asDouble",
+ "asFloat",
+ "asInstanceOf",
+ "asInt",
+ "asList",
+ "asLong",
+ "asShort",
+ "asString",
+ "bytes",
+ "decodedAsBase64",
+ "element",
+ "elements",
+ "extracting",
+ "extractingResultOf",
+ "first",
+ "flatExtracting",
+ "flatMap",
+ "last",
+ "map",
+ "rootCause",
+ "singleElement",
+ "size",
+ "usingRecursiveAssertion",
+ "usingRecursiveComparison"
+ )
+ );
+
+ private AssertJMethodSupport() {
+ throw new IllegalStateException("do not instantiate");
+ }
+
+ static boolean isIgnored(final String methodName) {
+ return IGNORED_METHODS.contains(methodName);
+ }
+
+ static String normalize(final String methodName) {
+ final int accessorIndex = methodName.indexOf("$accessor$");
+ if (accessorIndex > 0) {
+ return methodName.substring(0, accessorIndex);
+ }
+ if (DESCRIBED_AS.equals(methodName)) {
+ return AS;
+ }
+ return methodName;
+ }
+
+ static boolean isDescription(final String methodName) {
+ return AS.equals(methodName) || DESCRIBED_AS.equals(methodName);
+ }
+
+ static boolean isNavigation(final String methodName) {
+ return NAVIGATION_METHODS.contains(methodName);
+ }
+}
diff --git a/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJOperation.java b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJOperation.java
new file mode 100644
index 000000000..f06b8be49
--- /dev/null
+++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJOperation.java
@@ -0,0 +1,160 @@
+/*
+ * 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.assertj;
+
+import io.qameta.allure.model.Parameter;
+import io.qameta.allure.model.Stage;
+import io.qameta.allure.model.Status;
+import io.qameta.allure.model.StatusDetails;
+import io.qameta.allure.model.StepResult;
+
+import java.util.List;
+
+import static io.qameta.allure.util.ResultsUtils.getStatus;
+import static io.qameta.allure.util.ResultsUtils.getStatusDetails;
+
+/**
+ * Child Allure step for one meaningful AssertJ fluent operation.
+ *
+ * An operation is the report entry for one fluent method call inside an {@link AssertJChain}. The recorder creates
+ * it before proceeding with the intercepted AssertJ call, marks it passed or failed after the call returns, and keeps
+ * it attached to the chain that owns the assertion object. Earlier operations remain passed when a later operation
+ * fails, so the report shows the exact point where the assertion chain stopped matching the expectation.
+ *
+ * For a simple assertion, each checked method becomes one operation:
+ * {@code
+ * assertThat("Data").startsWith("Da").endsWith("ta")
+ *
+ * assert "Data"
+ * starts with "Da"
+ * ends with "ta"
+ * }
+ *
+ * For navigation methods, the operation name is enriched with the returned subject. The returned AssertJ object
+ * still belongs to the same chain, so the report stays readable as one story:
+ * {@code
+ * assertThat(users).first(InstanceOfAssertFactories.STRING).startsWith("alice")
+ *
+ * assert 1 string
+ * first element as InstanceOfAssertFactory -> "alice@example.org"
+ * starts with "alice"
+ * }
+ *
+ * For failures, this operation receives the failure status and status details, and the parent chain receives the
+ * same status. This makes the failed operation visible without losing the successful context before it:
+ * {@code
+ * assertThat("Data").startsWith("Da").hasSize(5)
+ *
+ * assert "Data" FAILED
+ * starts with "Da" PASSED
+ * has size 5 FAILED
+ * }
+ *
+ * Some AssertJ methods call other assertion methods internally. Those calls should not become extra child steps
+ * because they would duplicate implementation details instead of user intent. The {@code nestedLevel} counter lets the
+ * recorder reuse the active operation while those internal calls run, then finish only the user-visible operation.
+ */
+final class AssertJOperation {
+
+ private final AssertJChain chain;
+
+ private final String methodName;
+
+ private final StepResult step;
+
+ private final boolean navigation;
+
+ private String returnedSubject;
+
+ private int nestedLevel;
+
+ AssertJOperation(final AssertJChain chain,
+ final String methodName,
+ final String name,
+ final List parameters,
+ final boolean navigation) {
+ this.chain = chain;
+ this.methodName = methodName;
+ this.navigation = navigation;
+ this.step = new StepResult()
+ .setName(name)
+ .setParameters(parameters)
+ .setStage(Stage.RUNNING)
+ .setStart(System.currentTimeMillis());
+ }
+
+ AssertJChain getChain() {
+ return chain;
+ }
+
+ StepResult getStep() {
+ return step;
+ }
+
+ boolean isNavigation() {
+ return navigation;
+ }
+
+ boolean isDescription() {
+ return AssertJMethodSupport.isDescription(methodName);
+ }
+
+ boolean isNested() {
+ return nestedLevel > 0;
+ }
+
+ AssertJOperation nested() {
+ nestedLevel++;
+ return this;
+ }
+
+ void leaveNested() {
+ nestedLevel--;
+ }
+
+ void setReturnedSubject(final String subject) {
+ if (returnedSubject != null) {
+ return;
+ }
+
+ returnedSubject = subject;
+ step.setName(AssertJValueRenderer.truncateStepName(step.getName() + " -> " + subject));
+ }
+
+ void passed() {
+ if (step.getStatus() == null) {
+ step.setStatus(Status.PASSED);
+ }
+ finish();
+ }
+
+ void failed(final Throwable throwable) {
+ final Status status = getStatus(throwable).orElse(Status.BROKEN);
+ final StatusDetails details = getStatusDetails(throwable).orElse(null);
+ step
+ .setStatus(status)
+ .setStatusDetails(details);
+ chain.updateStatus(status, details);
+ finish();
+ }
+
+ private void finish() {
+ step
+ .setStage(Stage.FINISHED)
+ .setStop(System.currentTimeMillis());
+ chain.finish();
+ }
+}
diff --git a/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJRecorder.java b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJRecorder.java
new file mode 100644
index 000000000..18794e577
--- /dev/null
+++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJRecorder.java
@@ -0,0 +1,261 @@
+/*
+ * 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.assertj;
+
+import io.qameta.allure.AllureLifecycle;
+import org.assertj.core.api.AbstractAssert;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Records AssertJ objects by identity and builds one Allure step tree per assertion chain.
+ *
+ * The recorder is the stateful part behind {@link AllureAspectJ}. The aspect only detects user-side AssertJ
+ * factory calls and fluent operation calls; this class decides which {@link AssertJChain} owns the assertion object,
+ * where a new {@link AssertJOperation} should be attached, and how pass/fail state should be reflected in the retained
+ * Allure {@code StepResult} tree.
+ *
+ * Each aspect thread gets its own recorder instance. Assertion objects are tracked in an {@link IdentityHashMap}
+ * because AssertJ assertion classes can override {@code equals} and {@code hashCode}; object identity is the only safe
+ * way to know that a later fluent call belongs to the same assertion object that was created earlier.
+ *
+ * The normal hard-assertion flow is:
+ * {@code
+ * assertThat("Data").startsWith("Da").endsWith("ta")
+ *
+ * assertionCreated(assertThat result, "Data")
+ * startOperation(startsWith, ["Da"])
+ * operationPassed(startsWith)
+ * startOperation(endsWith, ["ta"])
+ * operationPassed(endsWith)
+ *
+ * assert "Data"
+ * starts with "Da"
+ * ends with "ta"
+ * }
+ *
+ * Stored assertion instances keep separate chains because the map key is the assertion instance itself:
+ * {@code
+ * final AbstractStringAssert> a = assertThat("alpha");
+ * final AbstractStringAssert> b = assertThat("bravo");
+ *
+ * a.isEqualTo("alpha");
+ * b.isEqualTo("bravo");
+ *
+ * assert "alpha"
+ * is equal to "alpha"
+ * assert "bravo"
+ * is equal to "bravo"
+ * }
+ *
+ * Navigation operations such as {@code extracting}, {@code first}, and {@code asInstanceOf} may return new AssertJ
+ * assertion objects. Those returned objects are registered against the existing chain, so later checks stay under the
+ * same parent step:
+ * {@code
+ * assertThat(results).extracting(Result::getName).containsExactly("passed")
+ *
+ * assert 1 Result item
+ * extracts Result::getName -> 1 string
+ * contains exactly ["passed"]
+ * }
+ *
+ * The {@code operations} stack tracks the currently executing user-visible operation. It has two jobs: assertions
+ * created inside callbacks such as {@code satisfies} are attached beneath the active operation, and AssertJ internal
+ * calls on the same chain are counted as nested work instead of being reported as extra steps.
+ *
+ * {@code
+ * assertThat("alpha").satisfies(value -> assertThat(value).startsWith("al"))
+ *
+ * assert "alpha"
+ * satisfies
+ * assert "alpha"
+ * starts with "al"
+ * }
+ *
+ * Soft assertion failures are reported before {@code assertAll()} throws. The AssertJ error collector callback calls
+ * {@link #softAssertionFailed(AssertionError)}, which marks the active operation and its chain as failed while
+ * preserving the earlier passed operations.
+ */
+final class AssertJRecorder {
+
+ private final Map, AssertJChain> chains = new IdentityHashMap<>();
+
+ private final Deque operations = new ArrayDeque<>();
+
+ private final AssertJValueRenderer renderer = new AssertJValueRenderer();
+
+ void assertionCreated(final AllureLifecycle lifecycle,
+ final AbstractAssert, ?> assertion,
+ final Object actual) {
+ if (chains.containsKey(assertion)) {
+ return;
+ }
+
+ final AssertJOperation activeOperation = activeOperation();
+ if (isNavigationResult(activeOperation)) {
+ chains.put(assertion, activeOperation.getChain());
+ return;
+ }
+
+ final AssertJChain chain = new AssertJChain(assertion, renderer.renderSubject(actual));
+ chains.put(assertion, chain);
+ attachChain(lifecycle, chain, activeOperation);
+ }
+
+ AssertJOperation startOperation(final AllureLifecycle lifecycle,
+ final AbstractAssert, ?> assertion,
+ final String methodName,
+ final Object... args) {
+ final AssertJChain chain = chainFor(lifecycle, assertion);
+ final String normalizedName = AssertJMethodSupport.normalize(methodName);
+
+ final AssertJOperation activeOperation = activeOperation();
+ if (isInternalCallOnSameChain(activeOperation, chain)) {
+ return activeOperation.nested();
+ }
+
+ final AssertJOperation operation = new AssertJOperation(
+ chain,
+ normalizedName,
+ renderer.renderOperation(normalizedName, args),
+ renderer.renderParameters(normalizedName, args),
+ AssertJMethodSupport.isNavigation(normalizedName)
+ );
+ chain.addOperation(operation);
+ operations.push(operation);
+ return operation;
+ }
+
+ void operationPassed(final AssertJOperation operation, final Object result) {
+ if (operation.isNested()) {
+ pop(operation);
+ return;
+ }
+
+ registerReturnedAssertion(operation, result);
+ renameChainFromDescription(operation);
+ operation.passed();
+ pop(operation);
+ }
+
+ void operationFailed(final AssertJOperation operation, final Throwable throwable) {
+ operation.failed(throwable);
+ pop(operation);
+ }
+
+ void softAssertionFailed(final AssertionError error) {
+ final AssertJOperation current = activeOperation();
+ if (current != null) {
+ current.failed(error);
+ }
+ }
+
+ boolean isIgnored(final String methodName) {
+ return AssertJMethodSupport.isIgnored(methodName);
+ }
+
+ private AssertJChain chainFor(final AllureLifecycle lifecycle, final AbstractAssert, ?> assertion) {
+ final AssertJChain chain = chains.get(assertion);
+ if (chain != null) {
+ return chain;
+ }
+
+ final AssertJChain created = new AssertJChain(assertion, renderer.renderSubject(actualOf(assertion)));
+ chains.put(assertion, created);
+ attachChain(lifecycle, created, activeOperation());
+ return created;
+ }
+
+ private void attachChain(final AllureLifecycle lifecycle,
+ final AssertJChain chain,
+ final AssertJOperation parentOperation) {
+ if (parentOperation == null) {
+ lifecycle.getCurrentExecutableKey().ifPresent(parent -> {
+ lifecycle.startStep(parent, chain.getKey(), chain.getStep());
+ lifecycle.stopStep(chain.getKey());
+ });
+ return;
+ }
+
+ parentOperation.getStep().getSteps().add(chain.getStep());
+ }
+
+ private void registerReturnedAssertion(final AssertJOperation operation, final Object result) {
+ if (!(result instanceof AbstractAssert)) {
+ return;
+ }
+
+ final AbstractAssert, ?> returned = (AbstractAssert, ?>) result;
+ chains.put(returned, operation.getChain());
+ if (operation.isNavigation()) {
+ operation.setReturnedSubject(renderer.renderSubject(actualOf(returned)));
+ }
+ }
+
+ private void renameChainFromDescription(final AssertJOperation operation) {
+ if (operation.isDescription()) {
+ operation.getChain().rename(descriptionOf(operation.getChain().getAssertion()));
+ }
+ }
+
+ private AssertJOperation activeOperation() {
+ return operations.peek();
+ }
+
+ private boolean isNavigationResult(final AssertJOperation activeOperation) {
+ return activeOperation != null && activeOperation.isNavigation();
+ }
+
+ private boolean isInternalCallOnSameChain(final AssertJOperation activeOperation, final AssertJChain chain) {
+ return activeOperation != null && activeOperation.getChain() == chain;
+ }
+
+ private void pop(final AssertJOperation operation) {
+ if (operation.isNested()) {
+ operation.leaveNested();
+ return;
+ }
+ if (!operations.isEmpty() && operations.peek() == operation) {
+ operations.pop();
+ }
+ }
+
+ private Object actualOf(final AbstractAssert, ?> assertion) {
+ return AllureAspectJ.withoutRecording(() -> {
+ try {
+ return assertion.actual();
+ } catch (RuntimeException e) {
+ return null;
+ }
+ });
+ }
+
+ private Optional descriptionOf(final AbstractAssert, ?> assertion) {
+ return AllureAspectJ.withoutRecording(() -> {
+ try {
+ return Optional.ofNullable(assertion.descriptionText())
+ .map(String::trim)
+ .filter(value -> !value.isEmpty());
+ } catch (RuntimeException e) {
+ return Optional.empty();
+ }
+ });
+ }
+}
diff --git a/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJValueRenderer.java b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJValueRenderer.java
new file mode 100644
index 000000000..49f9ba6ac
--- /dev/null
+++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJValueRenderer.java
@@ -0,0 +1,561 @@
+/*
+ * 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.assertj;
+
+import io.qameta.allure.model.Parameter;
+import io.qameta.allure.util.ObjectUtils;
+import org.assertj.core.description.Description;
+import org.assertj.core.groups.Tuple;
+
+import java.lang.reflect.Array;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Path;
+import java.time.temporal.TemporalAccessor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static io.qameta.allure.util.ResultsUtils.getLambdaName;
+
+/**
+ * Renders AssertJ subjects and arguments into semantic step names.
+ */
+@SuppressWarnings("all")
+final class AssertJValueRenderer {
+
+ private static final int STEP_NAME_LIMIT = 1000;
+
+ private static final int INLINE_VALUE_LIMIT = 3;
+
+ private static final String LAMBDA = "";
+
+ private static final String TRUNCATED = "...";
+
+ String renderSubject(final Object value) {
+ return truncateStepName(renderSubjectValue(value));
+ }
+
+ String renderOperation(final String methodName, final Object[] args) {
+ return truncateStepName(renderOperationName(methodName, args));
+ }
+
+ List renderParameters(final String methodName, final Object[] args) {
+ final Object[] values = parameterArguments(methodName, args);
+ if (values.length == 0) {
+ return Collections.emptyList();
+ }
+
+ final String renderedOperation = renderOperation(methodName, args);
+ final List parameters = new ArrayList<>();
+ for (int index = 0; index < values.length; index++) {
+ final String value = renderParameterValue(values[index]);
+ if (renderedOperation.contains(value)) {
+ continue;
+ }
+ parameters.add(
+ new Parameter()
+ .setName(parameterName(methodName, index))
+ .setValue(value)
+ .setMode(Parameter.Mode.DEFAULT)
+ );
+ }
+ return parameters;
+ }
+
+ static String truncateStepName(final String value) {
+ if (value == null || value.length() <= STEP_NAME_LIMIT) {
+ return value;
+ }
+ return value.substring(0, STEP_NAME_LIMIT - TRUNCATED.length()) + TRUNCATED;
+ }
+
+ private String renderSubjectValue(final Object value) {
+ if (value == null) {
+ return "null";
+ }
+ if (value instanceof CharSequence || isSimple(value)) {
+ return renderSimple(value);
+ }
+ if (value instanceof Collection) {
+ if (isInlineCollection((Collection>) value)) {
+ return renderCollectionValue((Collection>) value);
+ }
+ return renderCollectionSubject((Collection>) value);
+ }
+ if (value instanceof Map) {
+ return "map with " + renderEntryCount(((Map, ?>) value).size());
+ }
+ if (value.getClass().isArray()) {
+ return renderArraySubject(value);
+ }
+ if (value instanceof Iterable) {
+ return "iterable";
+ }
+ return simpleClassName(value);
+ }
+
+ private Object[] parameterArguments(final String methodName, final Object[] args) {
+ if (isDescriptionWithEmptyValues(args)) {
+ return new Object[]{args[0]};
+ }
+ if (isSingleVarargToUnwrap(methodName, args)) {
+ return new Object[]{Array.get(args[0], 0)};
+ }
+ return args;
+ }
+
+ private String parameterName(final String methodName, final int index) {
+ if ("hasFieldOrPropertyWithValue".equals(methodName)) {
+ return index == 0 ? "field or property" : "expected value";
+ }
+
+ if (index > 0) {
+ return "argument " + (index + 1);
+ }
+
+ switch (methodName) {
+ case "as":
+ return "description";
+ case "asInstanceOf":
+ case "first":
+ case "singleElement":
+ return "factory";
+ case "extracting":
+ case "flatExtracting":
+ return "extractor";
+ case "hasSize":
+ return "expected size";
+ case "satisfies":
+ return "condition";
+ case "contains":
+ case "containsExactly":
+ case "containsExactlyInAnyOrder":
+ case "endsWith":
+ case "isEqualTo":
+ case "startsWith":
+ return "expected";
+ default:
+ return "argument 1";
+ }
+ }
+
+ private String renderParameterValue(final Object value) {
+ return renderArgument(value);
+ }
+
+ private String renderOperationName(final String methodName, final Object[] args) {
+ if (args.length == 0) {
+ return readableMethodName(methodName);
+ }
+
+ final String arguments = renderArguments(methodName, args);
+ switch (methodName) {
+ case "as":
+ return "described as " + arguments;
+ case "asInstanceOf":
+ return "as instance of " + arguments;
+ case "contains":
+ return "contains " + arguments;
+ case "containsExactly":
+ return "contains exactly " + arguments;
+ case "containsExactlyInAnyOrder":
+ return "contains exactly in any order " + arguments;
+ case "endsWith":
+ return "ends with " + arguments;
+ case "extracting":
+ return "extracts " + arguments;
+ case "flatExtracting":
+ return "flat extracts " + arguments;
+ case "first":
+ return "first element as " + arguments;
+ case "hasFieldOrPropertyWithValue":
+ return renderHasFieldOrPropertyWithValue(args);
+ case "hasSize":
+ return "has size " + arguments;
+ case "isEqualTo":
+ return "is equal to " + arguments;
+ case "singleElement":
+ return "single element as " + arguments;
+ case "startsWith":
+ return "starts with " + arguments;
+ case "satisfies":
+ return "satisfies " + arguments;
+ default:
+ return readableMethodName(methodName) + " " + arguments;
+ }
+ }
+
+ private String renderHasFieldOrPropertyWithValue(final Object[] args) {
+ if (args.length != 2) {
+ return "has field or property with value " + renderEach(args);
+ }
+ return "has field or property " + renderArgument(args[0]) + " with value " + renderArgument(args[1]);
+ }
+
+ private String readableMethodName(final String methodName) {
+ if (methodName.startsWith("is") && methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2))) {
+ return "is " + splitCamelCase(methodName.substring(2));
+ }
+ if (methodName.startsWith("has") && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) {
+ return "has " + splitCamelCase(methodName.substring(3));
+ }
+ return splitCamelCase(methodName);
+ }
+
+ private String splitCamelCase(final String value) {
+ return value
+ .replaceAll("([a-z0-9])([A-Z])", "$1 $2")
+ .toLowerCase();
+ }
+
+ private String renderArguments(final String methodName, final Object[] args) {
+ if (isDescriptionWithEmptyValues(args)) {
+ return renderArgument(args[0]);
+ }
+ if (isSingleVarargToUnwrap(methodName, args)) {
+ return renderArgument(Array.get(args[0], 0));
+ }
+ if (isSingleArrayArgument(args)) {
+ return renderArray(args[0]);
+ }
+ return renderEach(args);
+ }
+
+ private boolean isDescriptionWithEmptyValues(final Object[] args) {
+ return args.length == 2
+ && args[1] != null
+ && args[1].getClass().isArray()
+ && Array.getLength(args[1]) == 0;
+ }
+
+ private boolean isSingleVarargToUnwrap(final String methodName, final Object[] args) {
+ return args.length == 1
+ && args[0] != null
+ && args[0].getClass().isArray()
+ && Array.getLength(args[0]) == 1
+ && shouldUnwrapSingleVararg(methodName);
+ }
+
+ private boolean isSingleArrayArgument(final Object[] args) {
+ return args.length == 1
+ && args[0] != null
+ && args[0].getClass().isArray();
+ }
+
+ private boolean shouldUnwrapSingleVararg(final String methodName) {
+ return !methodName.contains("Any")
+ && !methodName.contains("Exactly")
+ && !methodName.contains("Only")
+ && !methodName.contains("Sequence")
+ && !methodName.contains("Subsequence")
+ && !methodName.endsWith("In");
+ }
+
+ private String renderEach(final Object[] args) {
+ final List values = new ArrayList<>();
+ for (Object arg : args) {
+ values.add(renderArgument(arg));
+ }
+ return values.stream().collect(Collectors.joining(", "));
+ }
+
+ private String renderArgument(final Object value) {
+ if (value == null) {
+ return "null";
+ }
+ if (isLambda(value)) {
+ return renderLambda(value);
+ }
+ if (value instanceof Description) {
+ return renderSimple(value.toString());
+ }
+ if (value instanceof Tuple) {
+ return renderTuple((Tuple) value);
+ }
+ if (value instanceof CharSequence || isSimple(value)) {
+ return renderSimple(value);
+ }
+ if (value instanceof Collection) {
+ if (isInlineCollection((Collection>) value)) {
+ return renderCollectionValue((Collection>) value);
+ }
+ return renderCollectionSubject((Collection>) value);
+ }
+ if (value instanceof Map) {
+ return "map with " + renderEntryCount(((Map, ?>) value).size());
+ }
+ if (value.getClass().isArray()) {
+ return renderArray(value);
+ }
+ return simpleClassName(value);
+ }
+
+ private String renderArray(final Object array) {
+ if (array instanceof byte[]) {
+ return ObjectUtils.toString(array);
+ }
+
+ final int length = Array.getLength(array);
+ if (array.getClass().getComponentType().isPrimitive()) {
+ return ObjectUtils.toString(array);
+ }
+ if (allLambdas(array, length)) {
+ return length == 1 ? renderLambda(Array.get(array, 0)) : lambdaList(array, length);
+ }
+ if (allSimple(array, length) || !array.getClass().getComponentType().isPrimitive()) {
+ return renderObjectArray(array, length);
+ }
+ return array.getClass().getComponentType().getSimpleName() + "[](length=" + length + ")";
+ }
+
+ private String renderCollectionSubject(final Collection> value) {
+ final int size = value.size();
+ if (size == 0) {
+ return "empty collection";
+ }
+ return commonElementType(value)
+ .map(type -> renderElementCount(size, type))
+ .orElseGet(() -> renderItemCount(size));
+ }
+
+ private String renderArraySubject(final Object array) {
+ final int length = Array.getLength(array);
+ if (isInlineArray(array, length)) {
+ return renderArrayValue(array, length);
+ }
+ if (array instanceof byte[]) {
+ return "byte array with " + renderByteCount(length);
+ }
+ return renderElementCount(length, array.getClass().getComponentType());
+ }
+
+ private boolean isInlineCollection(final Collection> value) {
+ return value.size() <= INLINE_VALUE_LIMIT && allInlineValues(value);
+ }
+
+ private boolean allInlineValues(final Collection> value) {
+ for (Object item : value) {
+ if (!isInlineValue(item)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isInlineValue(final Object value) {
+ return value == null
+ || isLambda(value)
+ || value instanceof Description
+ || isInlineTuple(value)
+ || value instanceof CharSequence
+ || isSimple(value);
+ }
+
+ private boolean isInlineTuple(final Object value) {
+ if (!(value instanceof Tuple)) {
+ return false;
+ }
+ final Object[] values = ((Tuple) value).toArray();
+ return isInlineArray(values, values.length);
+ }
+
+ private String renderTuple(final Tuple tuple) {
+ final Object[] values = tuple.toArray();
+ return renderObjectArray(values, values.length)
+ .replaceFirst("^\\[", "(")
+ .replaceFirst("]$", ")");
+ }
+
+ private String renderCollectionValue(final Collection> value) {
+ final List values = new ArrayList<>();
+ for (Object item : value) {
+ values.add(renderArgument(item));
+ }
+ return values.stream().collect(Collectors.joining(", ", "[", "]"));
+ }
+
+ private boolean isInlineArray(final Object array, final int length) {
+ if (length > INLINE_VALUE_LIMIT || array instanceof byte[]) {
+ return false;
+ }
+ if (array.getClass().getComponentType().isPrimitive()) {
+ return true;
+ }
+ for (int i = 0; i < length; i++) {
+ if (!isInlineValue(Array.get(array, i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private String renderArrayValue(final Object array, final int length) {
+ if (array.getClass().getComponentType().isPrimitive()) {
+ return ObjectUtils.toString(array);
+ }
+ return renderObjectArray(array, length);
+ }
+
+ private Optional> commonElementType(final Collection> value) {
+ Class> result = null;
+ for (Object item : value) {
+ if (item == null) {
+ continue;
+ }
+ final Class> itemType = elementTypeOf(item);
+ if (result == null) {
+ result = itemType;
+ } else if (!result.equals(itemType)) {
+ return java.util.Optional.empty();
+ }
+ }
+ return java.util.Optional.ofNullable(result);
+ }
+
+ private Class> elementTypeOf(final Object item) {
+ if (item instanceof Collection) {
+ return Collection.class;
+ }
+ if (item instanceof Map) {
+ return Map.class;
+ }
+ return item.getClass();
+ }
+
+ private String renderElementCount(final int size, final Class> type) {
+ if (String.class.equals(type)) {
+ return size + " " + pluralize("string", size);
+ }
+ if (Boolean.class.equals(type) || Boolean.TYPE.equals(type)) {
+ return size + " " + pluralize("boolean", size);
+ }
+ if (Character.class.equals(type) || Character.TYPE.equals(type)) {
+ return size + " " + pluralize("character", size);
+ }
+ if (Number.class.isAssignableFrom(type) || type.isPrimitive() && !Boolean.TYPE.equals(type)
+ && !Character.TYPE.equals(type)) {
+ return size + " " + pluralize("number", size);
+ }
+ if (Collection.class.equals(type)) {
+ return size + " " + pluralize("collection", size);
+ }
+ if (Map.class.equals(type)) {
+ return size + " " + pluralize("map", size);
+ }
+ return size + " " + type.getSimpleName() + " " + pluralize("item", size);
+ }
+
+ private String renderItemCount(final int size) {
+ return size + " " + pluralize("item", size);
+ }
+
+ private String renderEntryCount(final int size) {
+ return size + " " + pluralize("entry", size);
+ }
+
+ private String renderByteCount(final int size) {
+ return size + " " + pluralize("byte", size);
+ }
+
+ private String pluralize(final String word, final int count) {
+ return count == 1 ? word : word + "s";
+ }
+
+ private String renderObjectArray(final Object array, final int length) {
+ final List values = new ArrayList<>();
+ for (int i = 0; i < length; i++) {
+ values.add(renderArgument(Array.get(array, i)));
+ }
+ return values.stream().collect(Collectors.joining(", ", "[", "]"));
+ }
+
+ private boolean allLambdas(final Object array, final int length) {
+ if (length == 0) {
+ return false;
+ }
+ for (int i = 0; i < length; i++) {
+ if (!isLambda(Array.get(array, i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private String lambdaList(final Object array, final int length) {
+ final List values = new ArrayList<>();
+ for (int i = 0; i < length; i++) {
+ values.add(renderLambda(Array.get(array, i)));
+ }
+ return values.stream().collect(Collectors.joining(", ", "[", "]"));
+ }
+
+ private boolean allSimple(final Object array, final int length) {
+ for (int i = 0; i < length; i++) {
+ final Object item = Array.get(array, i);
+ if (item != null && !isSimple(item) && !(item instanceof CharSequence)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isSimple(final Object value) {
+ return value instanceof Number
+ || value instanceof Boolean
+ || value instanceof Character
+ || value instanceof Enum
+ || value instanceof Path
+ || value instanceof URI
+ || value instanceof URL
+ || value instanceof TemporalAccessor;
+ }
+
+ private boolean isLambda(final Object value) {
+ if (value == null) {
+ return false;
+ }
+ final Class> type = value.getClass();
+ return type.isSynthetic() || type.getName().contains("$$Lambda$");
+ }
+
+ private String renderLambda(final Object value) {
+ return getLambdaName(value)
+ .orElse(LAMBDA);
+ }
+
+ private String renderSimple(final Object value) {
+ if (value instanceof CharSequence || value instanceof Character) {
+ return "\"" + ObjectUtils.toString(value) + "\"";
+ }
+ if (value instanceof Enum) {
+ return ((Enum>) value).name();
+ }
+ return ObjectUtils.toString(value);
+ }
+
+ private String simpleClassName(final Object value) {
+ final Class> type = value.getClass();
+ if (type.isAnonymousClass()) {
+ return type.getSuperclass().getSimpleName();
+ }
+ return type.getSimpleName();
+ }
+}
diff --git a/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.FixtureLifecycleListener b/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.FixtureLifecycleListener
new file mode 100644
index 000000000..65aaf3eca
--- /dev/null
+++ b/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.FixtureLifecycleListener
@@ -0,0 +1 @@
+io.qameta.allure.assertj.AssertJLifecycleListener
diff --git a/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener b/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener
new file mode 100644
index 000000000..65aaf3eca
--- /dev/null
+++ b/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener
@@ -0,0 +1 @@
+io.qameta.allure.assertj.AssertJLifecycleListener
diff --git a/allure-assertj/src/test/java/io/qameta/allure/assertj/AllureAspectJTest.java b/allure-assertj/src/test/java/io/qameta/allure/assertj/AllureAspectJTest.java
index a4c311e29..2f1410be0 100644
--- a/allure-assertj/src/test/java/io/qameta/allure/assertj/AllureAspectJTest.java
+++ b/allure-assertj/src/test/java/io/qameta/allure/assertj/AllureAspectJTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 Qameta Software OÜ
+ * 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.
@@ -15,92 +15,538 @@
*/
package io.qameta.allure.assertj;
+import io.qameta.allure.model.Parameter;
+import io.qameta.allure.model.Status;
+import io.qameta.allure.model.StatusDetails;
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.IsolatedLifecycle;
+import org.assertj.core.api.AbstractStringAssert;
+import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
+import java.io.Serializable;
import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.function.Function;
import static io.qameta.allure.test.RunUtils.runWithinTestContext;
import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * @author charlie (Dmitry Baev).
- */
+import static org.assertj.core.api.Assertions.tuple;
+@IsolatedLifecycle
class AllureAspectJTest {
@AllureFeatures.Steps
@Test
- void shouldCreateStepsForAsserts() {
+ void shouldCreateSemanticChainForScalarAssert() {
final AllureResults results = runWithinTestContext(() -> {
assertThat("Data")
.hasSize(4);
- }, AllureAspectJ::setLifecycle);
- assertThat(results.getTestResults())
- .flatExtracting(TestResult::getSteps)
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName, StepResult::getStatus)
+ .containsExactly(tuple("assert \"Data\"", Status.PASSED));
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
.extracting(StepResult::getName)
- .containsExactly(
- "assertThat 'Data'",
- "hasSize '4'"
- );
+ .containsExactly("has size 4");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .flatExtracting(StepResult::getParameters)
+ .isEmpty();
}
@AllureFeatures.Steps
@Test
- void shouldHandleNullableObject() {
+ void shouldUseAssertDescriptionAsChainName() {
final AllureResults results = runWithinTestContext(() -> {
assertThat((Object) null)
.as("Nullable object")
.isNull();
- }, AllureAspectJ::setLifecycle);
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .containsExactly("assert Nullable object");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("described as \"Nullable object\"", "is null");
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldRenderByteArraysWithoutPayload() {
+ final String value = "some string";
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat(value.getBytes(StandardCharsets.UTF_8))
+ .as("Byte array object")
+ .isEqualTo(value.getBytes(StandardCharsets.UTF_8));
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .containsExactly("assert Byte array object");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("described as \"Byte array object\"", "is equal to ");
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldRenderCollectionsAsSubjectsAndExpectedValuesAsValues() {
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat(Arrays.asList("a", "b"))
+ .containsExactly("a", "b");
+ });
- assertThat(results.getTestResults())
- .flatExtracting(TestResult::getSteps)
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .containsExactly("assert [\"a\", \"b\"]");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("contains exactly [\"a\", \"b\"]");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .flatExtracting(StepResult::getParameters)
+ .isEmpty();
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldRenderSmallArraysAsValues() {
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat(new int[]{1, 2})
+ .containsExactly(1, 2);
+
+ assertThat(new String[]{"alpha", "bravo"})
+ .containsExactly("alpha", "bravo");
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .containsExactly("assert [1, 2]", "assert [\"alpha\", \"bravo\"]");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("contains exactly [1, 2]", "contains exactly [\"alpha\", \"bravo\"]");
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldRenderTuplesAsValues() {
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat(
+ Arrays.asList(
+ tuple("first", Status.PASSED),
+ tuple("second", Status.FAILED)
+ )
+ )
+ .containsExactly(
+ tuple("first", Status.PASSED),
+ tuple("second", Status.FAILED)
+ );
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .containsExactly("assert [(\"first\", PASSED), (\"second\", FAILED)]");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("contains exactly [(\"first\", PASSED), (\"second\", FAILED)]");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .flatExtracting(StepResult::getParameters)
+ .isEmpty();
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldRenderNullValuesInContainsExactlyInAnyOrder() {
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat(Arrays.asList(null, "a", "b"))
+ .containsExactlyInAnyOrder(null, "a", "b");
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .containsExactly("assert [null, \"a\", \"b\"]");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("contains exactly in any order [null, \"a\", \"b\"]");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .flatExtracting(StepResult::getParameters)
+ .isEmpty();
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldRenderNullValuesAfterExtractingAndKeepLambdaVarargs() {
+ final TestResult model = new TestResult();
+
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat(model)
+ .extracting(TestResult::getDescription, TestResult::getDescriptionHtml)
+ .containsExactly(null, null);
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .containsExactly("assert TestResult");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
.extracting(StepResult::getName)
.containsExactly(
- "assertThat 'null'",
- "as 'Nullable object []'",
- "isNull"
+ "extracts [, ] -> [null, null]",
+ "contains exactly [null, null]"
);
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .flatExtracting(StepResult::getParameters)
+ .isEmpty();
}
@AllureFeatures.Steps
@Test
- void shouldHandleByteArrayObject() {
- final String s = "some string";
+ void shouldRenderFieldOrPropertyValueAssertions() {
+ final StatusDetails details = new StatusDetails()
+ .setMessage("Make the test failed");
+
final AllureResults results = runWithinTestContext(() -> {
- assertThat(s.getBytes(StandardCharsets.UTF_8))
- .as("Byte array object")
- .isEqualTo(s.getBytes(StandardCharsets.UTF_8));
- }, AllureAspectJ::setLifecycle);
+ assertThat(details)
+ .hasFieldOrPropertyWithValue("message", "Make the test failed");
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .containsExactly("assert StatusDetails");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("has field or property \"message\" with value \"Make the test failed\"");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .flatExtracting(StepResult::getParameters)
+ .isEmpty();
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldTruncateLongStepNamesAndAddOnlyTruncatedValuesAsParameters() {
+ final String value = String.join("", Collections.nCopies(1200, "a"));
- assertThat(results.getTestResults())
- .flatExtracting(TestResult::getSteps)
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat(value)
+ .isEqualTo(value);
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .singleElement()
+ .asString()
+ .hasSize(1000)
+ .startsWith("assert \"")
+ .endsWith("...");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .singleElement()
+ .asString()
+ .hasSize(1000)
+ .startsWith("is equal to \"")
+ .endsWith("...");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .flatExtracting(StepResult::getParameters)
+ .extracting(Parameter::getName, Parameter::getValue)
+ .containsExactly(tuple("expected", "\"" + value + "\""));
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldCreateSeparateChainsForMultipleAssertThatCalls() {
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat("Data")
+ .hasSize(4);
+
+ assertThat(42)
+ .isPositive()
+ .isEqualTo(42);
+
+ assertThat(Arrays.asList("a", "b"))
+ .hasSize(2)
+ .contains("a");
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName, StepResult::getStatus)
+ .containsExactly(
+ tuple("assert \"Data\"", Status.PASSED),
+ tuple("assert 42", Status.PASSED),
+ tuple("assert [\"a\", \"b\"]", Status.PASSED)
+ );
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly(
+ "has size 4",
+ "is positive",
+ "is equal to 42",
+ "has size 2",
+ "contains \"a\""
+ );
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldAttachOperationsToStoredAssertionInstances() {
+ final String targetA = "alpha";
+ final String targetB = "bravo";
+
+ final AllureResults results = runWithinTestContext(() -> {
+ final AbstractStringAssert> a = assertThat(targetA);
+ final AbstractStringAssert> b = assertThat(targetB);
+
+ a.isEqualTo("alpha");
+ b.isEqualTo("bravo");
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName, StepResult::getStatus)
+ .containsExactly(
+ tuple("assert \"alpha\"", Status.PASSED),
+ tuple("assert \"bravo\"", Status.PASSED)
+ );
+ assertThat(result.getSteps())
+ .filteredOn("name", "assert \"alpha\"")
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("is equal to \"alpha\"");
+ assertThat(result.getSteps())
+ .filteredOn("name", "assert \"bravo\"")
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("is equal to \"bravo\"");
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldAvoidVerboseModelToStringPayloads() {
+ final TestResult model = new TestResult()
+ .setUuid("uid")
+ .setName("testPassed")
+ .setFullName("other.PassingTest.testPassed");
+
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat(Collections.singletonList(model))
+ .hasSize(1)
+ .containsExactly(model);
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .containsExactly("assert 1 TestResult item");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("has size 1", "contains exactly [TestResult]");
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .noneMatch(name -> name.contains("fullName="))
+ .noneMatch(name -> name.contains("other.PassingTest.testPassed"));
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .noneMatch(name -> name.contains("fullName="))
+ .noneMatch(name -> name.contains("other.PassingTest.testPassed"));
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldKeepNavigationInsideTheSameChain() {
+ final TestResult model = new TestResult()
+ .setFullName("my.company.Test.testOne");
+
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat(Collections.singletonList(model))
+ .extracting(TestResult::getFullName)
+ .containsExactly("my.company.Test.testOne");
+
+ assertThat(Collections.singletonList("alpha"))
+ .first(InstanceOfAssertFactories.STRING)
+ .startsWith("al");
+
+ assertThat(Collections.singletonList("bravo"))
+ .singleElement(InstanceOfAssertFactories.STRING)
+ .endsWith("vo");
+
+ assertThat((Object) "charlie")
+ .asInstanceOf(InstanceOfAssertFactories.STRING)
+ .contains("har");
+
+ assertThat(Collections.singletonList(Collections.singletonList("delta")))
+ .flatExtracting(value -> value)
+ .containsExactly("delta");
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .hasSize(5);
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
.extracting(StepResult::getName)
.containsExactly(
- "assertThat ''",
- "describedAs 'Byte array object'",
- "isEqualTo ''"
+ "extracts -> [\"my.company.Test.testOne\"]",
+ "contains exactly [\"my.company.Test.testOne\"]",
+ "first element as InstanceOfAssertFactory -> \"alpha\"",
+ "starts with \"al\"",
+ "single element as InstanceOfAssertFactory -> \"bravo\"",
+ "ends with \"vo\"",
+ "as instance of InstanceOfAssertFactory -> \"charlie\"",
+ "contains \"har\"",
+ "flat extracts -> [\"delta\"]",
+ "contains exactly [\"delta\"]"
);
}
@AllureFeatures.Steps
@Test
- void softAssertions() {
+ void shouldRenderSerializedLambdaMethodReferences() {
+ final TestResult model = new TestResult()
+ .setFullName("my.company.Test.testOne");
+
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat(Collections.singletonList(model))
+ .extracting((Function & Serializable) TestResult::getFullName)
+ .containsExactly("my.company.Test.testOne");
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly(
+ "extracts TestResult::getFullName -> [\"my.company.Test.testOne\"]",
+ "contains exactly [\"my.company.Test.testOne\"]"
+ );
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldMarkTheFailedHardAssertionOperation() {
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat("Data")
+ .hasSize(5);
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName, StepResult::getStatus)
+ .containsExactly(tuple("assert \"Data\"", Status.FAILED));
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName, StepResult::getStatus)
+ .containsExactly(tuple("has size 5", Status.FAILED));
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .filteredOn("name", "has size 5")
+ .extracting(step -> step.getStatusDetails().getMessage())
+ .singleElement()
+ .asString()
+ .contains("size");
+ }
+
+ @AllureFeatures.Steps
+ @Test
+ void shouldMarkTheFailedSoftAssertionOperationBeforeAssertAll() {
final AllureResults results = runWithinTestContext(() -> {
final SoftAssertions soft = new SoftAssertions();
soft.assertThat(25)
- .as("Test description")
+ .as("Age")
.isEqualTo(26);
soft.assertAll();
- }, AllureAspectJ::setLifecycle);
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName, StepResult::getStatus)
+ .containsExactly(tuple("assert Age", Status.FAILED));
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName, StepResult::getStatus)
+ .containsExactly(
+ tuple("described as \"Age\"", Status.PASSED),
+ tuple("is equal to 26", Status.FAILED)
+ );
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .filteredOn("name", "is equal to 26")
+ .extracting(step -> step.getStatusDetails().getMessage())
+ .singleElement()
+ .asString()
+ .contains("expected: 26");
+ }
- assertThat(results.getTestResults())
- .flatExtracting(TestResult::getSteps)
+ @AllureFeatures.Steps
+ @Test
+ void shouldAttachNestedAssertionsUnderCallbackOperations() {
+ final AllureResults results = runWithinTestContext(() -> {
+ assertThat("alpha")
+ .satisfies(
+ value -> assertThat(value)
+ .startsWith("al")
+ .endsWith("ha")
+ );
+ });
+
+ final TestResult result = assertOnlyOneResult(results);
+ assertThat(result.getSteps())
+ .extracting(StepResult::getName)
+ .containsExactly("assert \"alpha\"");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
.extracting(StepResult::getName)
- .contains("as 'Test description []'", "isEqualTo '26'");
+ .containsExactly("satisfies ");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .filteredOn("name", "satisfies ")
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("assert \"alpha\"");
+ assertThat(result.getSteps())
+ .flatExtracting(StepResult::getSteps)
+ .filteredOn("name", "satisfies ")
+ .flatExtracting(StepResult::getSteps)
+ .flatExtracting(StepResult::getSteps)
+ .extracting(StepResult::getName)
+ .containsExactly("starts with \"al\"", "ends with \"ha\"");
+ }
+
+ private TestResult assertOnlyOneResult(final AllureResults results) {
+ assertThat(results.getTestResults()).hasSize(1);
+ return results.getTestResults().get(0);
}
}
diff --git a/allure-assertj/src/test/resources/allure.properties b/allure-assertj/src/test/resources/allure.properties
index 9c0b0a2d7..c881472e6 100644
--- a/allure-assertj/src/test/resources/allure.properties
+++ b/allure-assertj/src/test/resources/allure.properties
@@ -1,2 +1,3 @@
allure.results.directory=build/allure-results
allure.label.epic=#project.description#
+allure.label.module=allure-assertj
diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentContent.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentContent.java
deleted file mode 100644
index ab26abbb6..000000000
--- a/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentContent.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2019 Qameta Software OÜ
- *
- * 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.attachment;
-
-/**
- * @author charlie (Dmitry Baev).
- */
-public class DefaultAttachmentContent implements AttachmentContent {
-
- private final String content;
-
- private final String contentType;
-
- private final String fileExtension;
-
- public DefaultAttachmentContent(final String content,
- final String contentType,
- final String fileExtension) {
- this.content = content;
- this.contentType = contentType;
- this.fileExtension = fileExtension;
- }
-
- @Override
- public String getContent() {
- return content;
- }
-
- @Override
- public String getContentType() {
- return contentType;
- }
-
- @Override
- public String getFileExtension() {
- return fileExtension;
- }
-}
diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentProcessor.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentProcessor.java
deleted file mode 100644
index dc57e9a01..000000000
--- a/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentProcessor.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2019 Qameta Software OÜ
- *
- * 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.attachment;
-
-import io.qameta.allure.Allure;
-import io.qameta.allure.AllureLifecycle;
-
-import java.nio.charset.StandardCharsets;
-
-/**
- * @author charlie (Dmitry Baev).
- */
-public class DefaultAttachmentProcessor implements AttachmentProcessor {
-
- private final AllureLifecycle lifecycle;
-
- public DefaultAttachmentProcessor() {
- this(Allure.getLifecycle());
- }
-
- public DefaultAttachmentProcessor(final AllureLifecycle lifecycle) {
- this.lifecycle = lifecycle;
- }
-
- @Override
- public void addAttachment(final AttachmentData attachmentData,
- final AttachmentRenderer renderer) {
- final AttachmentContent content = renderer.render(attachmentData);
- lifecycle.addAttachment(
- attachmentData.getName(),
- content.getContentType(),
- content.getFileExtension(),
- content.getContent().getBytes(StandardCharsets.UTF_8)
- );
- }
-}
diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/FreemarkerAttachmentRenderer.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/FreemarkerAttachmentRenderer.java
deleted file mode 100644
index 9179f3ed7..000000000
--- a/allure-attachments/src/main/java/io/qameta/allure/attachment/FreemarkerAttachmentRenderer.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2019 Qameta Software OÜ
- *
- * 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.attachment;
-
-import freemarker.template.Configuration;
-import freemarker.template.Template;
-
-import java.io.StringWriter;
-import java.io.Writer;
-import java.util.Collections;
-
-/**
- * @author charlie (Dmitry Baev).
- */
-public class FreemarkerAttachmentRenderer implements AttachmentRenderer {
-
- private final Configuration configuration;
-
- private final String templateName;
-
- public FreemarkerAttachmentRenderer(final String templateName) {
- this.templateName = templateName;
- this.configuration = new Configuration(Configuration.VERSION_2_3_23);
- this.configuration.setLocalizedLookup(false);
- this.configuration.setTemplateUpdateDelayMilliseconds(0);
- this.configuration.setClassLoaderForTemplateLoading(getClass().getClassLoader(), "tpl");
- }
-
- @Override
- public DefaultAttachmentContent render(final AttachmentData data) {
- try (Writer writer = new StringWriter()) {
- final Template template = configuration.getTemplate(templateName);
- template.process(Collections.singletonMap("data", data), writer);
- return new DefaultAttachmentContent(writer.toString(), "text/html", ".html");
- } catch (Exception e) {
- throw new AttachmentRenderException("Could't render http attachment file", e);
- }
- }
-
-}
diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpRequestAttachment.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpRequestAttachment.java
deleted file mode 100644
index 831b2a5da..000000000
--- a/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpRequestAttachment.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright 2019 Qameta Software OÜ
- *
- * 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.attachment.http;
-
-import io.qameta.allure.attachment.AttachmentData;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-
-/**
- * @author charlie (Dmitry Baev).
- */
-public class HttpRequestAttachment implements AttachmentData {
-
- private final String name;
-
- private final String url;
-
- private final String method;
-
- private final String body;
-
- private final String curl;
-
- private final Map headers;
-
- private final Map cookies;
-
- public HttpRequestAttachment(final String name, final String url, final String method,
- final String body, final String curl, final Map headers,
- final Map cookies) {
- this.name = name;
- this.url = url;
- this.method = method;
- this.body = body;
- this.curl = curl;
- this.headers = headers;
- this.cookies = cookies;
- }
-
- public String getUrl() {
- return url;
- }
-
- public String getMethod() {
- return method;
- }
-
- public String getBody() {
- return body;
- }
-
- public Map getHeaders() {
- return headers;
- }
-
- public Map getCookies() {
- return cookies;
- }
-
- public String getCurl() {
- return curl;
- }
-
- @Override
- public String getName() {
- return name;
- }
-
- /**
- * Builder for HttpRequestAttachment.
- */
- @SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
- public static final class Builder {
-
- private final String name;
-
- private final String url;
-
- private String method;
-
- private String body;
-
- private final Map headers = new HashMap<>();
-
- private final Map cookies = new HashMap<>();
-
- private Builder(final String name, final String url) {
- Objects.requireNonNull(name, "Name must not be null value");
- Objects.requireNonNull(url, "Url must not be null value");
- this.name = name;
- this.url = url;
- }
-
- public static Builder create(final String attachmentName, final String url) {
- return new Builder(attachmentName, url);
- }
-
- public Builder setMethod(final String method) {
- Objects.requireNonNull(method, "Method must not be null value");
- this.method = method;
- return this;
- }
-
- public Builder setHeader(final String name, final String value) {
- Objects.requireNonNull(name, "Header name must not be null value");
- Objects.requireNonNull(value, "Header value must not be null value");
- this.headers.put(name, value);
- return this;
- }
-
- public Builder setHeaders(final Map headers) {
- Objects.requireNonNull(headers, "Headers must not be null value");
- this.headers.putAll(headers);
- return this;
- }
-
- public Builder setCookie(final String name, final String value) {
- Objects.requireNonNull(name, "Cookie name must not be null value");
- Objects.requireNonNull(value, "Cookie value must not be null value");
- this.cookies.put(name, value);
- return this;
- }
-
- public Builder setCookies(final Map cookies) {
- Objects.requireNonNull(cookies, "Cookies must not be null value");
- this.cookies.putAll(cookies);
- return this;
- }
-
- public Builder setBody(final String body) {
- Objects.requireNonNull(body, "Body should not be null value");
- this.body = body;
- return this;
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withMethod(final String method) {
- return setMethod(method);
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withHeader(final String name, final String value) {
- return setHeader(name, value);
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withHeaders(final Map headers) {
- return setHeaders(headers);
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withCookie(final String name, final String value) {
- return setCookie(name, value);
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withCookies(final Map cookies) {
- return setCookies(cookies);
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withBody(final String body) {
- return setBody(body);
- }
-
- public HttpRequestAttachment build() {
- return new HttpRequestAttachment(name, url, method, body, getCurl(), headers, cookies);
- }
-
- private String getCurl() {
- final StringBuilder builder = new StringBuilder("curl -v");
- if (Objects.nonNull(method)) {
- builder.append(" -X ").append(method);
- }
- builder.append(" '").append(url).append('\'');
- headers.forEach((key, value) -> appendHeader(builder, key, value));
- cookies.forEach((key, value) -> appendCookie(builder, key, value));
-
- if (Objects.nonNull(body)) {
- builder.append(" -d '").append(body).append('\'');
- }
- return builder.toString();
- }
-
- private static void appendHeader(final StringBuilder builder, final String key, final String value) {
- builder.append(" -H '")
- .append(key)
- .append(": ")
- .append(value)
- .append('\'');
- }
-
- private static void appendCookie(final StringBuilder builder, final String key, final String value) {
- builder.append(" -b '")
- .append(key)
- .append('=')
- .append(value)
- .append('\'');
- }
- }
-}
diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpResponseAttachment.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpResponseAttachment.java
deleted file mode 100644
index 0b5ef9841..000000000
--- a/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpResponseAttachment.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * Copyright 2019 Qameta Software OÜ
- *
- * 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.attachment.http;
-
-import io.qameta.allure.attachment.AttachmentData;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-
-/**
- * @author charlie (Dmitry Baev).
- */
-public class HttpResponseAttachment implements AttachmentData {
-
- private final String name;
-
- private final String url;
-
- private final String body;
-
- private final int responseCode;
-
- private final Map headers;
-
- private final Map cookies;
-
- public HttpResponseAttachment(final String name, final String url,
- final String body, final int responseCode,
- final Map headers, final Map cookies) {
- this.name = name;
- this.url = url;
- this.body = body;
- this.responseCode = responseCode;
- this.headers = headers;
- this.cookies = cookies;
- }
-
- @Override
- public String getName() {
- return name;
- }
-
- public String getUrl() {
- return url;
- }
-
- public String getBody() {
- return body;
- }
-
- public int getResponseCode() {
- return responseCode;
- }
-
- public Map getHeaders() {
- return headers;
- }
-
- public Map getCookies() {
- return cookies;
- }
-
- /**
- * Builder for HttpRequestAttachment.
- */
- @SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
- public static final class Builder {
-
- private final String name;
-
- private String url;
-
- private int responseCode;
-
- private String body;
-
- private final Map headers = new HashMap<>();
-
- private final Map cookies = new HashMap<>();
-
- private Builder(final String name) {
- Objects.requireNonNull(name, "Name must not be null value");
- this.name = name;
- }
-
- public static Builder create(final String attachmentName) {
- return new Builder(attachmentName);
- }
-
- public Builder setUrl(final String url) {
- Objects.requireNonNull(url, "Url must not be null value");
- this.url = url;
- return this;
- }
-
- public Builder setResponseCode(final int responseCode) {
- this.responseCode = responseCode;
- return this;
- }
-
- public Builder setHeader(final String name, final String value) {
- Objects.requireNonNull(name, "Header name must not be null value");
- Objects.requireNonNull(value, "Header value must not be null value");
- this.headers.put(name, value);
- return this;
- }
-
- public Builder setHeaders(final Map headers) {
- Objects.requireNonNull(headers, "Headers must not be null value");
- this.headers.putAll(headers);
- return this;
- }
-
- public Builder setCookie(final String name, final String value) {
- Objects.requireNonNull(name, "Cookie name must not be null value");
- Objects.requireNonNull(value, "Cookie value must not be null value");
- this.cookies.put(name, value);
- return this;
- }
-
- public Builder setCookies(final Map cookies) {
- Objects.requireNonNull(cookies, "Cookies must not be null value");
- this.cookies.putAll(cookies);
- return this;
- }
-
- public Builder setBody(final String body) {
- Objects.requireNonNull(body, "Body should not be null value");
- this.body = body;
- return this;
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withUrl(final String url) {
- return setUrl(url);
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withResponseCode(final int responseCode) {
- return setResponseCode(responseCode);
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withHeader(final String name, final String value) {
- return setHeader(name, value);
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withHeaders(final Map headers) {
- return setHeaders(headers);
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withCookie(final String name, final String value) {
- return setCookie(name, value);
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withCookies(final Map cookies) {
- return setCookies(cookies);
- }
-
- /**
- * Use setter method instead.
- * @deprecated scheduled for removal in 3.0 release
- */
- @Deprecated
- public Builder withBody(final String body) {
- return setBody(body);
- }
-
- public HttpResponseAttachment build() {
- return new HttpResponseAttachment(name, url, body, responseCode, headers, cookies);
- }
- }
-}
diff --git a/allure-attachments/src/main/resources/tpl/http-request.ftl b/allure-attachments/src/main/resources/tpl/http-request.ftl
deleted file mode 100644
index 5ef76b521..000000000
--- a/allure-attachments/src/main/resources/tpl/http-request.ftl
+++ /dev/null
@@ -1,38 +0,0 @@
-<#ftl output_format="HTML">
-<#-- @ftlvariable name="data" type="io.qameta.allure.attachment.http.HttpRequestAttachment" -->
-<#if data.method??>${data.method}<#else>GET#if> to <#if data.url??>${data.url}<#else>Unknown#if>
-
-<#if data.body??>
-Body
-
-#if>
-
-<#if (data.headers)?has_content>
-Headers
-
- <#list data.headers as name, value>
-
${name}: ${value}
- #list>
-
-#if>
-
-
-<#if (data.cookies)?has_content>
-Cookies
-
- <#list data.cookies as name, value>
-
${name}: ${value}
- #list>
-
-#if>
-
-<#if data.curl??>
-Curl
-
-${data.curl}
-
-#if>
diff --git a/allure-attachments/src/main/resources/tpl/http-response.ftl b/allure-attachments/src/main/resources/tpl/http-response.ftl
deleted file mode 100644
index 2bc55de9f..000000000
--- a/allure-attachments/src/main/resources/tpl/http-response.ftl
+++ /dev/null
@@ -1,32 +0,0 @@
-<#ftl output_format="HTML">
-<#-- @ftlvariable name="data" type="io.qameta.allure.attachment.http.HttpResponseAttachment" -->
-Status code <#if data.responseCode??>${data.responseCode} <#else>Unknown#if>
-<#if data.url??>${data.url}
#if>
-
-<#if data.body??>
-Body
-
-#if>
-
-<#if (data.headers)?has_content>
-Headers
-
- <#list data.headers as name, value>
-
${name}: ${value}
- #list>
-
-#if>
-
-
-<#if (data.cookies)?has_content>
-Cookies
-
- <#list data.cookies as name, value>
-
${name}: ${value}
- #list>
-
-#if>
diff --git a/allure-attachments/src/test/java/io/qameta/allure/attachment/DefaultAttachmentProcessorTest.java b/allure-attachments/src/test/java/io/qameta/allure/attachment/DefaultAttachmentProcessorTest.java
deleted file mode 100644
index 1283d6116..000000000
--- a/allure-attachments/src/test/java/io/qameta/allure/attachment/DefaultAttachmentProcessorTest.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2019 Qameta Software OÜ
- *
- * 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.attachment;
-
-import io.qameta.allure.AllureLifecycle;
-import io.qameta.allure.attachment.http.HttpRequestAttachment;
-import io.qameta.allure.test.AllureFeatures;
-import org.junit.jupiter.api.Test;
-
-import java.nio.charset.StandardCharsets;
-
-import static io.qameta.allure.attachment.testdata.TestData.randomAttachmentContent;
-import static io.qameta.allure.attachment.testdata.TestData.randomHttpRequestAttachment;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-/**
- * @author charlie (Dmitry Baev).
- */
-class DefaultAttachmentProcessorTest {
-
- @SuppressWarnings("unchecked")
- @AllureFeatures.Attachments
- @Test
- void shouldProcessAttachments() {
- final HttpRequestAttachment attachment = randomHttpRequestAttachment();
- final AllureLifecycle lifecycle = mock(AllureLifecycle.class);
- final AttachmentRenderer renderer = mock(AttachmentRenderer.class);
- final AttachmentContent content = randomAttachmentContent();
- doReturn(content)
- .when(renderer)
- .render(attachment);
-
- new DefaultAttachmentProcessor(lifecycle)
- .addAttachment(attachment, renderer);
-
- verify(renderer, times(1)).render(attachment);
- verify(lifecycle, times(1))
- .addAttachment(
- eq(attachment.getName()),
- eq(content.getContentType()),
- eq(content.getFileExtension()),
- eq(content.getContent().getBytes(StandardCharsets.UTF_8))
- );
- }
-}
diff --git a/allure-attachments/src/test/java/io/qameta/allure/attachment/FreemarkerAttachmentRendererTest.java b/allure-attachments/src/test/java/io/qameta/allure/attachment/FreemarkerAttachmentRendererTest.java
deleted file mode 100644
index 75ddac32d..000000000
--- a/allure-attachments/src/test/java/io/qameta/allure/attachment/FreemarkerAttachmentRendererTest.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2019 Qameta Software OÜ
- *
- * 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.attachment;
-
-import io.qameta.allure.attachment.http.HttpRequestAttachment;
-import io.qameta.allure.test.AllureFeatures;
-import org.junit.jupiter.api.Test;
-
-import static io.qameta.allure.attachment.testdata.TestData.randomHttpRequestAttachment;
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * @author charlie (Dmitry Baev).
- */
-class FreemarkerAttachmentRendererTest {
-
- @AllureFeatures.Attachments
- @Test
- void shouldRenderRequestAttachment() {
- final HttpRequestAttachment data = randomHttpRequestAttachment();
- final DefaultAttachmentContent content = new FreemarkerAttachmentRenderer("http-request.ftl")
- .render(data);
-
- assertThat(content)
- .hasFieldOrPropertyWithValue("contentType", "text/html")
- .hasFieldOrPropertyWithValue("fileExtension", ".html")
- .hasFieldOrProperty("content");
- }
-
- @AllureFeatures.Attachments
- @Test
- void shouldRenderResponseAttachment() {
- final HttpRequestAttachment data = randomHttpRequestAttachment();
- final DefaultAttachmentContent content = new FreemarkerAttachmentRenderer("http-response.ftl")
- .render(data);
-
- assertThat(content)
- .hasFieldOrPropertyWithValue("contentType", "text/html")
- .hasFieldOrPropertyWithValue("fileExtension", ".html")
- .hasFieldOrProperty("content");
- }
-}
diff --git a/allure-attachments/src/test/java/io/qameta/allure/attachment/testdata/TestData.java b/allure-attachments/src/test/java/io/qameta/allure/attachment/testdata/TestData.java
deleted file mode 100644
index 26eba1c6f..000000000
--- a/allure-attachments/src/test/java/io/qameta/allure/attachment/testdata/TestData.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2019 Qameta Software OÜ
- *
- * 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.attachment.testdata;
-
-import io.qameta.allure.attachment.AttachmentContent;
-import io.qameta.allure.attachment.DefaultAttachmentContent;
-import io.qameta.allure.attachment.http.HttpRequestAttachment;
-import io.qameta.allure.attachment.http.HttpResponseAttachment;
-import org.apache.commons.lang3.RandomStringUtils;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ThreadLocalRandom;
-
-/**
- * @author charlie (Dmitry Baev).
- */
-public final class TestData {
-
- private TestData() {
- throw new IllegalStateException("Do not instance");
- }
-
- public static String randomString() {
- return RandomStringUtils.randomAlphabetic(10);
- }
-
- public static HttpRequestAttachment randomHttpRequestAttachment() {
- return new HttpRequestAttachment(
- randomString(),
- randomString(),
- randomString(),
- randomString(),
- randomString(),
- randomMap(),
- randomMap()
- );
- }
-
- public static HttpResponseAttachment randomHttpResponseAttachment() {
- return new HttpResponseAttachment(
- randomString(),
- randomString(),
- randomString(),
- ThreadLocalRandom.current().nextInt(),
- randomMap(),
- randomMap()
- );
- }
-
- public static AttachmentContent randomAttachmentContent() {
- return new DefaultAttachmentContent(randomString(), randomString(), randomString());
- }
-
- public static Map randomMap() {
- final Map map = new HashMap<>();
- map.put(randomString(), randomString());
- map.put(randomString(), randomString());
- return map;
- }
-}
diff --git a/allure-awaitility/README.md b/allure-awaitility/README.md
new file mode 100644
index 000000000..51c1f72f8
--- /dev/null
+++ b/allure-awaitility/README.md
@@ -0,0 +1,54 @@
+# allure-awaitility
+
+Awaitility condition listener integration for Allure Java.
+
+Use this module when your tests wait with Awaitility and you want polling attempts, timeouts, ignored exceptions, and successful waits to be visible in Allure Report.
+
+## Supported Versions
+
+- Allure Java 3.x requires Java 17 or newer.
+- This module targets Awaitility 4.x.
+- The current build validates against Awaitility 4.3.0.
+
+## Installation
+
+Gradle:
+
+```kotlin
+dependencies {
+ testImplementation(platform("io.qameta.allure:allure-bom:"))
+ testImplementation("io.qameta.allure:allure-awaitility")
+}
+```
+
+Maven, with `allure-bom` imported in dependency management:
+
+```xml
+
+ io.qameta.allure
+ allure-awaitility
+ test
+
+```
+
+## Setup
+
+Register `io.qameta.allure.awaitility.AllureAwaitilityListener` as an Awaitility condition listener.
+
+```java
+await()
+ .conditionEvaluationListener(new AllureAwaitilityListener())
+ .untilAsserted(() -> assertThat(service.isReady()).isTrue());
+```
+
+You can also set it as the default listener for your test suite:
+
+```java
+Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener());
+```
+
+## Report Output
+
+- Await start, poll, satisfaction, timeout, and exception events.
+- Poll attempts as nested Allure steps.
+- Timing information rendered through `TemporalDuration`.
diff --git a/allure-cucumber3-jvm/build.gradle.kts b/allure-awaitility/build.gradle.kts
similarity index 56%
rename from allure-cucumber3-jvm/build.gradle.kts
rename to allure-awaitility/build.gradle.kts
index 681f1ab7c..b48443834 100644
--- a/allure-cucumber3-jvm/build.gradle.kts
+++ b/allure-awaitility/build.gradle.kts
@@ -1,35 +1,33 @@
-description = "Allure CucumberJVM 3.0 Integration"
+description = "Allure Awaitlity Integration"
val agent: Configuration by configurations.creating
-val cucumberVersion = "3.0.2"
+val awaitilityVersion = "4.3.0"
dependencies {
agent("org.aspectj:aspectjweaver")
api(project(":allure-java-commons"))
- implementation("io.cucumber:cucumber-core:$cucumberVersion")
- implementation("io.cucumber:cucumber-java:$cucumberVersion")
- testImplementation("commons-io:commons-io")
- testImplementation("io.github.glytching:junit-extensions")
+ compileOnly("org.awaitility:awaitility:$awaitilityVersion")
+ testImplementation("jakarta.annotation:jakarta.annotation-api")
testImplementation("org.assertj:assertj-core")
+ testImplementation(project(":allure-assertj"))
+ testImplementation("org.awaitility:awaitility:$awaitilityVersion")
testImplementation("org.junit.jupiter:junit-jupiter-api")
testImplementation("org.slf4j:slf4j-simple")
testImplementation(project(":allure-java-commons-test"))
testImplementation(project(":allure-junit-platform"))
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
+ testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.jar {
manifest {
attributes(mapOf(
- "Automatic-Module-Name" to "io.qameta.allure.cucumber3jvm"
+ "Automatic-Module-Name" to "io.qameta.allure.awaitility"
))
}
}
tasks.test {
useJUnitPlatform()
- doFirst {
- jvmArgs("-javaagent:${agent.singleFile}")
- }
}
diff --git a/allure-awaitility/src/main/java/io/qameta/allure/awaitility/AllureAwaitilityListener.java b/allure-awaitility/src/main/java/io/qameta/allure/awaitility/AllureAwaitilityListener.java
new file mode 100644
index 000000000..d3afd8afb
--- /dev/null
+++ b/allure-awaitility/src/main/java/io/qameta/allure/awaitility/AllureAwaitilityListener.java
@@ -0,0 +1,325 @@
+/*
+ * 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.awaitility;
+
+import io.qameta.allure.Allure;
+import io.qameta.allure.AllureExternalKey;
+import io.qameta.allure.AllureLifecycle;
+import io.qameta.allure.AllureThreadBinding;
+import io.qameta.allure.AttachmentOptions;
+import io.qameta.allure.model.Status;
+import io.qameta.allure.model.StepResult;
+import org.awaitility.Awaitility;
+import org.awaitility.core.ConditionEvaluationListener;
+import org.awaitility.core.ConditionFactory;
+import org.awaitility.core.EvaluatedCondition;
+import org.awaitility.core.IgnoredException;
+import org.awaitility.core.StartEvaluationEvent;
+import org.awaitility.core.TimeoutEvent;
+
+import java.io.ByteArrayInputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+/**
+ *
+ * Class implements Awaitility condition listener to log all awaiting and polls as {@link io.qameta.allure.Step}.
+ *
+ *
+ * Usage with single condition
+ *
+ *
+ * Awaitility.await()
+ * .conditionEvaluationListener(new AllureAwaitilityListener())
+ * .until(() -> somethingHappen());
+ *
+ *
+ *