Skip to content

Commit 585b4cf

Browse files
committed
stages
1 parent 0d07510 commit 585b4cf

5 files changed

Lines changed: 299 additions & 6 deletions

File tree

allure-java-commons/src/main/java/io/qameta/allure/Allure.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,29 @@ public static <T> T step(final ThrowableContextRunnable<T, StepContext> runnable
192192
}
193193
}
194194

195+
/**
196+
* Starts a stage — a lightweight marker for a semantic test phase, rendered as a regular step. A stage has no
197+
* explicit stop: it stays open, collecting the steps and attachments that follow, until the next stage starts
198+
* or the enclosing step, test, or fixture ends. A stage started inside a step becomes a child of that step.
199+
* Takes no effect if no test run at the moment.
200+
*
201+
* <pre><code>
202+
* Allure.stage("prepare data");
203+
* final Customer customer = createCustomer();
204+
*
205+
* Allure.stage("submit order");
206+
* final Order order = submitOrder(customer);
207+
*
208+
* Allure.stage("verify result");
209+
* assertThat(order.getStatus()).isEqualTo("created");
210+
* </code></pre>
211+
*
212+
* @param name the name of the stage.
213+
*/
214+
public static void stage(final String name) {
215+
getLifecycle().startStage(new StepResult().setName(name));
216+
}
217+
195218
/**
196219
* Adds epic label to current test if any. Takes no effect
197220
* if no test run at the moment. Shortcut for {@link #label(String, String)}.

allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@ public void stopTest(final AllureExternalKey key) {
328328
LOGGER.warn("Could not stop test: test with key {} is not running", key);
329329
return;
330330
}
331+
if (isCurrentRoot(key)) {
332+
closeOpenStages();
333+
}
331334
notifier.beforeTestStop(testResult);
332335
testResult
333336
.setStage(Stage.FINISHED)
@@ -460,6 +463,9 @@ public void stopFixture(final AllureExternalKey key) {
460463
return;
461464
}
462465
final FixtureResult fixture = item.result;
466+
if (isCurrentRoot(key)) {
467+
closeOpenStages();
468+
}
463469
notifier.beforeFixtureStop(fixture);
464470
fixture.setStage(Stage.FINISHED);
465471
fixture.setStop(System.currentTimeMillis());
@@ -515,6 +521,11 @@ public void startStep(final AllureExternalKey parentKey, final AllureExternalKey
515521

516522
private void startStep(final AllureExternalKey parentKey, final AllureExternalKey key,
517523
final StepResult result, final boolean bind) {
524+
startStep(parentKey, key, result, bind, false);
525+
}
526+
527+
private void startStep(final AllureExternalKey parentKey, final AllureExternalKey key,
528+
final StepResult result, final boolean bind, final boolean stage) {
518529
Objects.requireNonNull(key, EXTERNAL_KEY);
519530
Objects.requireNonNull(parentKey, EXTERNAL_KEY);
520531
final Object parent = items.get(parentKey);
@@ -542,13 +553,79 @@ private void startStep(final AllureExternalKey parentKey, final AllureExternalKe
542553
final AllureExecutionContext snapshot = bind
543554
? threadContext.copy()
544555
: deriveSnapshot(parent, key);
545-
items.put(key, new StepItem(result, writeOwnerOf(parentKey, parent), snapshot));
556+
items.put(key, new StepItem(result, writeOwnerOf(parentKey, parent), snapshot, stage));
546557
synchronized (parent) {
547558
((WithSteps) parentModel).getSteps().add(result);
548559
}
549560
notifier.afterStepStart(result);
550561
}
551562

563+
/**
564+
* Starts a stage — a lightweight phase marker rendered as a regular step. A stage has no explicit stop: it stays
565+
* open, collecting the steps and attachments that follow, until the next stage starts at the same level or the
566+
* enclosing step, test, or fixture ends. A stage started inside a step becomes a child of that step. A stage
567+
* with no status when it closes is marked passed.
568+
*
569+
* <p>Stages are an ambient-only concept: their lifetime is defined by the calling thread's binding, so there is
570+
* no key-addressed form. Takes no effect if no executable is running.</p>
571+
*
572+
* @param result the stage step, carrying its name
573+
*/
574+
public void startStage(final StepResult result) {
575+
if (threadContext.getCurrentExecutable().isEmpty()) {
576+
LOGGER.warn("Could not start stage: no test or fixture running");
577+
return;
578+
}
579+
closeOpenStages();
580+
final Optional<AllureExternalKey> parent = threadContext.getCurrentExecutable();
581+
if (parent.isEmpty()) {
582+
return;
583+
}
584+
startStep(parent.get(), AllureExternalKey.random(AllureLifecycle.class), result, true, true);
585+
}
586+
587+
/**
588+
* Closes consecutive open stages on top of the calling thread's stack. A stage with no status is marked passed.
589+
*/
590+
private void closeOpenStages() {
591+
while (true) {
592+
final Optional<AllureExternalKey> current = threadContext.getCurrentStep();
593+
if (current.isEmpty()) {
594+
return;
595+
}
596+
final Object item = items.get(current.get());
597+
if (!(item instanceof StepItem) || !((StepItem) item).stage) {
598+
return;
599+
}
600+
finalizeStage((StepItem) item);
601+
stopStep(current.get(), true);
602+
}
603+
}
604+
605+
/**
606+
* Closes consecutive open stages bound above the given step on the calling thread's stack.
607+
*/
608+
private void closeOpenStagesAbove(final AllureExternalKey key) {
609+
while (true) {
610+
final Optional<AllureExternalKey> current = threadContext.getCurrentStep();
611+
if (current.isEmpty() || current.get().equals(key)) {
612+
return;
613+
}
614+
final Object item = items.get(current.get());
615+
if (!(item instanceof StepItem) || !((StepItem) item).stage) {
616+
return;
617+
}
618+
finalizeStage((StepItem) item);
619+
stopStep(current.get(), true);
620+
}
621+
}
622+
623+
private static void finalizeStage(final StepItem item) {
624+
if (Objects.isNull(item.result.getStatus())) {
625+
item.result.setStatus(Status.PASSED);
626+
}
627+
}
628+
552629
/**
553630
* Updates step by specified key.
554631
*
@@ -566,32 +643,55 @@ public void updateStep(final AllureExternalKey key, final Consumer<StepResult> u
566643
}
567644

568645
/**
569-
* Updates current step.
646+
* Updates current step. Stages are transparent to this: the update targets the deepest non-stage step, so a
647+
* caller finishing its own step is not misdirected onto a stage the user opened inside it.
570648
*
571649
* @param update the update function.
572650
*/
573651
public void updateStep(final Consumer<StepResult> update) {
574-
final Optional<AllureExternalKey> current = threadContext.getCurrent();
652+
final Optional<AllureExternalKey> current = currentNonStageStep();
575653
if (current.isEmpty()) {
576654
LOGGER.warn("Could not update step: no step running");
577655
return;
578656
}
579657
updateStep(current.get(), update);
580658
}
581659

660+
private Optional<AllureExternalKey> currentNonStageStep() {
661+
final AllureExternalKey root = threadContext.getRoot().orElse(null);
662+
for (final AllureExternalKey key : threadContext.getLocalKeys()) {
663+
if (Objects.equals(key, root)) {
664+
return Optional.empty();
665+
}
666+
final Object item = items.get(key);
667+
if (!(item instanceof StepItem)) {
668+
return Optional.empty();
669+
}
670+
if (!((StepItem) item).stage) {
671+
return Optional.of(key);
672+
}
673+
}
674+
return Optional.empty();
675+
}
676+
582677
/**
583-
* Stops step by given key. Pure manual form: no thread state is touched.
678+
* Stops step by given key. Pure manual form with one thread-affine convenience: when the stopped step is bound
679+
* on the calling thread with open stages above it, those stages are closed first.
584680
*
585681
* @param key the external step key
586682
*/
587683
public void stopStep(final AllureExternalKey key) {
684+
if (threadContext.getLocalKeys().contains(key)) {
685+
closeOpenStagesAbove(key);
686+
}
588687
stopStep(key, false);
589688
}
590689

591690
/**
592-
* Stops the current running step and pops it from the calling thread.
691+
* Stops the current running step and pops it from the calling thread. Open stages above it are closed first.
593692
*/
594693
public void stopStep() {
694+
closeOpenStages();
595695
final Optional<AllureExternalKey> current = threadContext.getCurrentStep();
596696
if (current.isEmpty()) {
597697
LOGGER.warn("Could not stop step: no step running");
@@ -1312,11 +1412,14 @@ private static final class StepItem {
13121412

13131413
private final AllureExecutionContext contextSnapshot;
13141414

1415+
private final boolean stage;
1416+
13151417
private StepItem(final StepResult result, final AllureExternalKey writeOwnerKey,
1316-
final AllureExecutionContext contextSnapshot) {
1418+
final AllureExecutionContext contextSnapshot, final boolean stage) {
13171419
this.result = result;
13181420
this.writeOwnerKey = writeOwnerKey;
13191421
this.contextSnapshot = contextSnapshot;
1422+
this.stage = stage;
13201423
}
13211424
}
13221425

allure-java-commons/src/main/java/io/qameta/allure/internal/AllureExecutionContext.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
import io.qameta.allure.AllureExternalKey;
1919

20+
import java.util.ArrayList;
2021
import java.util.Deque;
2122
import java.util.LinkedList;
23+
import java.util.List;
2224
import java.util.Objects;
2325
import java.util.Optional;
2426

@@ -90,6 +92,15 @@ public void start(final AllureExternalKey key) {
9092
localKeys.push(key);
9193
}
9294

95+
/**
96+
* Returns a snapshot of the locally bound keys, most recent first.
97+
*
98+
* @return local keys, top of the stack first
99+
*/
100+
public List<AllureExternalKey> getLocalKeys() {
101+
return new ArrayList<>(localKeys);
102+
}
103+
93104
/**
94105
* Returns a copy of this execution context.
95106
*

allure-java-commons/src/main/java/io/qameta/allure/internal/AllureThreadContext.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.util.Deque;
2121
import java.util.LinkedList;
22+
import java.util.List;
2223
import java.util.Objects;
2324
import java.util.Optional;
2425

@@ -59,6 +60,15 @@ public Optional<AllureExternalKey> getCurrentExecutable() {
5960
return get().getCurrentExecutable();
6061
}
6162

63+
/**
64+
* Returns a snapshot of the current binding's locally bound keys, most recent first.
65+
*
66+
* @return local keys, top of the stack first
67+
*/
68+
public List<AllureExternalKey> getLocalKeys() {
69+
return get().getLocalKeys();
70+
}
71+
6272
/**
6373
* Adds new key.
6474
*/

0 commit comments

Comments
 (0)