Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions allure-spock2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Use this module when your specifications run on Spock 2 and you want specificati
## Supported Versions

- Allure Java 3.x requires Java 17 or newer.
- This module targets Spock 2.
- The current build validates against Spock 2.4 for Groovy 5.0 and Groovy 5.0.6.
- This module targets Spock 2.4, whose block-listener API is used for block reporting.
- The current build validates against Spock 2.4 for Groovy 5.0 and Groovy 5.0.7.

## Installation

Expand All @@ -35,8 +35,11 @@ Maven, with `allure-bom` imported in dependency management:

Add the dependency to a Spock 2 project. The module registers `io.qameta.allure.spock2.AllureSpock2` as a Spock global extension through service loader metadata.

Block reporting uses Spock runtime events and does not install a Groovy AST transformation, so the order of the Spock and Allure dependencies does not affect compilation.

## Report Output

- Specifications, features, iterations, fixture methods, and errors.
- Executed `given`/`setup`, `expect`, `when`, `then`, and `cleanup` blocks, with iteration variables resolved in block descriptions. Spock does not emit execution events for `where` and `filter` blocks.
- Data-driven parameters and Spock tags.
- Labels, links, JavaDoc descriptions, test-plan filtering, and fixture metadata.
188 changes: 182 additions & 6 deletions allure-spock2/src/main/java/io/qameta/allure/spock2/AllureSpock2.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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.testfilter.FileTestPlanSupplier;
import io.qameta.allure.testfilter.TestPlan;
Expand All @@ -33,23 +34,30 @@
import io.qameta.allure.util.ExceptionUtils;
import io.qameta.allure.util.ResultsUtils;
import org.spockframework.runtime.AbstractRunListener;
import org.spockframework.runtime.extension.IBlockListener;
import org.spockframework.runtime.extension.IGlobalExtension;
import org.spockframework.runtime.extension.IMethodInterceptor;
import org.spockframework.runtime.extension.IMethodInvocation;
import org.spockframework.runtime.extension.builtin.UnrollIterationNameProvider;
import org.spockframework.runtime.model.BlockInfo;
import org.spockframework.runtime.model.BlockKind;
import org.spockframework.runtime.model.ErrorInfo;
import org.spockframework.runtime.model.FeatureInfo;
import org.spockframework.runtime.model.IterationInfo;
import org.spockframework.runtime.model.MethodInfo;
import org.spockframework.runtime.model.MethodKind;
import org.spockframework.runtime.model.SpecInfo;
import org.spockframework.runtime.model.TestTag;
import spock.lang.Specification;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -81,10 +89,17 @@
*
* <p>Register this extension with Spock to convert specification, feature, iteration, fixture, and error events into Allure results. The constructor accepting a test plan enables Allure test plan filtering before execution.</p>
*/
public class AllureSpock2 extends AbstractRunListener implements IGlobalExtension {
@SuppressWarnings("PMD.GodClass")
public class AllureSpock2 extends AbstractRunListener implements IGlobalExtension, IBlockListener {

private static final String BLOCK = "block";

private final ThreadLocal<String> testResults = new ThreadLocal<>();

private final ThreadLocal<Map<BlockInfo, BlockStep>> blockSteps = new ThreadLocal<>();

private final ThreadLocal<BlockStep> activeBlockStep = new ThreadLocal<>();

private final AllureLifecycle lifecycle;

private final TestPlan testPlan;
Expand Down Expand Up @@ -122,7 +137,10 @@ public AllureSpock2(final AllureLifecycle lifecycle, final TestPlan plan) {
*/
@Override
public void visitSpec(final SpecInfo spec) {
spec.getAllFeatures().forEach(methodInfo -> methodInfo.setSkipped(this.isSkipped(methodInfo)));
spec.getAllFeatures().forEach(featureInfo -> {
featureInfo.setSkipped(this.isSkipped(featureInfo));
featureInfo.addBlockListener(this);
});

spec.addListener(this);

Expand All @@ -138,14 +156,14 @@ public void visitSpec(final SpecInfo spec) {
}
});

// add each feature to this container
// Spec fixtures execute once but apply to every feature in the spec.
spec.getAllFeatures().stream()
.map(FeatureInfo::getFeatureMethod)
.filter(Objects::nonNull)
.forEach(fm -> fm.addInterceptor(i -> {
getLifecycle().getCurrentExecutableKey().ifPresent(key -> {
getLifecycle().addTestToScope(scopeKey(specContainerUuid), key);
});
getLifecycle().getCurrentExecutableKey().ifPresent(
key -> getLifecycle().addTestToScope(scopeKey(specContainerUuid), key)
);
i.proceed();
}));
}
Expand Down Expand Up @@ -261,6 +279,8 @@ public void beforeIteration(final IterationInfo iteration) {
getLifecycle().scheduleTest(testKey, result);
getLifecycle().addDefaultLabels(testKey, defaultLabels);
getLifecycle().startTest(testKey);
blockSteps.set(new IdentityHashMap<>());
activeBlockStep.remove();

}

Expand Down Expand Up @@ -318,6 +338,7 @@ public void error(final ErrorInfo error) {
return;
}
final Throwable exception = error.getException();
updateBlockStep(error);
getLifecycle().updateTest(testKey(uuid), testResult -> {
testResult.setStatus(getStatus(exception).orElse(null));

Expand Down Expand Up @@ -347,10 +368,12 @@ public void afterIteration(final IterationInfo iteration) {
final String uuid = testResults.get();
if (Objects.isNull(uuid)) {
testResults.remove();
clearBlockState();
return;
}

try {
clearBlockState();
final AllureExternalKey testKey = testKey(uuid);
getLifecycle().updateTest(testKey, testResult -> {
if (Objects.isNull(testResult.getStatus())) {
Expand All @@ -364,6 +387,147 @@ public void afterIteration(final IterationInfo iteration) {
}
}

/**
* {@inheritDoc}
*/
@Override
public <S extends Specification> void blockEntered(final S specificationInstance, final BlockInfo blockInfo) {
final Map<BlockInfo, BlockStep> steps = blockSteps.get();
if (Objects.isNull(testResults.get()) || Objects.isNull(steps)) {
return;
}

stopActiveBlockStep();

final IterationInfo iteration = specificationInstance.getSpecificationContext().getCurrentIteration();
if (Objects.isNull(iteration)) {
return;
}

final StepResult result = new StepResult()
.setName(getBlockName(blockInfo, iteration));
final BlockStep blockStep = new BlockStep(blockStepKey(), result);

lifecycle.startStep(blockStep.key, result);
steps.put(blockInfo, blockStep);
activeBlockStep.set(blockStep);
}

/**
* {@inheritDoc}
*/
@Override
public <S extends Specification> void blockExited(final S specificationInstance, final BlockInfo blockInfo) {
final Map<BlockInfo, BlockStep> steps = blockSteps.get();
if (Objects.isNull(steps)) {
return;
}

final BlockStep blockStep = steps.get(blockInfo);
if (Objects.isNull(blockStep)) {
return;
}

blockStep.result.setStatus(Status.PASSED);
if (Objects.equals(blockStep, activeBlockStep.get())) {
lifecycle.updateStep(blockStep.key, step -> step.setStatus(Status.PASSED));
stopActiveBlockStep();
}
}

private void updateBlockStep(final ErrorInfo error) {
if (Objects.isNull(error.getErrorContext()) || Objects.isNull(error.getErrorContext().getBlock())) {
return;
}

final Map<BlockInfo, BlockStep> steps = blockSteps.get();
if (Objects.isNull(steps)) {
return;
}

final BlockStep blockStep = steps.get(error.getErrorContext().getBlock());
if (Objects.isNull(blockStep)) {
return;
}

final Throwable exception = error.getException();
final Status status = getStatus(exception).orElse(Status.BROKEN);
final StatusDetails details = getStatusDetails(exception).orElse(null);
blockStep.result
.setStatus(status)
.setStatusDetails(details);

if (Objects.equals(blockStep, activeBlockStep.get())) {
lifecycle.updateStep(
blockStep.key,
step -> step
.setStatus(status)
.setStatusDetails(details)
);
stopActiveBlockStep();
}
}

private void clearBlockState() {
try {
stopActiveBlockStep();
} finally {
blockSteps.remove();
activeBlockStep.remove();
}
}

private String getBlockName(final BlockInfo blockInfo, final IterationInfo iteration) {
final String kind = BlockKind.SETUP.equals(blockInfo.getKind())
? "given"
: Optional.ofNullable(blockInfo.getKind())
.map(Enum::name)
.map(name -> name.toLowerCase(Locale.ENGLISH))
.orElse(BLOCK);
final List<String> texts = blockInfo.getTexts();
if (Objects.isNull(texts) || texts.isEmpty()) {
return kind;
}

final String description = texts.stream()
.filter(Objects::nonNull)
.map(text -> resolveBlockText(iteration, text))
.collect(Collectors.joining(", "));
return description.isEmpty() ? kind : kind + ": " + description;
}

private String resolveBlockText(final IterationInfo iteration, final String text) {
try {
return new UnrollIterationNameProvider(iteration.getFeature(), text, false).getName(iteration);
} catch (RuntimeException ignored) {
return text;
}
}

private void stopActiveBlockStep() {
final BlockStep blockStep = activeBlockStep.get();
if (Objects.isNull(blockStep)) {
return;
}

final boolean isCurrent = lifecycle.getCurrentExecutableKey()
.filter(blockStep.key::equals)
.isPresent();
if (isCurrent) {
lifecycle.stopStep();
} else {
lifecycle.stopStep(blockStep.key);
Optional.ofNullable(testResults.get())
.map(this::testKey)
.ifPresent(lifecycle::setCurrent);
}
activeBlockStep.remove();
}

private AllureExternalKey blockStepKey() {
return AllureExternalKey.of(AllureSpock2.class, BLOCK, UUID.randomUUID().toString());
}

private List<Parameter> getParameters(final List<String> names, final Object... values) {
return IntStream.range(0, Math.min(names.size(), values.length))
.mapToObj(index -> createParameter(names.get(index), values[index]))
Expand Down Expand Up @@ -391,6 +555,18 @@ private AllureExternalKey fixtureKey(final String uuid) {
return AllureExternalKey.of(AllureSpock2.class, "fixture", uuid);
}

private static final class BlockStep {

private final AllureExternalKey key;

private final StepResult result;

private BlockStep(final AllureExternalKey key, final StepResult result) {
this.key = key;
this.result = result;
}
}

/**
* IMethodInterceptor that reports feature fixture methods.
* Creates container and fixture result for feature fixtures. Such fixtures (setup/cleanup) are
Expand Down
Loading
Loading