7676import java .util .UUID ;
7777import java .util .concurrent .ConcurrentHashMap ;
7878import java .util .concurrent .CopyOnWriteArrayList ;
79- import java .util .concurrent .locks .ReadWriteLock ;
80- import java .util .concurrent .locks .ReentrantReadWriteLock ;
8179import java .util .function .Consumer ;
8280import java .util .stream .Collectors ;
8381import java .util .stream .Stream ;
@@ -164,9 +162,10 @@ public class AllureTestNg
164162 .withInitial (() -> UUID .randomUUID ().toString ());
165163
166164 /**
167- * Store uuid for per-class scopes.
165+ * Store uuid for per-class scopes: one scope per test class instance. Class fixtures run once per instance,
166+ * so factory-created instances of the same class each get their own scope holding only that instance's tests.
168167 */
169- private final Map <ITestClass , String > classScopeUuidStorage = new ConcurrentHashMap <>();
168+ private final Map <ClassInstanceKey , String > classScopeUuidStorage = new ConcurrentHashMap <>();
170169
171170 /**
172171 * Store uuid for data provider scopes.
@@ -186,7 +185,6 @@ public class AllureTestNg
186185 * so they can no longer be reached through storage and are linked by these recorded uuids instead.
187186 */
188187 private final Map <GroupKey , List <String >> groupTestUuidStorage = new ConcurrentHashMap <>();
189- private final ReadWriteLock lock = new ReentrantReadWriteLock ();
190188 private final AllureLifecycle lifecycle ;
191189 private final AllureTestNgTestFilter testFilter ;
192190
@@ -264,11 +262,6 @@ public void onStart(final ITestContext context) {
264262 final String uuid = getUniqueUuid (context );
265263 getLifecycle ().registerScope (scopeKey (uuid ));
266264
267- Stream .of (context .getAllTestMethods ())
268- .map (ITestNGMethod ::getTestClass )
269- .distinct ()
270- .forEach (this ::onBeforeClass );
271-
272265 if (!config .isHideDisabledTests ()) {
273266 context .getExcludedMethods ().stream ()
274267 .filter (ITestNGMethod ::isTest )
@@ -297,38 +290,34 @@ public void onFinish(final ITestContext context) {
297290 final String uuid = getUniqueUuid (context );
298291 getLifecycle ().writeScope (scopeKey (uuid ));
299292
300- Stream .of (context .getAllTestMethods ())
301- .map (ITestNGMethod ::getTestClass )
302- .distinct ()
303- .forEach (this ::onAfterClass );
304-
305- groupScopeUuidStorage .entrySet ().removeIf (entry -> {
293+ classScopeUuidStorage .entrySet ().removeIf (entry -> {
306294 if (entry .getKey ().context ().equals (context )) {
307295 getLifecycle ().writeScope (scopeKey (entry .getValue ()));
308296 return true ;
309297 }
310298 return false ;
311299 });
312- groupTestUuidStorage .keySet ().removeIf (key -> key .context ().equals (context ));
313- }
314300
315- public void onBeforeClass (final ITestClass testClass ) {
316- final String uuid = UUID .randomUUID ().toString ();
317- getLifecycle ().registerScope (scopeKey (uuid ));
318- setClassScope (testClass , uuid );
319- }
320-
321- public void onAfterClass (final ITestClass testClass ) {
322- getClassScope (testClass ).ifPresent (uuid -> {
323- getLifecycle ().writeScope (scopeKey (uuid ));
324- });
301+ final List <ITestClass > contextClasses = Stream .of (context .getAllTestMethods ())
302+ .map (ITestNGMethod ::getTestClass )
303+ .distinct ()
304+ .collect (Collectors .toList ());
325305 dataProviderScopeUuidStorage .entrySet ().removeIf (entry -> {
326- if (entry .getKey ().getTestClass ().equals (testClass )) {
306+ if (contextClasses . stream (). anyMatch ( clazz -> entry .getKey ().getTestClass ().equals (clazz ) )) {
327307 getLifecycle ().writeScope (scopeKey (entry .getValue ()));
328308 return true ;
329309 }
330310 return false ;
331311 });
312+
313+ groupScopeUuidStorage .entrySet ().removeIf (entry -> {
314+ if (entry .getKey ().context ().equals (context )) {
315+ getLifecycle ().writeScope (scopeKey (entry .getValue ()));
316+ return true ;
317+ }
318+ return false ;
319+ });
320+ groupTestUuidStorage .keySet ().removeIf (key -> key .context ().equals (context ));
332321 }
333322
334323 @ Override
@@ -345,10 +334,7 @@ public void onTestStart(final ITestResult testResult) {
345334
346335 linkPendingBeforeMethodScopes (testKey (uuid ));
347336
348- Optional .of (testResult )
349- .map (ITestResult ::getMethod )
350- .map (ITestNGMethod ::getTestClass )
351- .ifPresent (clazz -> addTestToClassScope (clazz , uuid ));
337+ addTestToClassScope (testResult , uuid );
352338
353339 Optional .of (testResult )
354340 .map (ITestResult ::getMethod )
@@ -357,6 +343,14 @@ public void onTestStart(final ITestResult testResult) {
357343 addTestToGroupScopes (testResult , uuid );
358344 }
359345
346+ private void addTestToClassScope (final ITestResult testResult , final String uuid ) {
347+ final ITestClass testClass = testResult .getMethod ().getTestClass ();
348+ final String scopeUuid = getOrCreateClassScope (
349+ testResult .getTestContext (), testClass , testResult .getInstance ()
350+ );
351+ getLifecycle ().addTestToScope (scopeKey (scopeUuid ), testKey (uuid ));
352+ }
353+
360354 private void addTestToGroupScopes (final ITestResult testResult , final String uuid ) {
361355 final String [] groups = testResult .getMethod ().getGroups ();
362356 if (groups .length == 0 ) {
@@ -555,7 +549,7 @@ public void beforeInvocation(final IInvokedMethod method, final ITestResult test
555549 ifSuiteFixtureStarted (context .getSuite (), testMethod );
556550 ifTestFixtureStarted (context , testMethod );
557551 ifGroupsFixtureStarted (context , testMethod );
558- ifClassFixtureStarted (testMethod );
552+ ifClassFixtureStarted (context , testMethod , testResult . getInstance () );
559553 ifMethodFixtureStarted (testMethod );
560554 }
561555 }
@@ -569,17 +563,34 @@ private void ifSuiteFixtureStarted(final ISuite suite, final ITestNGMethod testM
569563 }
570564 }
571565
572- private void ifClassFixtureStarted (final ITestNGMethod testMethod ) {
566+ private void ifClassFixtureStarted (final ITestContext context ,
567+ final ITestNGMethod testMethod ,
568+ final Object instance ) {
573569 if (testMethod .isBeforeClassConfiguration ()) {
574- getClassScope (testMethod .getTestClass ())
575- .ifPresent (parentUuid -> startBefore (parentUuid , testMethod ));
570+ startBefore (getOrCreateClassScope (context , testMethod .getTestClass (), instance ), testMethod );
576571 }
577572 if (testMethod .isAfterClassConfiguration ()) {
578- getClassScope (testMethod .getTestClass ())
579- .ifPresent (parentUuid -> startAfter (parentUuid , testMethod ));
573+ startAfter (getOrCreateClassScope (context , testMethod .getTestClass (), instance ), testMethod );
580574 }
581575 }
582576
577+ /**
578+ * Returns the uuid of the scope shared by the class fixtures and tests running on the given test class
579+ * instance, registering it on first use. The scope is keyed by the real class rather than {@link ITestClass}:
580+ * TestNG hands out different {@link ITestClass} references for invocation-count and data-provider clones of
581+ * the same class, while the real class and the instance stay stable.
582+ */
583+ private String getOrCreateClassScope (final ITestContext context ,
584+ final ITestClass testClass ,
585+ final Object instance ) {
586+ final ClassInstanceKey key = new ClassInstanceKey (context , testClass .getRealClass (), instance );
587+ return classScopeUuidStorage .computeIfAbsent (key , k -> {
588+ final String uuid = UUID .randomUUID ().toString ();
589+ getLifecycle ().registerScope (scopeKey (uuid ));
590+ return uuid ;
591+ });
592+ }
593+
583594 private void ifTestFixtureStarted (final ITestContext context , final ITestNGMethod testMethod ) {
584595 if (testMethod .isBeforeTestConfiguration ()) {
585596 startBefore (getUniqueUuid (context ), testMethod );
@@ -720,7 +731,10 @@ public void onConfigurationFailure(final ITestResult itr) {
720731
721732 linkTestToScope (getUniqueUuid (itr .getTestContext ()), uuid );
722733 linkTestToScope (getUniqueUuid (itr .getTestContext ().getSuite ()), uuid );
723- addTestToClassScope (itr .getMethod ().getTestClass (), uuid );
734+ linkTestToScope (
735+ getOrCreateClassScope (itr .getTestContext (), itr .getMethod ().getTestClass (), itr .getInstance ()),
736+ uuid
737+ );
724738 if (itr .getMethod ().isBeforeGroupsConfiguration ()) {
725739 linkTestToScope (getOrCreateGroupsScope (itr .getTestContext (), itr .getMethod ().getBeforeGroups ()), uuid );
726740 }
@@ -1004,36 +1018,9 @@ private void addTestToDataProviderScope(final ITestNGMethod method, final String
10041018 this .linkTestToScope (dataProviderScopeUuidStorage .get (method ), childUuid );
10051019 }
10061020
1007- private void addTestToClassScope (final ITestClass clazz , final String childUuid ) {
1008- this .linkTestToScope (classScopeUuidStorage .get (clazz ), childUuid );
1009- }
1010-
10111021 private void linkTestToScope (final String scopeUuid , final String childUuid ) {
1012- lock .writeLock ().lock ();
1013- try {
1014- if (nonNull (scopeUuid )) {
1015- getLifecycle ().addTestToScope (scopeKey (scopeUuid ), testKey (childUuid ));
1016- }
1017- } finally {
1018- lock .writeLock ().unlock ();
1019- }
1020- }
1021-
1022- private Optional <String > getClassScope (final ITestClass clazz ) {
1023- lock .readLock ().lock ();
1024- try {
1025- return Optional .ofNullable (classScopeUuidStorage .get (clazz ));
1026- } finally {
1027- lock .readLock ().unlock ();
1028- }
1029- }
1030-
1031- private void setClassScope (final ITestClass clazz , final String uuid ) {
1032- lock .writeLock ().lock ();
1033- try {
1034- classScopeUuidStorage .put (clazz , uuid );
1035- } finally {
1036- lock .writeLock ().unlock ();
1022+ if (nonNull (scopeUuid )) {
1023+ getLifecycle ().addTestToScope (scopeKey (scopeUuid ), testKey (childUuid ));
10371024 }
10381025 }
10391026
@@ -1053,6 +1040,26 @@ private static boolean isClassAvailableOnClasspath(final String clazz) {
10531040 private record CurrentTest (ITestResult source , String uuid ) {
10541041 }
10551042
1043+ /**
1044+ * The identity of a per-class scope: the test context and real class plus the specific instance its class
1045+ * fixtures and tests run on. The instance is compared by reference: test classes may override equals, and
1046+ * factory-created instances built from equal parameters must still get separate scopes.
1047+ */
1048+ private record ClassInstanceKey (ITestContext context , Class <?> realClass , Object instance ) {
1049+ @ Override
1050+ public boolean equals (final Object other ) {
1051+ return other instanceof ClassInstanceKey key
1052+ && Objects .equals (context , key .context )
1053+ && Objects .equals (realClass , key .realClass )
1054+ && instance == key .instance ;
1055+ }
1056+
1057+ @ Override
1058+ public int hashCode () {
1059+ return Objects .hash (context , realClass ) * 31 + System .identityHashCode (instance );
1060+ }
1061+ }
1062+
10561063 /**
10571064 * The identity of a group fixture scope: the test context the group configuration methods run in, plus the set
10581065 * of group names they declare.
0 commit comments