Skip to content

Commit 368e014

Browse files
authored
improve spock block and fixture reporting (fixes #965, fixes #967, via #1339)
1 parent 73549dc commit 368e014

7 files changed

Lines changed: 361 additions & 235 deletions

File tree

allure-spock2/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ Use this module when your specifications run on Spock 2 and you want specificati
77
## Supported Versions
88

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

1313
## Installation
1414

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

3636
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.
3737

38+
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.
39+
3840
## Report Output
3941

4042
- Specifications, features, iterations, fixture methods, and errors.
43+
- 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.
4144
- Data-driven parameters and Spock tags.
4245
- Labels, links, JavaDoc descriptions, test-plan filtering, and fixture metadata.

allure-spock2/src/main/java/io/qameta/allure/spock2/AllureSpock2.java

Lines changed: 182 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.qameta.allure.model.Parameter;
2626
import io.qameta.allure.model.Status;
2727
import io.qameta.allure.model.StatusDetails;
28+
import io.qameta.allure.model.StepResult;
2829
import io.qameta.allure.model.TestResult;
2930
import io.qameta.allure.testfilter.FileTestPlanSupplier;
3031
import io.qameta.allure.testfilter.TestPlan;
@@ -33,23 +34,30 @@
3334
import io.qameta.allure.util.ExceptionUtils;
3435
import io.qameta.allure.util.ResultsUtils;
3536
import org.spockframework.runtime.AbstractRunListener;
37+
import org.spockframework.runtime.extension.IBlockListener;
3638
import org.spockframework.runtime.extension.IGlobalExtension;
3739
import org.spockframework.runtime.extension.IMethodInterceptor;
3840
import org.spockframework.runtime.extension.IMethodInvocation;
41+
import org.spockframework.runtime.extension.builtin.UnrollIterationNameProvider;
42+
import org.spockframework.runtime.model.BlockInfo;
43+
import org.spockframework.runtime.model.BlockKind;
3944
import org.spockframework.runtime.model.ErrorInfo;
4045
import org.spockframework.runtime.model.FeatureInfo;
4146
import org.spockframework.runtime.model.IterationInfo;
4247
import org.spockframework.runtime.model.MethodInfo;
4348
import org.spockframework.runtime.model.MethodKind;
4449
import org.spockframework.runtime.model.SpecInfo;
4550
import org.spockframework.runtime.model.TestTag;
51+
import spock.lang.Specification;
4652

4753
import java.lang.reflect.Method;
4854
import java.util.ArrayList;
4955
import java.util.Arrays;
5056
import java.util.Collections;
57+
import java.util.IdentityHashMap;
5158
import java.util.List;
5259
import java.util.Locale;
60+
import java.util.Map;
5361
import java.util.Objects;
5462
import java.util.Optional;
5563
import java.util.Set;
@@ -81,10 +89,17 @@
8189
*
8290
* <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>
8391
*/
84-
public class AllureSpock2 extends AbstractRunListener implements IGlobalExtension {
92+
@SuppressWarnings("PMD.GodClass")
93+
public class AllureSpock2 extends AbstractRunListener implements IGlobalExtension, IBlockListener {
94+
95+
private static final String BLOCK = "block";
8596

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

99+
private final ThreadLocal<Map<BlockInfo, BlockStep>> blockSteps = new ThreadLocal<>();
100+
101+
private final ThreadLocal<BlockStep> activeBlockStep = new ThreadLocal<>();
102+
88103
private final AllureLifecycle lifecycle;
89104

90105
private final TestPlan testPlan;
@@ -122,7 +137,10 @@ public AllureSpock2(final AllureLifecycle lifecycle, final TestPlan plan) {
122137
*/
123138
@Override
124139
public void visitSpec(final SpecInfo spec) {
125-
spec.getAllFeatures().forEach(methodInfo -> methodInfo.setSkipped(this.isSkipped(methodInfo)));
140+
spec.getAllFeatures().forEach(featureInfo -> {
141+
featureInfo.setSkipped(this.isSkipped(featureInfo));
142+
featureInfo.addBlockListener(this);
143+
});
126144

127145
spec.addListener(this);
128146

@@ -138,14 +156,14 @@ public void visitSpec(final SpecInfo spec) {
138156
}
139157
});
140158

141-
// add each feature to this container
159+
// Spec fixtures execute once but apply to every feature in the spec.
142160
spec.getAllFeatures().stream()
143161
.map(FeatureInfo::getFeatureMethod)
144162
.filter(Objects::nonNull)
145163
.forEach(fm -> fm.addInterceptor(i -> {
146-
getLifecycle().getCurrentExecutableKey().ifPresent(key -> {
147-
getLifecycle().addTestToScope(scopeKey(specContainerUuid), key);
148-
});
164+
getLifecycle().getCurrentExecutableKey().ifPresent(
165+
key -> getLifecycle().addTestToScope(scopeKey(specContainerUuid), key)
166+
);
149167
i.proceed();
150168
}));
151169
}
@@ -261,6 +279,8 @@ public void beforeIteration(final IterationInfo iteration) {
261279
getLifecycle().scheduleTest(testKey, result);
262280
getLifecycle().addDefaultLabels(testKey, defaultLabels);
263281
getLifecycle().startTest(testKey);
282+
blockSteps.set(new IdentityHashMap<>());
283+
activeBlockStep.remove();
264284

265285
}
266286

@@ -318,6 +338,7 @@ public void error(final ErrorInfo error) {
318338
return;
319339
}
320340
final Throwable exception = error.getException();
341+
updateBlockStep(error);
321342
getLifecycle().updateTest(testKey(uuid), testResult -> {
322343
testResult.setStatus(getStatus(exception).orElse(null));
323344

@@ -347,10 +368,12 @@ public void afterIteration(final IterationInfo iteration) {
347368
final String uuid = testResults.get();
348369
if (Objects.isNull(uuid)) {
349370
testResults.remove();
371+
clearBlockState();
350372
return;
351373
}
352374

353375
try {
376+
clearBlockState();
354377
final AllureExternalKey testKey = testKey(uuid);
355378
getLifecycle().updateTest(testKey, testResult -> {
356379
if (Objects.isNull(testResult.getStatus())) {
@@ -364,6 +387,147 @@ public void afterIteration(final IterationInfo iteration) {
364387
}
365388
}
366389

390+
/**
391+
* {@inheritDoc}
392+
*/
393+
@Override
394+
public <S extends Specification> void blockEntered(final S specificationInstance, final BlockInfo blockInfo) {
395+
final Map<BlockInfo, BlockStep> steps = blockSteps.get();
396+
if (Objects.isNull(testResults.get()) || Objects.isNull(steps)) {
397+
return;
398+
}
399+
400+
stopActiveBlockStep();
401+
402+
final IterationInfo iteration = specificationInstance.getSpecificationContext().getCurrentIteration();
403+
if (Objects.isNull(iteration)) {
404+
return;
405+
}
406+
407+
final StepResult result = new StepResult()
408+
.setName(getBlockName(blockInfo, iteration));
409+
final BlockStep blockStep = new BlockStep(blockStepKey(), result);
410+
411+
lifecycle.startStep(blockStep.key, result);
412+
steps.put(blockInfo, blockStep);
413+
activeBlockStep.set(blockStep);
414+
}
415+
416+
/**
417+
* {@inheritDoc}
418+
*/
419+
@Override
420+
public <S extends Specification> void blockExited(final S specificationInstance, final BlockInfo blockInfo) {
421+
final Map<BlockInfo, BlockStep> steps = blockSteps.get();
422+
if (Objects.isNull(steps)) {
423+
return;
424+
}
425+
426+
final BlockStep blockStep = steps.get(blockInfo);
427+
if (Objects.isNull(blockStep)) {
428+
return;
429+
}
430+
431+
blockStep.result.setStatus(Status.PASSED);
432+
if (Objects.equals(blockStep, activeBlockStep.get())) {
433+
lifecycle.updateStep(blockStep.key, step -> step.setStatus(Status.PASSED));
434+
stopActiveBlockStep();
435+
}
436+
}
437+
438+
private void updateBlockStep(final ErrorInfo error) {
439+
if (Objects.isNull(error.getErrorContext()) || Objects.isNull(error.getErrorContext().getBlock())) {
440+
return;
441+
}
442+
443+
final Map<BlockInfo, BlockStep> steps = blockSteps.get();
444+
if (Objects.isNull(steps)) {
445+
return;
446+
}
447+
448+
final BlockStep blockStep = steps.get(error.getErrorContext().getBlock());
449+
if (Objects.isNull(blockStep)) {
450+
return;
451+
}
452+
453+
final Throwable exception = error.getException();
454+
final Status status = getStatus(exception).orElse(Status.BROKEN);
455+
final StatusDetails details = getStatusDetails(exception).orElse(null);
456+
blockStep.result
457+
.setStatus(status)
458+
.setStatusDetails(details);
459+
460+
if (Objects.equals(blockStep, activeBlockStep.get())) {
461+
lifecycle.updateStep(
462+
blockStep.key,
463+
step -> step
464+
.setStatus(status)
465+
.setStatusDetails(details)
466+
);
467+
stopActiveBlockStep();
468+
}
469+
}
470+
471+
private void clearBlockState() {
472+
try {
473+
stopActiveBlockStep();
474+
} finally {
475+
blockSteps.remove();
476+
activeBlockStep.remove();
477+
}
478+
}
479+
480+
private String getBlockName(final BlockInfo blockInfo, final IterationInfo iteration) {
481+
final String kind = BlockKind.SETUP.equals(blockInfo.getKind())
482+
? "given"
483+
: Optional.ofNullable(blockInfo.getKind())
484+
.map(Enum::name)
485+
.map(name -> name.toLowerCase(Locale.ENGLISH))
486+
.orElse(BLOCK);
487+
final List<String> texts = blockInfo.getTexts();
488+
if (Objects.isNull(texts) || texts.isEmpty()) {
489+
return kind;
490+
}
491+
492+
final String description = texts.stream()
493+
.filter(Objects::nonNull)
494+
.map(text -> resolveBlockText(iteration, text))
495+
.collect(Collectors.joining(", "));
496+
return description.isEmpty() ? kind : kind + ": " + description;
497+
}
498+
499+
private String resolveBlockText(final IterationInfo iteration, final String text) {
500+
try {
501+
return new UnrollIterationNameProvider(iteration.getFeature(), text, false).getName(iteration);
502+
} catch (RuntimeException ignored) {
503+
return text;
504+
}
505+
}
506+
507+
private void stopActiveBlockStep() {
508+
final BlockStep blockStep = activeBlockStep.get();
509+
if (Objects.isNull(blockStep)) {
510+
return;
511+
}
512+
513+
final boolean isCurrent = lifecycle.getCurrentExecutableKey()
514+
.filter(blockStep.key::equals)
515+
.isPresent();
516+
if (isCurrent) {
517+
lifecycle.stopStep();
518+
} else {
519+
lifecycle.stopStep(blockStep.key);
520+
Optional.ofNullable(testResults.get())
521+
.map(this::testKey)
522+
.ifPresent(lifecycle::setCurrent);
523+
}
524+
activeBlockStep.remove();
525+
}
526+
527+
private AllureExternalKey blockStepKey() {
528+
return AllureExternalKey.of(AllureSpock2.class, BLOCK, UUID.randomUUID().toString());
529+
}
530+
367531
private List<Parameter> getParameters(final List<String> names, final Object... values) {
368532
return IntStream.range(0, Math.min(names.size(), values.length))
369533
.mapToObj(index -> createParameter(names.get(index), values[index]))
@@ -391,6 +555,18 @@ private AllureExternalKey fixtureKey(final String uuid) {
391555
return AllureExternalKey.of(AllureSpock2.class, "fixture", uuid);
392556
}
393557

558+
private static final class BlockStep {
559+
560+
private final AllureExternalKey key;
561+
562+
private final StepResult result;
563+
564+
private BlockStep(final AllureExternalKey key, final StepResult result) {
565+
this.key = key;
566+
this.result = result;
567+
}
568+
}
569+
394570
/**
395571
* IMethodInterceptor that reports feature fixture methods.
396572
* Creates container and fixture result for feature fixtures. Such fixtures (setup/cleanup) are

0 commit comments

Comments
 (0)