fix(test): bound native fixture and acknowledgement lifetimes
Release / release (push) Failing after 15m44s
Release / release (push) Failing after 15m44s
This commit is contained in:
1 parent
8b14eddf43
commit
e54896cb38
3 files changed
+141
-9
No files matched your search
@@ -1630,6 +1630,9 @@ class FantasyStickLifecycleNativeTest {
|
||||
HandlerList.unregisterAll(plugin); plugin.getLogger().removeHandler(logHandler);
|
||||
loader.close(); configScope.close(); platform.close(); protocolScope.close();
|
||||
}
|
||||
// Only after successful shutdown/drain and scoped cleanup: Mockito's strong
|
||||
// stubbing value can retain the fixture (and its own weak-key protocol mock).
|
||||
org.mockito.Mockito.framework().clearInlineMock(protocol);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
/** Approved US-011 verification enabler: dispose this fixture's mock, not global native/mock state. */
|
||||
class FixtureDisposalNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap()throws Exception{NativeRuntime.bootstrap();}
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
void ownedCleanupRemainsPendingUntilRealCampOutboxWorkerFinishes()throws Exception{
|
||||
try(var fixture=new FantasyStickLifecycleNativeTest.Fixture(data)){
|
||||
fixture.plugin.setEnabled(true);
|
||||
var bridge=FantasyStickLifecycleNativeTest.Fixture.field(fixture.plugin,"campAuditEvents");
|
||||
var outbox=FantasyStickLifecycleNativeTest.Fixture.field(bridge,"outbox");
|
||||
var worker=(java.util.concurrent.ExecutorService)FantasyStickLifecycleNativeTest.Fixture.field(outbox,"worker");
|
||||
var entered=new java.util.concurrent.CountDownLatch(1);var release=new java.util.concurrent.CountDownLatch(1);
|
||||
worker.execute(()->{entered.countDown();try{assertTrue(release.await(4,java.util.concurrent.TimeUnit.SECONDS));}
|
||||
catch(InterruptedException interrupted){Thread.currentThread().interrupt();throw new AssertionError(interrupted);}});
|
||||
assertTrue(entered.await(2,java.util.concurrent.TimeUnit.SECONDS));
|
||||
try{
|
||||
fixture.plugin.setEnabled(false);fixture.awaitWorkers();
|
||||
assertFalse(ownedCleanup(fixture).toCompletableFuture().isDone(),
|
||||
"A terminated authority worker is not acknowledgement of a still-running fixture-owned camp outbox");
|
||||
}finally{release.countDown();ownedCleanup(fixture).toCompletableFuture().get(5,java.util.concurrent.TimeUnit.SECONDS);}
|
||||
}
|
||||
}
|
||||
|
||||
static java.util.concurrent.CompletionStage<Void> ownedCleanup(FantasyStickLifecycleNativeTest.Fixture fixture){
|
||||
var stages=new java.util.ArrayList<java.util.concurrent.CompletableFuture<?>>();
|
||||
for(var field:fixture.plugin.getClass().getDeclaredFields()){
|
||||
if(!field.getName().endsWith("Drain")||!java.util.concurrent.CompletionStage.class.isAssignableFrom(field.getType()))continue;
|
||||
field.setAccessible(true);
|
||||
try{
|
||||
var stage=(java.util.concurrent.CompletionStage<?>)field.get(fixture.plugin);
|
||||
assertNotNull(stage,"Existing fixture-owned drain must be initialized: "+field.getName());stages.add(stage.toCompletableFuture());
|
||||
}catch(IllegalAccessException failure){throw new AssertionError("Could not capture existing fixture-owned drain",failure);}
|
||||
}
|
||||
assertFalse(stages.isEmpty(),"Actual plugin must expose its owned cleanup stages");
|
||||
return java.util.concurrent.CompletableFuture.allOf(stages.toArray(java.util.concurrent.CompletableFuture<?>[]::new));
|
||||
}
|
||||
|
||||
@ParameterizedTest @ValueSource(strings={"normal","ProtocolPresentation","presentation-timer"})
|
||||
void disposedFixtureReleasesOnlyOwnedProtocolTrackingAfterRealCleanupAndPreservesUnrelatedMocks(String stage)throws Exception{
|
||||
var unrelated=mock(java.util.concurrent.ExecutorService.class);when(unrelated.isShutdown()).thenReturn(false);
|
||||
var fixture=new FantasyStickLifecycleNativeTest.Fixture(data);var owned=fixture.protocol;
|
||||
try(fixture){
|
||||
if(stage.equals("normal")){
|
||||
fixture.plugin.setEnabled(true);assertTrue(fixture.plugin.isEnabled());
|
||||
verify(owned).addPacketListener(fixture.packetListener);
|
||||
assertDoesNotThrow(owned::getMinecraftVersion,"Live fixture mock tracking must remain usable for real native verification");
|
||||
fixture.plugin.setEnabled(false);fixture.awaitWorkers();
|
||||
}else{
|
||||
fixture.failRegistrationAt=stage;assertThrows(IllegalStateException.class,()->fixture.plugin.setEnabled(true));
|
||||
assertDoesNotThrow(owned::getMinecraftVersion,"Partial-enable cleanup verification must run before disposal");
|
||||
}
|
||||
verify(owned).removePacketListener(fixture.packetListener);
|
||||
assertNotNull(mockingDetails(owned).getMockHandler().getInvocationContainer(),
|
||||
"In-scope listener verification retains this fixture's invocation state until disposal");
|
||||
assertFalse(java.util.Arrays.stream(org.bukkit.event.inventory.CraftItemEvent.getHandlerList().getRegisteredListeners()).anyMatch(listener->listener.getPlugin()==fixture.plugin));
|
||||
assertFalse(unrelated.isShutdown());
|
||||
ownedCleanup(fixture).toCompletableFuture().get(5,java.util.concurrent.TimeUnit.SECONDS);
|
||||
}
|
||||
assertTrue(mockingDetails(unrelated).isMock());assertFalse(unrelated.isShutdown());verify(unrelated,times(2)).isShutdown();
|
||||
assertNotNull(mockingDetails(unrelated).getMockHandler().getInvocationContainer());
|
||||
assertThrows(org.mockito.exceptions.misusing.DisabledMockException.class,
|
||||
()->mockingDetails(owned).getMockHandler(),
|
||||
"Disposed fixture-owned tracked handler must no longer expose the invocation/stubbing state retaining its closed plugin");
|
||||
}
|
||||
}
|
||||
@@ -82,19 +82,19 @@ class MagicCoreEventsNativeTest {
|
||||
rare.buy(a.player,shop.id(),market.current().toCompletableFuture().join().revision(),RareProduct.MAGIC_CORE,noStock::set);
|
||||
ReskillEventsNativeTest.await(f,()->noStock.get()!=null);assertEquals(RareShop.Status.SOLD_OUT,noStock.get().status());
|
||||
useCore(f,a,initial.add(BigInteger.ONE));
|
||||
PartyBookNativeTest.open(f,a);PartyBookNativeTest.click(f,a,PartyBookNativeTest.button(a,"[+1 STR]"));
|
||||
openAcknowledgedBook(f,a);PartyBookNativeTest.click(f,a,PartyBookNativeTest.button(a,"[+1 STR]"));
|
||||
ReskillEventsNativeTest.await(f,()->current(f,a).stats().get(Stat.STR).equals(BigInteger.valueOf(13)));
|
||||
useCore(f,a,initial.add(BigInteger.ONE));
|
||||
PartyBookNativeTest.open(f,a);PartyBookNativeTest.click(f,a,button(a,"[Learn]","great_sword"));
|
||||
openAcknowledgedBook(f,a);PartyBookNativeTest.click(f,a,button(a,"[Learn]","great_sword"));
|
||||
ReskillEventsNativeTest.await(f,()->current(f,a).learned().contains("great_sword"));
|
||||
useCore(f,a,initial.add(BigInteger.ONE));
|
||||
PartyBookNativeTest.open(f,a);PartyBookNativeTest.click(f,a,button(a,"[Learn]","quicker_regeneration_i"));
|
||||
openAcknowledgedBook(f,a);PartyBookNativeTest.click(f,a,button(a,"[Learn]","quicker_regeneration_i"));
|
||||
ReskillEventsNativeTest.await(f,()->current(f,a).passives().contains("quicker_regeneration_i"));
|
||||
useCore(f,a,initial.add(BigInteger.ONE));
|
||||
buy(f,a,shop.id(),13);
|
||||
ReskillEventsNativeTest.await(f,()->inventory.quantity(character,RareProduct.RESKILL).equals(BigInteger.ONE)
|
||||
&¤t(f,a).coins().signum()==0);
|
||||
PartyBookNativeTest.open(f,a);PartyBookNativeTest.click(f,a,button(a,"[Use]","reskill"));
|
||||
openAcknowledgedBook(f,a);PartyBookNativeTest.click(f,a,button(a,"[Use]","reskill"));
|
||||
ReskillEventsNativeTest.await(f,()->PartyBookNativeTest.text(a).contains("Confirm Reskill"));
|
||||
PartyBookNativeTest.click(f,a,PartyBookNativeTest.button(a,"[Confirm Reskill]"));
|
||||
ReskillEventsNativeTest.await(f,()->inventory.quantity(character,RareProduct.RESKILL).signum()==0
|
||||
@@ -127,6 +127,32 @@ class MagicCoreEventsNativeTest {
|
||||
"Restart retains the full global catalog; Core does not emit another refresh");
|
||||
}
|
||||
|
||||
@Test void unacknowledgedCoreBookCannotReuseOldNativeButton()throws Exception{
|
||||
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
|
||||
f.plugin.setEnabled(true);var a=eligibleActor(f,BigInteger.valueOf(100));
|
||||
var inventory=(RareInventoryDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareInventory");
|
||||
inventory.add(a.core.record.id(),RareProduct.MAGIC_CORE,UUID.randomUUID()).join();
|
||||
var old=coreBookAction(f,a);int presented=a.books.size();
|
||||
var controller=FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"books");
|
||||
var scheduler=controller.getClass().getDeclaredField("nextTick");scheduler.setAccessible(true);
|
||||
Object original=scheduler.get(controller);var held=new java.util.concurrent.LinkedBlockingQueue<Runnable>();
|
||||
try{
|
||||
scheduler.set(controller,(java.util.function.Consumer<Runnable>)held::add);
|
||||
assertThrows(AssertionError.class,()->coreBookAction(f,a),
|
||||
"A requested-but-withheld native book is not acknowledged; never return the old displayed Core payload");
|
||||
assertEquals(presented,a.books.size());assertFalse(held.isEmpty());
|
||||
PartyBookNativeTest.click(f,a,old);PartyBookNativeTest.drain(f);
|
||||
assertEquals(BigInteger.valueOf(100),current(f,a).unspentPoints());
|
||||
assertEquals(BigInteger.ONE,inventory.quantity(a.core.record.id(),RareProduct.MAGIC_CORE));
|
||||
}finally{
|
||||
scheduler.set(controller,original);Runnable callback;while((callback=held.poll())!=null)callback.run();
|
||||
}
|
||||
useCore(f,a,BigInteger.valueOf(101));
|
||||
assertEquals(BigInteger.ZERO,inventory.quantity(a.core.record.id(),RareProduct.MAGIC_CORE));
|
||||
f.plugin.setEnabled(false);f.awaitWorkers();FixtureDisposalNativeTest.ownedCleanup(f).toCompletableFuture().get(5,TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test void failedPointSaveRefundsCoreAndThrowingAuditCannotDuplicateOrUndoCredit() throws Exception {
|
||||
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
|
||||
f.plugin.setEnabled(true);var a=eligibleActor(f,BigInteger.valueOf(100));
|
||||
@@ -134,7 +160,7 @@ class MagicCoreEventsNativeTest {
|
||||
inventory.add(a.core.record.id(),RareProduct.MAGIC_CORE,UUID.randomUUID()).join();
|
||||
try(var channel=java.nio.channels.FileChannel.open(data.resolve("characters/.records.lock"),StandardOpenOption.WRITE);
|
||||
var lock=channel.lock()){
|
||||
assertTrue(lock.isValid());PartyBookNativeTest.open(f,a);PartyBookNativeTest.click(f,a,button(a,"[Use]","magic-core"));
|
||||
assertTrue(lock.isValid());openAcknowledgedBook(f,a);PartyBookNativeTest.click(f,a,button(a,"[Use]","magic-core"));
|
||||
ReskillEventsNativeTest.await(f,()->a.messages.stream().anyMatch(message->message.contains("Magic Core could not be acknowledged")));
|
||||
assertEquals(BigInteger.valueOf(100),current(f,a).unspentPoints());
|
||||
assertEquals(BigInteger.ONE,inventory.quantity(a.core.record.id(),RareProduct.MAGIC_CORE));assertTrue(credits().isEmpty());
|
||||
@@ -168,16 +194,42 @@ class MagicCoreEventsNativeTest {
|
||||
return sessions.currentRecord(sessions.active(a.core.id).orElseThrow()).orElseThrow();
|
||||
}
|
||||
private void useCore(FantasyStickLifecycleNativeTest.Fixture f,FantasyStickLifecycleNativeTest.Actor a,BigInteger expected) throws Exception {
|
||||
PartyBookNativeTest.open(f,a);var action=button(a,"[Use]","magic-core");PartyBookNativeTest.click(f,a,action);
|
||||
ReskillEventsNativeTest.await(f,()->current(f,a).unspentPoints().equals(expected));
|
||||
PartyBookNativeTest.click(f,a,action);
|
||||
var inventory=(RareInventoryDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareInventory");
|
||||
var before=current(f,a);var quantity=inventory.quantity(before.id(),RareProduct.MAGIC_CORE);int messages=a.messages.size();
|
||||
var action=coreBookAction(f,a);PartyBookNativeTest.click(f,a,action);
|
||||
awaitAcknowledgement(f,a,()->a.messages.subList(messages,a.messages.size()).stream().anyMatch(message->
|
||||
message.startsWith("Used Magic Core")||message.startsWith("Magic Core use refused")||message.startsWith("Magic Core could not")),
|
||||
"Core result callback");
|
||||
assertTrue(a.messages.subList(messages,a.messages.size()).contains("Used Magic Core and gained 1 ordinary unspent point."),
|
||||
"Fresh acknowledged Core action must report APPLIED, not a refused/stale/failed production outcome");
|
||||
assertEquals(expected,current(f,a).unspentPoints());assertEquals(before.unspentPoints().add(BigInteger.ONE),expected);
|
||||
assertEquals(quantity.subtract(BigInteger.ONE),inventory.quantity(before.id(),RareProduct.MAGIC_CORE));
|
||||
var saved=new YamlCharacterRepository(data).load(before.id()).orElseThrow();assertEquals(expected,saved.unspentPoints());
|
||||
assertEquals(before.experience(),saved.experience());assertEquals(before.level(),saved.level());
|
||||
PartyBookNativeTest.click(f,a,action);PartyBookNativeTest.drain(f);
|
||||
assertEquals(expected,current(f,a).unspentPoints());assertEquals(quantity.subtract(BigInteger.ONE),inventory.quantity(before.id(),RareProduct.MAGIC_CORE));
|
||||
}
|
||||
private static void openAcknowledgedBook(FantasyStickLifecycleNativeTest.Fixture f,FantasyStickLifecycleNativeTest.Actor a)throws Exception{
|
||||
int before=a.books.size();PartyBookNativeTest.open(f,a);
|
||||
awaitAcknowledgement(f,a,()->a.books.size()>before,"new native book presentation");
|
||||
}
|
||||
private static void awaitAcknowledgement(FantasyStickLifecycleNativeTest.Fixture f,FantasyStickLifecycleNativeTest.Actor a,
|
||||
java.util.function.BooleanSupplier acknowledged,String phase)throws Exception{
|
||||
long deadline=System.nanoTime()+TimeUnit.SECONDS.toNanos(5);
|
||||
while(!acknowledged.getAsBoolean()&&System.nanoTime()<deadline){var callback=f.tasks.poll(25,TimeUnit.MILLISECONDS);if(callback!=null)callback.run();}
|
||||
assertTrue(acknowledged.getAsBoolean(),()->phase+" not acknowledged; books="+a.books.size()+", queued callbacks="+f.tasks.size()+", messages="+a.messages.size());
|
||||
}
|
||||
private static net.minecraft.network.chat.ClickEvent.Custom coreBookAction(FantasyStickLifecycleNativeTest.Fixture f,FantasyStickLifecycleNativeTest.Actor a)throws Exception{
|
||||
openAcknowledgedBook(f,a);return button(a,"[Use]","magic-core");
|
||||
}
|
||||
private static void buy(FantasyStickLifecycleNativeTest.Fixture f,FantasyStickLifecycleNativeTest.Actor a,UUID shop,int slot) throws Exception {
|
||||
var menus=(RareShopMenus)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareShopMenus");int size=a.inventories.size();
|
||||
menus.open(a.player,shop);ReskillEventsNativeTest.await(f,()->a.inventories.size()>size);
|
||||
var menu=a.inventories.getLast();var click=mock(InventoryClickEvent.class);
|
||||
when(click.getInventory()).thenReturn(menu);when(click.getWhoClicked()).thenReturn(a.player);when(click.getCurrentItem()).thenReturn(menu.getItem(slot));
|
||||
menus.click(click);
|
||||
int messages=a.messages.size();menus.click(click);
|
||||
awaitAcknowledgement(f,a,()->a.messages.subList(messages,a.messages.size()).stream().anyMatch(message->message.startsWith("Purchased ")||message.startsWith("Rare purchase refused:")),"purchase result callback");
|
||||
assertTrue(a.messages.subList(messages,a.messages.size()).stream().anyMatch(message->message.startsWith("Purchased ")),"Exact stock/debit assertions must follow an acknowledged purchase, not a refusal");
|
||||
}
|
||||
private static net.minecraft.network.chat.ClickEvent.Custom button(FantasyStickLifecycleNativeTest.Actor a,String label,String identity){
|
||||
var content=org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(a.books.getLast())
|
||||
|
||||
Reference in new issue
Block a user