2525import io .qameta .allure .model .Parameter ;
2626import io .qameta .allure .model .Status ;
2727import io .qameta .allure .model .StatusDetails ;
28+ import io .qameta .allure .model .StepResult ;
2829import io .qameta .allure .model .TestResult ;
2930import io .qameta .allure .testfilter .FileTestPlanSupplier ;
3031import io .qameta .allure .testfilter .TestPlan ;
3334import io .qameta .allure .util .ExceptionUtils ;
3435import io .qameta .allure .util .ResultsUtils ;
3536import org .spockframework .runtime .AbstractRunListener ;
37+ import org .spockframework .runtime .extension .IBlockListener ;
3638import org .spockframework .runtime .extension .IGlobalExtension ;
3739import org .spockframework .runtime .extension .IMethodInterceptor ;
3840import 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 ;
3944import org .spockframework .runtime .model .ErrorInfo ;
4045import org .spockframework .runtime .model .FeatureInfo ;
4146import org .spockframework .runtime .model .IterationInfo ;
4247import org .spockframework .runtime .model .MethodInfo ;
4348import org .spockframework .runtime .model .MethodKind ;
4449import org .spockframework .runtime .model .SpecInfo ;
4550import org .spockframework .runtime .model .TestTag ;
51+ import spock .lang .Specification ;
4652
4753import java .lang .reflect .Method ;
4854import java .util .ArrayList ;
4955import java .util .Arrays ;
5056import java .util .Collections ;
57+ import java .util .IdentityHashMap ;
5158import java .util .List ;
5259import java .util .Locale ;
60+ import java .util .Map ;
5361import java .util .Objects ;
5462import java .util .Optional ;
5563import java .util .Set ;
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