3535import java .io .StringWriter ;
3636import java .nio .charset .StandardCharsets ;
3737import java .util .concurrent .TimeUnit ;
38+ import java .util .concurrent .atomic .AtomicBoolean ;
39+ import java .util .concurrent .atomic .AtomicReference ;
3840
3941import static java .util .concurrent .TimeUnit .MILLISECONDS ;
4042
6062 * </code>
6163 * </pre>
6264 * </p>
65+ * <p>
66+ * A listener instance tracks a single condition at a time, so prefer one instance per condition when tests run in
67+ * parallel. Awaitility fires no callback when a non-ignored exception escapes a condition; such an aborted wait
68+ * stays the reporting parent until the same listener starts its next condition, which finalizes the aborted one,
69+ * or until the test ends.
70+ * </p>
6371 *
6472 * @see org.awaitility.core.ConditionEvaluationListener
6573 * @see Awaitility#setDefaultConditionEvaluationListener(ConditionEvaluationListener)
6977@ SuppressWarnings ("unused" )
7078public class AllureAwaitilityListener implements ConditionEvaluationListener <Object > {
7179
72- private TimeUnit unit ;
73- private boolean logIgnoredExceptions ;
80+ private final AtomicReference < TimeUnit > unit = new AtomicReference <>( MILLISECONDS ) ;
81+ private final AtomicBoolean logIgnoredExceptions = new AtomicBoolean ( true ) ;
7482 private final String onStartStepTextPattern ;
7583 private final String onSatisfiedStepTextPattern ;
7684 private final String onAwaitStepTextPattern ;
7785 private final String onTimeoutStepTextPattern ;
7886 private final String onExceptionStepTextPattern ;
7987
80- private AllureExternalKey currentConditionStepKey ;
81-
82- private AllureThreadBinding currentConditionBinding ;
88+ private final AtomicReference <ConditionState > currentCondition = new AtomicReference <>();
8389
8490 /**
8591 * Returns the lifecycle.
@@ -94,8 +100,6 @@ public static AllureLifecycle getLifecycle() {
94100 * Default all args constructor with default params.
95101 */
96102 public AllureAwaitilityListener () {
97- this .unit = MILLISECONDS ;
98- this .logIgnoredExceptions = true ;
99103 this .onStartStepTextPattern = "Awaitility: %s" ;
100104 this .onSatisfiedStepTextPattern = "%s after %d %s (remaining time %d %s, last poll interval was %s)" ;
101105 this .onAwaitStepTextPattern = "%s (elapsed time %d %s, remaining time %d %s (last poll interval was %s))" ;
@@ -110,7 +114,7 @@ public AllureAwaitilityListener() {
110114 * @return this factory
111115 */
112116 public AllureAwaitilityListener setUnit (final TimeUnit unit ) {
113- this .unit = unit ;
117+ this .unit . set ( unit ) ;
114118 return this ;
115119 }
116120
@@ -121,7 +125,7 @@ public AllureAwaitilityListener setUnit(final TimeUnit unit) {
121125 * @return this factory
122126 */
123127 public AllureAwaitilityListener setLogIgnoredExceptions (final boolean logging ) {
124- this .logIgnoredExceptions = logging ;
128+ this .logIgnoredExceptions . set ( logging ) ;
125129 return this ;
126130 }
127131
@@ -132,24 +136,26 @@ public AllureAwaitilityListener setLogIgnoredExceptions(final boolean logging) {
132136 */
133137 @ Override
134138 public void beforeEvaluation (final StartEvaluationEvent <Object > startEvaluationEvent ) {
135- currentConditionStepKey = null ;
136- getLifecycle ().getCurrentExecutableKey ().ifPresent (parent -> {
139+ finishCurrentCondition ();
140+ final AllureLifecycle lifecycle = getLifecycle ();
141+ lifecycle .getCurrentExecutableKey ().ifPresent (parent -> {
137142 final String nameWoAlias = String .format (onStartStepTextPattern , startEvaluationEvent .getDescription ());
138143 final String nameWithAlias = String .format (onStartStepTextPattern , startEvaluationEvent .getAlias ());
139144 final String stepName = startEvaluationEvent .getAlias () != null ? nameWithAlias : nameWoAlias ;
140145 final AllureExternalKey conditionStepKey = AllureExternalKey .random (AllureAwaitilityListener .class );
141- currentConditionStepKey = conditionStepKey ;
142- getLifecycle ().startStep (
146+ lifecycle .startStep (
143147 parent ,
144148 conditionStepKey ,
145149 new StepResult ()
146150 .setName (stepName )
147151 .setDescription ("Awaitility condition started" )
148152 .setStatus (Status .FAILED )
149153 );
150- // bind the polling thread to the condition step, so steps produced while evaluating the
151- // condition — for example assertion steps from untilAsserted polls — nest under it
152- currentConditionBinding = getLifecycle ().bindDetached (conditionStepKey );
154+ final ConditionState condition = new ConditionState (lifecycle , conditionStepKey );
155+ currentCondition .set (condition );
156+ // Keep condition-body steps under the wait. The binding remembers the caller thread's stack, so
157+ // Awaitility may close it from a polling callback without mutating a reused worker's unrelated context.
158+ condition .setBinding (lifecycle .bindDetached (conditionStepKey ));
153159 });
154160 }
155161
@@ -160,18 +166,18 @@ public void beforeEvaluation(final StartEvaluationEvent<Object> startEvaluationE
160166 */
161167 @ Override
162168 public void onTimeout (final TimeoutEvent timeoutEvent ) {
163- if (currentConditionStepKey == null ) {
169+ final ConditionState condition = currentCondition .get ();
170+ if (condition == null ) {
164171 return ;
165172 }
166- closeConditionBinding ();
167- getLifecycle ().logStep (
168- currentConditionStepKey ,
173+ condition .getLifecycle ().logStep (
174+ condition .getStepKey (),
169175 new StepResult ()
170176 .setName (String .format (onTimeoutStepTextPattern , timeoutEvent .getDescription ()))
171177 .setDescription ("Awaitility condition timeout" )
172178 .setStatus (Status .BROKEN )
173179 );
174- getLifecycle (). stopStep ( currentConditionStepKey );
180+ finishCondition ( condition );
175181 }
176182
177183 /**
@@ -181,10 +187,11 @@ public void onTimeout(final TimeoutEvent timeoutEvent) {
181187 */
182188 @ Override
183189 public void conditionEvaluated (final EvaluatedCondition <Object > condition ) {
190+ final TimeUnit currentUnit = unit .get ();
184191 final String description = condition .getDescription ();
185- final long elapsedTime = unit .convert (condition .getElapsedTimeInMS (), MILLISECONDS );
186- final long remainingTime = unit .convert (condition .getRemainingTimeInMS (), MILLISECONDS );
187- final String unitAsString = unit .toString ().toLowerCase ();
192+ final long elapsedTime = currentUnit .convert (condition .getElapsedTimeInMS (), MILLISECONDS );
193+ final long remainingTime = currentUnit .convert (condition .getRemainingTimeInMS (), MILLISECONDS );
194+ final String unitAsString = currentUnit .toString ().toLowerCase ();
188195
189196 final String message = String .format (
190197 condition .isSatisfied () ? onSatisfiedStepTextPattern : onAwaitStepTextPattern ,
@@ -196,29 +203,79 @@ public void conditionEvaluated(final EvaluatedCondition<Object> condition) {
196203 new TemporalDuration (condition .getPollInterval ())
197204 );
198205
199- if (currentConditionStepKey == null ) {
206+ final ConditionState current = currentCondition .get ();
207+ if (current == null ) {
200208 return ;
201209 }
202- getLifecycle ().logStep (
203- currentConditionStepKey ,
210+ current . getLifecycle ().logStep (
211+ current . getStepKey () ,
204212 new StepResult ()
205213 .setName (message )
206214 .setDescription ("Awaitility condition satisfied or not, but awaiting still in progress" )
207215 .setStatus (Status .PASSED )
208216 );
209217 if (condition .isSatisfied ()) {
210- closeConditionBinding ();
211- getLifecycle ().updateStep (
212- currentConditionStepKey , awaitilityCondition -> awaitilityCondition .setStatus (Status .PASSED )
218+ current .getLifecycle ().updateStep (
219+ current .getStepKey (), awaitilityCondition -> awaitilityCondition .setStatus (Status .PASSED )
213220 );
214- getLifecycle ().stopStep (currentConditionStepKey );
221+ finishCondition (current );
222+ }
223+ }
224+
225+ private void finishCurrentCondition () {
226+ final ConditionState condition = currentCondition .getAndSet (null );
227+ if (condition != null ) {
228+ condition .finish ();
215229 }
216230 }
217231
218- private void closeConditionBinding () {
219- if (currentConditionBinding != null ) {
220- currentConditionBinding .close ();
221- currentConditionBinding = null ;
232+ private void finishCondition (final ConditionState condition ) {
233+ currentCondition .compareAndSet (condition , null );
234+ condition .finish ();
235+ }
236+
237+ private static final class ConditionState {
238+
239+ private final AllureLifecycle lifecycle ;
240+ private final AllureExternalKey stepKey ;
241+ private final AtomicReference <AllureThreadBinding > binding = new AtomicReference <>();
242+ private final AtomicBoolean finished = new AtomicBoolean ();
243+
244+ private ConditionState (final AllureLifecycle lifecycle , final AllureExternalKey stepKey ) {
245+ this .lifecycle = lifecycle ;
246+ this .stepKey = stepKey ;
247+ }
248+
249+ private AllureLifecycle getLifecycle () {
250+ return lifecycle ;
251+ }
252+
253+ private AllureExternalKey getStepKey () {
254+ return stepKey ;
255+ }
256+
257+ private void setBinding (final AllureThreadBinding value ) {
258+ binding .set (value );
259+ if (finished .get ()) {
260+ closeBinding ();
261+ }
262+ }
263+
264+ private void finish () {
265+ if (finished .compareAndSet (false , true )) {
266+ try {
267+ closeBinding ();
268+ } finally {
269+ lifecycle .stopStep (stepKey );
270+ }
271+ }
272+ }
273+
274+ private void closeBinding () {
275+ final AllureThreadBinding current = binding .getAndSet (null );
276+ if (current != null ) {
277+ current .close ();
278+ }
222279 }
223280 }
224281
@@ -237,30 +294,31 @@ private void closeConditionBinding() {
237294 */
238295 @ Override
239296 public void exceptionIgnored (final IgnoredException ignoredException ) {
240- if (logIgnoredExceptions && currentConditionStepKey != null ) {
297+ final ConditionState condition = currentCondition .get ();
298+ if (logIgnoredExceptions .get () && condition != null ) {
241299 final AllureExternalKey exceptionIgnoredStepKey = AllureExternalKey .random (AllureAwaitilityListener .class );
242300 final String message = String .format (
243301 onExceptionStepTextPattern , ignoredException .getThrowable ().getMessage ()
244302 );
245303 final StringWriter stringWriter = new StringWriter ();
246304 ignoredException .getThrowable ().printStackTrace (new PrintWriter (stringWriter ));
247305 final String stackTrace = stringWriter .toString ();
248- getLifecycle ().startStep (
249- currentConditionStepKey ,
306+ condition . getLifecycle ().startStep (
307+ condition . getStepKey () ,
250308 exceptionIgnoredStepKey ,
251309 new StepResult ()
252310 .setName (message )
253311 .setDescription ("Exception occurred and ignored, but awaiting still in progress" )
254312 .setStatus (Status .SKIPPED )
255313 );
256- getLifecycle ().addAttachment (
314+ condition . getLifecycle ().addAttachment (
257315 exceptionIgnoredStepKey ,
258316 ignoredException .getThrowable ().getMessage (),
259317 "text/plain" ,
260318 new ByteArrayInputStream (stackTrace .getBytes (StandardCharsets .UTF_8 )),
261319 AttachmentOptions .empty ()
262320 );
263- getLifecycle ().stopStep (exceptionIgnoredStepKey );
321+ condition . getLifecycle ().stopStep (exceptionIgnoredStepKey );
264322 }
265323 }
266324
0 commit comments