@@ -1440,3 +1440,49 @@ TEST_CASE("TestCombinedGetter")
14401440 ++count;
14411441 }
14421442}
1443+
1444+ TEST_CASE (" TestWritingCursorLastIndexAndReserve" )
1445+ {
1446+ // Nails down the WritingCursor semantics the AOD-producer reserves depend on:
1447+ // lastIndex() returns the *last index* (rows - 1), not the row count, and
1448+ // reserve(newRows + lastIndex() + 1) reserves exactly the post-batch total so a
1449+ // fully-filled, no-skip batch neither overruns (the fwdTrkCls crash) nor trips
1450+ // the release() / per-row UnsafeAppend guard.
1451+ Produces<o2::aod::Points> cursor; // Points has two persistent columns: X, Y
1452+ auto * builder = new TableBuilder ();
1453+ cursor.resetCursor (LifetimeHolder<TableBuilder>(builder));
1454+
1455+ // Empty cursor: no row written, so the last index is -1 and rows == lastIndex()+1 == 0.
1456+ REQUIRE (cursor.lastIndex () == -1 );
1457+
1458+ // operator() increments before the append, but only to the index of the row it
1459+ // writes: after N writes lastIndex() == N - 1, NOT N.
1460+ cursor (10 , 20 );
1461+ REQUIRE (cursor.lastIndex () == 0 );
1462+ cursor (11 , 21 );
1463+ REQUIRE (cursor.lastIndex () == 1 );
1464+ cursor (12 , 22 );
1465+ REQUIRE (cursor.lastIndex () == 2 );
1466+ REQUIRE (cursor.lastIndex () + 1 == 3 ); // rows-so-far == last index + 1
1467+
1468+ // Reserve a second batch the correct way: total = newRows + rowsSoFar
1469+ // = newRows + (lastIndex() + 1).
1470+ // The (buggy) newRows + lastIndex() would reserve 4 here and under-reserve the
1471+ // 5th row; the + 1 makes it exactly 5.
1472+ int64_t const newRows = 2 ;
1473+ int64_t const reserved = newRows + cursor.lastIndex () + 1 ; // correct total -> reserve(5)
1474+ cursor.reserve (reserved);
1475+ cursor (13 , 23 ); // row index 3
1476+ cursor (14 , 24 ); // row index 4 — fills the batch exactly (5 rows total)
1477+ REQUIRE (cursor.lastIndex () == 4 );
1478+
1479+ // The contract release() enforces: rows filled (lastIndex()+1) must not exceed
1480+ // what was reserved. Correct (+1) gives reserved == 5 -> 5 <= 5 (green); the buggy
1481+ // newRows + lastIndex() reserves only 4 -> 5 <= 4 fails (red).
1482+ REQUIRE (cursor.lastIndex () + 1 <= reserved);
1483+
1484+ auto table = builder->finalize ();
1485+ REQUIRE (table->num_rows () == 5 );
1486+ REQUIRE (table->num_columns () == 2 );
1487+ delete builder;
1488+ }
0 commit comments