@@ -406,6 +406,179 @@ public void testReapplyByIdAndBytesOverflowRollback() throws Exception {
406406 "overflowed slot must read null — the entry never landed in attrs_data" );
407407 }
408408
409+ // -----------------------------------------------------------------------
410+ // Acceptance tests for the MAX_CUSTOM_SLOTS guard fixes
411+ // -----------------------------------------------------------------------
412+
413+ /**
414+ * Test 1: setContextValuesByIdAndBytes must return false immediately when
415+ * the arrays are longer than MAX_CUSTOM_SLOTS (10), and must not perform
416+ * any partial write before the rejection.
417+ */
418+ @ Test
419+ public void testSetContextValuesByIdAndBytesRejectsArraysLongerThanMaxSlots () throws Exception {
420+ Assumptions .assumeTrue (!Platform .isJ9 ());
421+ registerCurrentThreadForWallClockProfiling ();
422+ ContextSetter contextSetter = new ContextSetter (profiler , Arrays .asList ("tag1" , "tag2" ));
423+ int slot = contextSetter .offsetOf ("tag1" );
424+
425+ // Establish a known value and capture its constant ID.
426+ assertTrue (contextSetter .setContextValue ("tag1" , "original" ));
427+ int savedId = contextSetter .snapshotTags ()[slot ];
428+ assertNotEquals (0 , savedId );
429+
430+ // Build arrays of length 11 (> MAX_CUSTOM_SLOTS = 10).
431+ int [] ids = new int [11 ];
432+ byte [][] utf8 = new byte [11 ][];
433+ ids [0 ] = savedId ;
434+ utf8 [0 ] = "original" .getBytes (StandardCharsets .UTF_8 );
435+ // All other entries remain 0 / null.
436+
437+ // The call must be rejected outright.
438+ assertFalse (contextSetter .setContextValuesByIdAndBytes (ids , utf8 ),
439+ "setContextValuesByIdAndBytes must return false when array length > MAX_CUSTOM_SLOTS" );
440+
441+ // No partial write: the sidecar for slot 0 must be unchanged.
442+ assertEquals (savedId , contextSetter .snapshotTags ()[slot ],
443+ "sidecar must not be modified before the length guard fires" );
444+ }
445+
446+ /**
447+ * Test 2: setContextValuesByIdAndBytes must accept arrays of exactly
448+ * MAX_CUSTOM_SLOTS (10) and return true, restoring all sidecar values.
449+ */
450+ @ Test
451+ public void testSetContextValuesByIdAndBytesAcceptsExactlyMaxSlots () throws Exception {
452+ Assumptions .assumeTrue (!Platform .isJ9 ());
453+ registerCurrentThreadForWallClockProfiling ();
454+ List <String > attrs = new ArrayList <>();
455+ for (int i = 1 ; i <= 10 ; i ++) {
456+ attrs .add ("tag" + i );
457+ }
458+ ContextSetter contextSetter = new ContextSetter (profiler , attrs );
459+
460+ // Set all 10 attributes to distinct values and capture constant IDs + bytes.
461+ int [] savedIds = new int [10 ];
462+ byte [][] savedBytes = new byte [10 ][];
463+ for (int i = 0 ; i < 10 ; i ++) {
464+ String value = "val" + i ;
465+ assertTrue (contextSetter .setContextValue ("tag" + (i + 1 ), value ));
466+ savedBytes [i ] = value .getBytes (StandardCharsets .UTF_8 );
467+ }
468+ int [] snapshot = contextSetter .snapshotTags ();
469+ for (int i = 0 ; i < 10 ; i ++) {
470+ savedIds [i ] = snapshot [i ];
471+ assertNotEquals (0 , savedIds [i ], "tag" + (i + 1 ) + " must have a non-zero sidecar ID" );
472+ }
473+
474+ // Wipe all slots via setContext (span activation).
475+ profiler .setContext (1L , 42L , 0L , 42L );
476+
477+ // Reapply with exactly-10-element arrays — must succeed.
478+ assertTrue (contextSetter .setContextValuesByIdAndBytes (savedIds , savedBytes ),
479+ "setContextValuesByIdAndBytes must return true for arrays of length == MAX_CUSTOM_SLOTS" );
480+
481+ // All 10 sidecar IDs must be restored.
482+ int [] restored = contextSetter .snapshotTags ();
483+ for (int i = 0 ; i < 10 ; i ++) {
484+ assertEquals (savedIds [i ], restored [i ],
485+ "sidecar for tag" + (i + 1 ) + " must be restored after reapply" );
486+ }
487+ }
488+
489+ /**
490+ * Test 3: snapshotTags(int[]) with an oversized buffer (length > attributes.size())
491+ * must write the managed indices [0, attributes.size()) with the current sidecar values,
492+ * and zero out the extra indices [attributes.size(), snapshot.length).
493+ */
494+ @ Test
495+ public void testSnapshotTagsOversizedBufferCopiesAndZerosExtras () throws Exception {
496+ Assumptions .assumeTrue (!Platform .isJ9 ());
497+ registerCurrentThreadForWallClockProfiling ();
498+ ContextSetter contextSetter = new ContextSetter (profiler , Arrays .asList ("tag1" , "tag2" ));
499+
500+ assertTrue (contextSetter .setContextValue ("tag1" , "v1" ));
501+ assertTrue (contextSetter .setContextValue ("tag2" , "v2" ));
502+
503+ // Verify no-arg overload returns valid IDs.
504+ int [] canonical = contextSetter .snapshotTags ();
505+ assertNotEquals (0 , canonical [0 ]);
506+ assertNotEquals (0 , canonical [1 ]);
507+
508+ // Oversized buffer: length 5 > attributes.size() == 2.
509+ int [] oversized = new int [5 ];
510+ Arrays .fill (oversized , -1 );
511+ contextSetter .snapshotTags (oversized );
512+
513+ // Managed indices [0, attributes.size()) must contain the current sidecar values.
514+ assertEquals (canonical [0 ], oversized [0 ],
515+ "oversized buffer[0] must match no-arg snapshotTags()[0]" );
516+ assertEquals (canonical [1 ], oversized [1 ],
517+ "oversized buffer[1] must match no-arg snapshotTags()[1]" );
518+
519+ // Extra indices [attributes.size(), snapshot.length) must be zeroed.
520+ for (int i = 2 ; i < oversized .length ; i ++) {
521+ assertEquals (0 , oversized [i ],
522+ "oversized buffer element [" + i + "] must be zeroed by snapshotTags" );
523+ }
524+
525+ // No-arg overload must still work correctly.
526+ int [] check = contextSetter .snapshotTags ();
527+ assertEquals (canonical [0 ], check [0 ]);
528+ assertEquals (canonical [1 ], check [1 ]);
529+ }
530+
531+ /**
532+ * Test 4: snapshotTags(int[]) with an undersized buffer (length < attributes.size())
533+ * must be a no-op — existing no-op semantics must be preserved.
534+ */
535+ @ Test
536+ public void testSnapshotTagsUndersizedBufferIsNoOp () throws Exception {
537+ Assumptions .assumeTrue (!Platform .isJ9 ());
538+ registerCurrentThreadForWallClockProfiling ();
539+ ContextSetter contextSetter = new ContextSetter (profiler , Arrays .asList ("tag1" , "tag2" , "tag3" ));
540+
541+ assertTrue (contextSetter .setContextValue ("tag1" , "a" ));
542+ assertTrue (contextSetter .setContextValue ("tag2" , "b" ));
543+ assertTrue (contextSetter .setContextValue ("tag3" , "c" ));
544+
545+ // Undersized buffer: length 1 < attributes.size() == 3.
546+ int [] undersized = new int [1 ];
547+ undersized [0 ] = -1 ;
548+ contextSetter .snapshotTags (undersized );
549+
550+ assertEquals (-1 , undersized [0 ],
551+ "undersized buffer must not be written by snapshotTags" );
552+ }
553+
554+ /**
555+ * Test 5: snapshotTags(int[]) with an exact-size buffer (length == attributes.size())
556+ * must copy the current sidecar values correctly.
557+ */
558+ @ Test
559+ public void testSnapshotTagsExactSizeBufferCopiesCorrectly () throws Exception {
560+ Assumptions .assumeTrue (!Platform .isJ9 ());
561+ registerCurrentThreadForWallClockProfiling ();
562+ ContextSetter contextSetter = new ContextSetter (profiler , Arrays .asList ("tag1" , "tag2" ));
563+
564+ assertTrue (contextSetter .setContextValue ("tag1" , "x" ));
565+ assertTrue (contextSetter .setContextValue ("tag2" , "y" ));
566+
567+ // No-arg overload to obtain expected values.
568+ int [] canonical = contextSetter .snapshotTags ();
569+ assertNotEquals (0 , canonical [0 ]);
570+ assertNotEquals (0 , canonical [1 ]);
571+
572+ // Exact-size buffer: length 2 == attributes.size() == 2.
573+ int [] exact = new int [2 ];
574+ contextSetter .snapshotTags (exact );
575+
576+ assertEquals (canonical [0 ], exact [0 ],
577+ "exact-size buffer[0] must match no-arg snapshotTags()[0]" );
578+ assertEquals (canonical [1 ], exact [1 ],
579+ "exact-size buffer[1] must match no-arg snapshotTags()[1]" );
580+ }
581+
409582 private void work (ContextSetter contextSetter , String contextAttribute , String contextValue )
410583 throws InterruptedException {
411584 assertTrue (contextSetter .setContextValue (contextAttribute , contextValue ));
0 commit comments