Skip to content

Commit ee99690

Browse files
committed
fix notifier
1 parent e8e87e3 commit ee99690

3 files changed

Lines changed: 59 additions & 104 deletions

File tree

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

Lines changed: 21 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import java.util.function.Consumer;
6262

6363
import static io.qameta.allure.AllureConstants.ATTACHMENT_FILE_SUFFIX;
64+
import static io.qameta.allure.util.ResultsUtils.firstNonEmpty;
6465
import static io.qameta.allure.util.ResultsUtils.getStatus;
6566
import static io.qameta.allure.util.ResultsUtils.getStatusDetails;
6667
import static io.qameta.allure.util.ServiceLoaderUtils.load;
@@ -230,7 +231,7 @@ public void scheduleTest(final AllureExternalKey key, final TestResult result) {
230231
LOGGER.warn(KEY_ALREADY_EXISTS, SCHEDULE_TEST, key);
231232
return;
232233
}
233-
if (isEmpty(result.getUuid())) {
234+
if (firstNonEmpty(result.getUuid()).isEmpty()) {
234235
result.setUuid(UUID.randomUUID().toString());
235236
}
236237
notifier.beforeTestSchedule(result);
@@ -584,22 +585,12 @@ public void startStage(final StepResult result) {
584585
* Closes consecutive open stages on top of the calling thread's stack. A stage with no status is marked passed.
585586
*/
586587
private void closeOpenStages() {
587-
while (true) {
588-
final Optional<AllureExternalKey> current = threadContext.getCurrentStep();
589-
if (current.isEmpty()) {
590-
return;
591-
}
592-
final Object item = items.get(current.get());
593-
if (!(item instanceof StepItem) || !((StepItem) item).stage()) {
594-
return;
595-
}
596-
finalizeStage((StepItem) item);
597-
stopStep(current.get(), true);
598-
}
588+
closeOpenStagesAbove(null);
599589
}
600590

601591
/**
602-
* Closes consecutive open stages bound above the given step on the calling thread's stack.
592+
* Closes consecutive open stages bound above the given step on the calling thread's stack, or all consecutive
593+
* top stages when the step is {@code null}. A stage with no status is marked passed.
603594
*/
604595
private void closeOpenStagesAbove(final AllureExternalKey key) {
605596
while (true) {
@@ -611,17 +602,13 @@ private void closeOpenStagesAbove(final AllureExternalKey key) {
611602
if (!(item instanceof StepItem) || !((StepItem) item).stage()) {
612603
return;
613604
}
614-
finalizeStage((StepItem) item);
605+
if (Objects.isNull(((StepItem) item).result().getStatus())) {
606+
((StepItem) item).result().setStatus(Status.PASSED);
607+
}
615608
stopStep(current.get(), true);
616609
}
617610
}
618611

619-
private static void finalizeStage(final StepItem item) {
620-
if (Objects.isNull(item.result().getStatus())) {
621-
item.result().setStatus(Status.PASSED);
622-
}
623-
}
624-
625612
/**
626613
* Updates step by specified key.
627614
*
@@ -837,7 +824,7 @@ private CompletableFuture<Void> addAttachmentAsync(final AllureExternalKey key,
837824

838825
/**
839826
* Adds an attachment wrapped in its own instant step under the current executable — the default representation
840-
* for user-facing attachments. Falls back to a plain attachment while a lifecycle listener callback is running.
827+
* for user-facing attachments. Safe to call from listener callbacks: the wrapper step emits no further events.
841828
* Takes no effect if no executable is running.
842829
*
843830
* @param name the name of attachment
@@ -852,10 +839,6 @@ public void addAttachmentStep(final String name, final String type,
852839
LOGGER.warn(NO_CONTEXT_FOR_ATTACHMENT);
853840
return;
854841
}
855-
if (LifecycleNotifier.isListenerCallbackRunning()) {
856-
addAttachment(current.get(), name, type, content, options);
857-
return;
858-
}
859842
final AllureExternalKey key = AllureExternalKey.random(AllureLifecycle.class);
860843
startStep(current.get(), key, new StepResult().setName(attachmentStepName(name)), true);
861844
if (!items.containsKey(key)) {
@@ -876,8 +859,7 @@ public void addAttachmentStep(final String name, final String type,
876859

877860
/**
878861
* Adds an attachment wrapped in its own instant step under the specified parent — the default representation
879-
* for user-facing attachments. Pure manual linkage: no thread state is touched. Falls back to a plain
880-
* attachment while a lifecycle listener callback is running.
862+
* for user-facing attachments. Pure manual linkage: no thread state is touched.
881863
*
882864
* @param parentKey the external parent key
883865
* @param name the name of attachment
@@ -887,11 +869,6 @@ public void addAttachmentStep(final String name, final String type,
887869
*/
888870
public void addAttachmentStep(final AllureExternalKey parentKey, final String name, final String type,
889871
final InputStream content, final AttachmentOptions options) {
890-
if (LifecycleNotifier.isListenerCallbackRunning()) {
891-
// a listener adding attachments must not spawn steps (and further listener callbacks) recursively
892-
addAttachment(parentKey, name, type, content, options);
893-
return;
894-
}
895872
final AllureExternalKey key = AllureExternalKey.random(AllureLifecycle.class);
896873
startStep(parentKey, key, new StepResult().setName(attachmentStepName(name)), false);
897874
if (!items.containsKey(key)) {
@@ -913,8 +890,7 @@ public void addAttachmentStep(final AllureExternalKey parentKey, final String na
913890
/**
914891
* Adds an async attachment wrapped in its own instant step under the current executable. The attachment content
915892
* is awaited before the owning test or scope is written; a failed body marks the step broken before the owner
916-
* is serialized. Falls back to a plain async attachment while a lifecycle listener callback is running. Takes
917-
* no effect if no executable is running.
893+
* is serialized. Takes no effect if no executable is running.
918894
*
919895
* @param name the name of attachment
920896
* @param type the content type of attachment
@@ -930,9 +906,6 @@ public CompletableFuture<Void> addAttachmentStepAsync(final String name, final S
930906
LOGGER.warn(NO_CONTEXT_FOR_ATTACHMENT);
931907
return CompletableFuture.completedFuture(null);
932908
}
933-
if (LifecycleNotifier.isListenerCallbackRunning()) {
934-
return addAttachmentAsync(current.get(), name, type, body, options, null);
935-
}
936909
final AllureExternalKey key = AllureExternalKey.random(AllureLifecycle.class);
937910
final StepResult step = new StepResult()
938911
.setName(attachmentStepName(name))
@@ -958,8 +931,7 @@ public CompletableFuture<Void> addAttachmentStepAsync(final String name, final S
958931
/**
959932
* Adds an async attachment wrapped in its own instant step under the specified parent. Pure manual linkage: no
960933
* thread state is touched. The attachment content is awaited before the owning test or scope is written; a
961-
* failed body marks the step broken before the owner is serialized. Falls back to a plain async attachment
962-
* while a lifecycle listener callback is running.
934+
* failed body marks the step broken before the owner is serialized.
963935
*
964936
* @param parentKey the external parent key
965937
* @param name the name of attachment
@@ -972,9 +944,6 @@ public CompletableFuture<Void> addAttachmentStepAsync(final AllureExternalKey pa
972944
final String type,
973945
final CompletionStage<? extends InputStream> body,
974946
final AttachmentOptions options) {
975-
if (LifecycleNotifier.isListenerCallbackRunning()) {
976-
return addAttachmentAsync(parentKey, name, type, body, options);
977-
}
978947
final AllureExternalKey key = AllureExternalKey.random(AllureLifecycle.class);
979948
final StepResult step = new StepResult()
980949
.setName(attachmentStepName(name))
@@ -998,7 +967,7 @@ public CompletableFuture<Void> addAttachmentStepAsync(final AllureExternalKey pa
998967
}
999968

1000969
private static String attachmentStepName(final String name) {
1001-
return isEmpty(name) ? "Attachment" : name;
970+
return firstNonEmpty(name).orElse("Attachment");
1002971
}
1003972

1004973
private Optional<String> addAttachmentLink(final AllureExternalKey key, final String name,
@@ -1011,8 +980,8 @@ private Optional<String> addAttachmentLink(final AllureExternalKey key, final St
1011980
return Optional.empty();
1012981
}
1013982
final Attachment attachment = new Attachment()
1014-
.setName(isEmpty(name) ? null : name)
1015-
.setType(isEmpty(type) ? null : type)
983+
.setName(firstNonEmpty(name).orElse(null))
984+
.setType(firstNonEmpty(type).orElse(null))
1016985
.setSource(createAttachmentSource(type, options));
1017986
synchronized (item) {
1018987
((WithAttachments) model).getAttachments().add(attachment);
@@ -1268,7 +1237,7 @@ private void sweepOwnedSteps(final AllureExternalKey ownerKey) {
12681237
}
12691238

12701239
private void addChild(final ScopeItem scope, final String childUuid) {
1271-
if (isEmpty(childUuid)) {
1240+
if (firstNonEmpty(childUuid).isEmpty()) {
12721241
return;
12731242
}
12741243
synchronized (scope) {
@@ -1327,19 +1296,11 @@ private static TestResultContainer toScopeContainer(final ScopeResult scope) {
13271296
* detaches any list alias the consumer may have retained.
13281297
*/
13291298
private static void normalizeScope(final ScopeResult scope) {
1330-
scope.setTests(mutableList(scope.getTests()));
1331-
scope.setFixtures(mutableList(scope.getFixtures()));
1332-
scope.setLabels(mutableList(scope.getLabels()));
1333-
scope.setLinks(mutableList(scope.getLinks()));
1334-
scope.setParameters(mutableList(scope.getParameters()));
1335-
}
1336-
1337-
private static <T> List<T> mutableList(final List<T> values) {
1338-
return Objects.isNull(values) ? new ArrayList<>() : new ArrayList<>(values);
1339-
}
1340-
1341-
private static boolean isEmpty(final String s) {
1342-
return Objects.isNull(s) || s.isEmpty();
1299+
scope.setTests(new ArrayList<>(Objects.requireNonNullElse(scope.getTests(), List.of())));
1300+
scope.setFixtures(new ArrayList<>(Objects.requireNonNullElse(scope.getFixtures(), List.of())));
1301+
scope.setLabels(new ArrayList<>(Objects.requireNonNullElse(scope.getLabels(), List.of())));
1302+
scope.setLinks(new ArrayList<>(Objects.requireNonNullElse(scope.getLinks(), List.of())));
1303+
scope.setParameters(new ArrayList<>(Objects.requireNonNullElse(scope.getParameters(), List.of())));
13431304
}
13441305

13451306
// ── Items ────────────────────────────────────────────────────────────────────────────────

allure-java-commons/src/main/java/io/qameta/allure/listener/LifecycleNotifier.java

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
import java.util.function.BiConsumer;
2727

2828
/**
29+
* Dispatches lifecycle events to the registered listeners.
30+
*
31+
* <p>Notification is not re-entrant: lifecycle operations triggered from inside a listener callback mutate the
32+
* model normally but emit no further events. This lets listeners enrich results with the regular API — steps,
33+
* stages, attachment steps — without recursing back into themselves.</p>
34+
*
2935
* @since 2.0
3036
*/
3137
@SuppressWarnings("PMD.TooManyMethods")
@@ -207,29 +213,25 @@ public void afterStepStop(final StepResult result) {
207213
runSafely(stepListeners, StepLifecycleListener::afterStepStop, result);
208214
}
209215

210-
/**
211-
* Returns whether a lifecycle listener callback is running on the current thread.
212-
*
213-
* @return true if a lifecycle listener callback is running, false otherwise
214-
*/
215-
public static boolean isListenerCallbackRunning() {
216-
return LISTENER_CALLBACK_RUNNING.get();
217-
}
218-
219216
protected <T extends LifecycleListener, S> void runSafely(final List<T> listeners,
220217
final BiConsumer<T, S> method,
221218
final S object) {
222-
listeners.forEach(listener -> {
223-
final boolean previous = LISTENER_CALLBACK_RUNNING.get();
224-
LISTENER_CALLBACK_RUNNING.set(true);
225-
try {
226-
method.accept(listener, object);
227-
} catch (Exception e) {
228-
LOGGER.error("Could not invoke listener method", e);
229-
} finally {
230-
LISTENER_CALLBACK_RUNNING.set(previous);
231-
}
232-
});
219+
if (LISTENER_CALLBACK_RUNNING.get()) {
220+
// operations triggered from inside a listener callback emit no further events
221+
return;
222+
}
223+
LISTENER_CALLBACK_RUNNING.set(true);
224+
try {
225+
listeners.forEach(listener -> {
226+
try {
227+
method.accept(listener, object);
228+
} catch (Exception e) {
229+
LOGGER.error("Could not invoke listener method", e);
230+
}
231+
});
232+
} finally {
233+
LISTENER_CALLBACK_RUNNING.set(false);
234+
}
233235
}
234236

235237
}

allure-java-commons/src/test/java/io/qameta/allure/StepLifecycleListenerTest.java

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -69,22 +69,25 @@ public void afterStepStart(final StepResult result) {
6969
.extracting(StepResult::getName)
7070
.containsExactly("first", "second");
7171

72-
assertThat(run.getTestResults())
73-
.flatExtracting(TestResult::getSteps)
74-
.filteredOn("name", "first")
75-
.flatExtracting(StepResult::getAttachments)
76-
.extracting(Attachment::getName)
77-
.containsExactly("inner first");
72+
// the listener's attachment gets the default representation — an attachment step —
73+
// whose own events are suppressed by the notifier
74+
assertAttachmentStep(run, "first", "inner first");
75+
assertAttachmentStep(run, "second", "inner second");
76+
77+
assertThat(executionCount.get())
78+
.isEqualTo(2);
79+
}
7880

81+
private static void assertAttachmentStep(final AllureResults run, final String stepName,
82+
final String attachmentName) {
7983
assertThat(run.getTestResults())
8084
.flatExtracting(TestResult::getSteps)
81-
.filteredOn("name", "second")
85+
.filteredOn("name", stepName)
86+
.flatExtracting(StepResult::getSteps)
87+
.filteredOn("name", attachmentName)
8288
.flatExtracting(StepResult::getAttachments)
8389
.extracting(Attachment::getName)
84-
.containsExactly("inner second");
85-
86-
assertThat(executionCount.get())
87-
.isEqualTo(2);
90+
.containsExactly(attachmentName);
8891
}
8992

9093
@Issue("177")
@@ -105,19 +108,8 @@ public void beforeStepStop(final StepResult result) {
105108
.extracting(StepResult::getName)
106109
.containsExactly("first", "second");
107110

108-
assertThat(run.getTestResults())
109-
.flatExtracting(TestResult::getSteps)
110-
.filteredOn("name", "first")
111-
.flatExtracting(StepResult::getAttachments)
112-
.extracting(Attachment::getName)
113-
.containsExactly("inner first");
114-
115-
assertThat(run.getTestResults())
116-
.flatExtracting(TestResult::getSteps)
117-
.filteredOn("name", "second")
118-
.flatExtracting(StepResult::getAttachments)
119-
.extracting(Attachment::getName)
120-
.containsExactly("inner second");
111+
assertAttachmentStep(run, "first", "inner first");
112+
assertAttachmentStep(run, "second", "inner second");
121113

122114
assertThat(executionCount.get())
123115
.isEqualTo(2);

0 commit comments

Comments
 (0)