Skip to content

Commit e4073ce

Browse files
committed
test: clean up failed faction fixture construction
1 parent 9e9d2ee commit e4073ce

2 files changed

Lines changed: 187 additions & 2 deletions

File tree

‎src/test/java/net/tfminecraft/simplefactions/testsupport/FactionDomainFixture.java‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,22 @@ public final class FactionDomainFixture implements AutoCloseable {
7070
private final Map<Field, Object> originalGlobals = new LinkedHashMap<>();
7171
private final Map<Map<Object, Object>, Map<Object, Object>> originalMapContents =
7272
new java.util.IdentityHashMap<>();
73-
private final MockedStatic<BannerFetcher> banners;
73+
private MockedStatic<BannerFetcher> banners;
7474

7575
public FactionDomainFixture() {
76+
try {
77+
initialise();
78+
} catch (RuntimeException | Error failure) {
79+
try {
80+
close();
81+
} catch (RuntimeException | Error cleanupFailure) {
82+
failure.addSuppressed(cleanupFailure);
83+
}
84+
throw failure;
85+
}
86+
}
87+
88+
private void initialise() {
7689
replace(FactionManager.class, "factions", new ArrayList<Faction>());
7790
replace(FactionManager.class, "map", map);
7891
replace(FactionManager.class, "inv", inventory);
@@ -354,7 +367,7 @@ private void replace(Class<?> owner, String name, Object value) {
354367
@Override
355368
public void close() {
356369
try {
357-
banners.close();
370+
if (banners != null) banners.close();
358371
} finally {
359372
try {
360373
originalMapContents.forEach(
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package net.tfminecraft.simplefactions.testsupport;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
import static org.mockito.Mockito.mockStatic;
5+
6+
import java.lang.reflect.Field;
7+
import java.lang.reflect.Modifier;
8+
import java.time.Duration;
9+
import java.util.ArrayList;
10+
import java.util.IdentityHashMap;
11+
import java.util.LinkedHashMap;
12+
import java.util.List;
13+
import java.util.Map;
14+
import net.tfminecraft.simplefactions.Cache;
15+
import net.tfminecraft.simplefactions.SimpleFactions;
16+
import net.tfminecraft.simplefactions.identity.LeaderCharacters;
17+
import net.tfminecraft.simplefactions.inactivity.DecayClock;
18+
import net.tfminecraft.simplefactions.inactivity.InactivityService;
19+
import net.tfminecraft.simplefactions.loaders.*;
20+
import net.tfminecraft.simplefactions.managers.FactionManager;
21+
import net.tfminecraft.simplefactions.managers.LogManager;
22+
import net.tfminecraft.simplefactions.managers.WarManager;
23+
import net.tfminecraft.simplefactions.prestige.MemberPlaytime;
24+
import net.tfminecraft.simplefactions.rest.BannerFetcher;
25+
import org.bukkit.Bukkit;
26+
import org.bukkit.Material;
27+
import org.bukkit.inventory.ItemStack;
28+
import org.junit.jupiter.api.Test;
29+
import org.mockito.exceptions.base.MockitoException;
30+
31+
class FactionDomainFixtureTest {
32+
@Test
33+
void failedConstructionRestoresGlobalsAndOnlyClosesItsOwnMocks() {
34+
// Isolate thread-scoped leaks if this regression fails, without clearing other tests' mocks.
35+
assertTimeoutPreemptively(
36+
Duration.ofSeconds(30),
37+
() -> {
38+
try (Globals original = new Globals()) {
39+
Cache.worldName = "retained-world";
40+
Cache.maxMembers = 237;
41+
Cache.branchUpgradeCost = 321.0;
42+
RankLoader.ranks = new ArrayList<>(RankLoader.ranks);
43+
clocks("guildClocks").put("retained-guild", new DecayClock(10, 12345));
44+
clocks("factionClocks").put("retained-faction", new DecayClock(20, 23456));
45+
Globals expected = new Globals();
46+
List<String> externalBanner = List.of("externally-owned-banner");
47+
48+
try (var external = mockStatic(BannerFetcher.class)) {
49+
external.when(BannerFetcher::placeholder).thenReturn(externalBanner);
50+
51+
MockitoException failure =
52+
assertThrows(MockitoException.class, FactionDomainFixture::new);
53+
54+
assertTrue(failure.getMessage().contains("already registered"));
55+
assertEquals(0, failure.getSuppressed().length);
56+
assertAll(
57+
expected::assertRestored,
58+
() -> assertFalse(external.isClosed()),
59+
() -> assertSame(externalBanner, BannerFetcher.placeholder()),
60+
() -> {
61+
try (var freshUi = new GuiTestFixture()) {
62+
assertSame(freshUi.world, Bukkit.getWorld("world"));
63+
assertEquals(Material.PAPER, new ItemStack(Material.PAPER).getType());
64+
}
65+
});
66+
}
67+
68+
try (var fresh = new FactionDomainFixture()) {
69+
assertSame(fresh.ui.plugin, SimpleFactions.plugin);
70+
assertEquals(List.of("white"), BannerFetcher.placeholder());
71+
assertEquals(
72+
List.of("common", "renowned"),
73+
RankLoader.ranks.stream().map(rank -> rank.getId()).toList());
74+
}
75+
expected.assertRestored();
76+
}
77+
});
78+
}
79+
80+
@SuppressWarnings("unchecked")
81+
private static Map<String, DecayClock> clocks(String name) throws ReflectiveOperationException {
82+
Field field = InactivityService.class.getDeclaredField(name);
83+
field.setAccessible(true);
84+
return (Map<String, DecayClock>) field.get(null);
85+
}
86+
87+
/** Snapshot only the named globals that the fixture owns; never invoke methods reflectively. */
88+
private static final class Globals implements AutoCloseable {
89+
private final Map<Field, Object> values = new LinkedHashMap<>();
90+
private final Map<Map<Object, Object>, Map<Object, Object>> contents = new IdentityHashMap<>();
91+
92+
private Globals() throws ReflectiveOperationException {
93+
capture(SimpleFactions.class, "plugin");
94+
capture(FactionManager.class, "factions", "map", "inv", "loading");
95+
capture(RankLoader.class, "ranks");
96+
capture(TierLoader.class, "oList");
97+
capture(TitleLoader.class, "titles");
98+
capture(GuildLoader.class, "map");
99+
capture(BranchLoader.class, "map");
100+
capture(UpgradeLoader.class, "map");
101+
capture(LawLoader.class, "map");
102+
capture(RegimentLoader.class, "oList");
103+
capture(RelationLoader.class, "types", "attitudes");
104+
capture(WarManager.class, "wars");
105+
capture(MemberPlaytime.class, "probe");
106+
capture(LeaderCharacters.class, "probe");
107+
capture(
108+
InactivityService.class,
109+
"loaded",
110+
"saveBlocked",
111+
"guildClocks",
112+
"factionClocks",
113+
"earliestDue");
114+
capture(LogManager.class, "enabled");
115+
capture(
116+
Cache.class,
117+
"worldName",
118+
"provincesEnabled",
119+
"branchUpgradeCost",
120+
"branchUpgradeExponent",
121+
"maxWealthPrestige",
122+
"maxMembers",
123+
"maxExtraNodeCapacity",
124+
"maxUntitledProvinces",
125+
"maxFreeTitles",
126+
"baseEffects");
127+
}
128+
129+
@SuppressWarnings("unchecked")
130+
private void capture(Class<?> owner, String... names) throws ReflectiveOperationException {
131+
for (String name : names) {
132+
Field field = owner.getDeclaredField(name);
133+
field.setAccessible(true);
134+
Object value = field.get(null);
135+
values.put(field, value);
136+
if (value instanceof Map<?, ?> map) {
137+
contents.put((Map<Object, Object>) map, new LinkedHashMap<>((Map<Object, Object>) map));
138+
}
139+
}
140+
}
141+
142+
private void assertRestored() {
143+
assertAll(
144+
values.entrySet().stream()
145+
.map(
146+
entry ->
147+
() -> {
148+
Field field = entry.getKey();
149+
if (field.getType().isPrimitive()) {
150+
assertEquals(entry.getValue(), field.get(null), field.toString());
151+
} else {
152+
assertSame(entry.getValue(), field.get(null), field.toString());
153+
}
154+
}));
155+
contents.forEach((map, saved) -> assertEquals(saved, map));
156+
}
157+
158+
@Override
159+
public void close() throws ReflectiveOperationException {
160+
contents.forEach(
161+
(map, saved) -> {
162+
map.clear();
163+
map.putAll(saved);
164+
});
165+
for (var entry : values.entrySet()) {
166+
if (!Modifier.isFinal(entry.getKey().getModifiers())) {
167+
entry.getKey().set(null, entry.getValue());
168+
}
169+
}
170+
}
171+
}
172+
}

0 commit comments

Comments
 (0)