diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 43cae2f..e9941ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: Build GTNH Mod on: push: branches: [ "**" ] - tags: [ "v*" ] + tags: [ "v*", "*test*" ] pull_request: branches: [ master, main ] @@ -11,7 +11,7 @@ jobs: build: runs-on: ubuntu-latest permissions: - contents: write + contents: read pull-requests: read steps: @@ -37,10 +37,96 @@ jobs: path: build/libs/*.jar if-no-files-found: error - - name: Create Release - if: startsWith(github.ref, 'refs/tags/v') + release-stable: + needs: build + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: mod-jar + path: build/libs + + - name: Create stable release uses: softprops/action-gh-release@v2 with: files: | build/libs/*[!s].jar generate_release_notes: true + + release-test: + needs: build + if: startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'test') + runs-on: ubuntu-latest + permissions: + contents: write + concurrency: + group: test-prerelease-release + cancel-in-progress: true + + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: mod-jar + path: build/libs + + - name: Delete previous rolling test prerelease + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const rollingTag = 'test-latest'; + + try { + const release = await github.rest.repos.getReleaseByTag({ + owner, + repo, + tag: rollingTag, + }); + + core.info(`Deleting existing release ${release.data.id} for ${rollingTag}`); + await github.rest.repos.deleteRelease({ + owner, + repo, + release_id: release.data.id, + }); + } catch (error) { + if (error.status !== 404) { + throw error; + } + core.info(`No existing release found for ${rollingTag}`); + } + + try { + await github.rest.git.deleteRef({ + owner, + repo, + ref: `tags/${rollingTag}`, + }); + core.info(`Deleted existing ref tags/${rollingTag}`); + } catch (error) { + if (error.status !== 404 && error.status !== 422) { + throw error; + } + core.info(`No existing ref to delete for tags/${rollingTag}`); + } + + - name: Create rolling test prerelease + uses: softprops/action-gh-release@v2 + with: + tag_name: test-latest + name: Test Build + prerelease: true + target_commitish: ${{ github.sha }} + body: | + Rolling prerelease updated from source tag `${{ github.ref_name }}`. + + Commit: `${{ github.sha }}` + files: | + build/libs/*[!s].jar diff --git a/src/main/java/com/github/ae2patterngen/filter/BlacklistFilter.java b/src/main/java/com/github/ae2patterngen/filter/BlacklistFilter.java index bb0f6f3..b69c113 100644 --- a/src/main/java/com/github/ae2patterngen/filter/BlacklistFilter.java +++ b/src/main/java/com/github/ae2patterngen/filter/BlacklistFilter.java @@ -15,10 +15,15 @@ public class BlacklistFilter implements IRecipeFilter { private final ExplicitStackMatcher matcher; public BlacklistFilter(String keyword, boolean checkInputs, boolean checkOutputs) { + this(keyword, checkInputs, checkOutputs, new ExplicitStackMatcher.StackMatchCache()); + } + + BlacklistFilter(String keyword, boolean checkInputs, boolean checkOutputs, + ExplicitStackMatcher.StackMatchCache stackMatchCache) { this.keyword = keyword; this.checkInputs = checkInputs; this.checkOutputs = checkOutputs; - this.matcher = new ExplicitStackMatcher(keyword); + this.matcher = new ExplicitStackMatcher(keyword, stackMatchCache); } @Override diff --git a/src/main/java/com/github/ae2patterngen/filter/ExplicitStackMatcher.java b/src/main/java/com/github/ae2patterngen/filter/ExplicitStackMatcher.java index ffca309..2fd9d6f 100644 --- a/src/main/java/com/github/ae2patterngen/filter/ExplicitStackMatcher.java +++ b/src/main/java/com/github/ae2patterngen/filter/ExplicitStackMatcher.java @@ -4,6 +4,7 @@ import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; +import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -32,24 +33,29 @@ public class ExplicitStackMatcher { private final String source; private final boolean disabled; private final boolean invalid; - private final ParsedRules parsedRules; - private final IdentityHashMap stackCache = new IdentityHashMap(); + private final MatchPlan matchPlan; + private final StackMatchCache stackCache; public ExplicitStackMatcher(String source) { + this(source, new StackMatchCache()); + } + + ExplicitStackMatcher(String source, StackMatchCache stackCache) { this.source = source; + this.stackCache = stackCache != null ? stackCache : new StackMatchCache(); String normalized = source != null ? source.trim() : ""; if (normalized.isEmpty() || "*".equals(normalized)) { this.disabled = true; this.invalid = false; - this.parsedRules = ParsedRules.empty(); + this.matchPlan = MatchPlan.NO_RULES; return; } ParsedRules rules = parseRules(normalized); this.disabled = false; this.invalid = !rules.hasRules(); - this.parsedRules = rules; + this.matchPlan = this.invalid ? MatchPlan.NO_RULES : rules.toMatchPlan(); } public boolean isDisabled() { @@ -65,13 +71,21 @@ public boolean matches(ItemStack stack) { return false; } - StackMatchData data = stackCache.get(stack); - if (data == null) { - data = new StackMatchData(stack); - stackCache.put(stack, data); + Item item = stack.getItem(); + if (item == null) { + return false; } - return parsedRules.matches(data); + final int itemId = Item.getIdFromItem(item); + final int meta = stack.getItemDamage(); + + return matches( + itemId, + meta, + () -> stackCache.getOrCreate(stack) + .getOreNames(), + () -> stackCache.getOrCreate(stack) + .getDisplayName()); } boolean matches(String displayName, int itemId, int meta, String[] oreNames) { @@ -79,7 +93,14 @@ boolean matches(String displayName, int itemId, int meta, String[] oreNames) { return false; } - return parsedRules.matches(new RawMatchData(displayName, itemId, meta, oreNames)); + return matches(itemId, meta, () -> oreNames, () -> displayName); + } + + boolean matches(int itemId, int meta, Supplier oreNamesSupplier, Supplier displayNameSupplier) { + if (disabled || invalid) { + return false; + } + return matchPlan.matches(itemId, meta, oreNamesSupplier, displayNameSupplier); } String getSource() { @@ -226,53 +247,21 @@ private static ParseResult parseDelimitedToken(String source, int startIndex, ch return ParseResult.invalid(); } - private interface MatchData { - - int getItemId(); - - int getMeta(); + static final class StackMatchCache { - String[] getOreNames(); - - String getDisplayName(); - } - - private static final class RawMatchData implements MatchData { - - private final String displayName; - private final int itemId; - private final int meta; - private final String[] oreNames; - - private RawMatchData(String displayName, int itemId, int meta, String[] oreNames) { - this.displayName = displayName != null ? displayName : ""; - this.itemId = itemId; - this.meta = meta; - this.oreNames = oreNames != null ? oreNames : EMPTY_STRINGS; - } + private final IdentityHashMap stackCache = new IdentityHashMap(); - @Override - public int getItemId() { - return itemId; - } - - @Override - public int getMeta() { - return meta; - } - - @Override - public String[] getOreNames() { - return oreNames; - } - - @Override - public String getDisplayName() { - return displayName; + private StackMatchData getOrCreate(ItemStack stack) { + StackMatchData data = stackCache.get(stack); + if (data == null) { + data = new StackMatchData(stack); + stackCache.put(stack, data); + } + return data; } } - private static final class StackMatchData implements MatchData { + private static final class StackMatchData { private final ItemStack stack; private final int itemId; @@ -295,17 +284,14 @@ private StackMatchData(ItemStack stack) { } } - @Override public int getItemId() { return itemId; } - @Override public int getMeta() { return meta; } - @Override public String[] getOreNames() { if (!oreNamesLoaded) { oreNames = OreDictUtil.getOreNamesSafe(stack); @@ -314,7 +300,6 @@ public String[] getOreNames() { return oreNames != null ? oreNames : EMPTY_STRINGS; } - @Override public String getDisplayName() { if (!displayNameLoaded) { displayName = ItemStackUtil.getSafeDisplayName(stack); @@ -349,38 +334,21 @@ private boolean hasRules() { return !(idRules.isEmpty() && oreDictRules.isEmpty() && displayNameRules.isEmpty()); } - private boolean matches(MatchData data) { - if (data == null || !hasRules()) { - return false; - } + private MatchPlan toMatchPlan() { + boolean hasId = !idRules.isEmpty(); + boolean hasOre = !oreDictRules.isEmpty(); + boolean hasDisplay = !displayNameRules.isEmpty(); - int itemId = data.getItemId(); - int meta = data.getMeta(); - for (IdRule rule : idRules) { - if (rule.matches(itemId, meta)) { - return true; - } + if (hasId && !hasOre && !hasDisplay) { + return new IdOnlyMatchPlan(idRules); } - - if (!oreDictRules.isEmpty()) { - String[] oreNames = data.getOreNames(); - for (RegexRule rule : oreDictRules) { - if (rule.matchesAny(oreNames)) { - return true; - } - } + if (!hasId && hasOre && !hasDisplay) { + return new OreOnlyMatchPlan(oreDictRules); } - - if (!displayNameRules.isEmpty()) { - String displayName = data.getDisplayName(); - for (RegexRule rule : displayNameRules) { - if (rule.matches(displayName)) { - return true; - } - } + if (!hasId && !hasOre && hasDisplay) { + return new DisplayOnlyMatchPlan(displayNameRules); } - - return false; + return new MixedMatchPlan(idRules, oreDictRules, displayNameRules); } } @@ -432,6 +400,147 @@ private boolean matchesAny(String[] candidates) { } } + private interface MatchPlan { + + MatchPlan NO_RULES = new MatchPlan() { + + @Override + public boolean matches(int itemId, int meta, Supplier oreNamesSupplier, + Supplier displayNameSupplier) { + return false; + } + }; + + boolean matches(int itemId, int meta, Supplier oreNamesSupplier, + Supplier displayNameSupplier); + } + + private abstract static class BaseMatchPlan implements MatchPlan { + + protected String[] getOreNames(Supplier oreNamesSupplier) { + if (oreNamesSupplier == null) { + return EMPTY_STRINGS; + } + + String[] oreNames = oreNamesSupplier.get(); + return oreNames != null ? oreNames : EMPTY_STRINGS; + } + + protected String getDisplayName(Supplier displayNameSupplier) { + if (displayNameSupplier == null) { + return ""; + } + + String displayName = displayNameSupplier.get(); + return displayName != null ? displayName : ""; + } + } + + private static final class IdOnlyMatchPlan extends BaseMatchPlan { + + private final List idRules; + + private IdOnlyMatchPlan(List idRules) { + this.idRules = idRules; + } + + @Override + public boolean matches(int itemId, int meta, Supplier oreNamesSupplier, + Supplier displayNameSupplier) { + for (IdRule rule : idRules) { + if (rule.matches(itemId, meta)) { + return true; + } + } + return false; + } + } + + private static final class OreOnlyMatchPlan extends BaseMatchPlan { + + private final List oreDictRules; + + private OreOnlyMatchPlan(List oreDictRules) { + this.oreDictRules = oreDictRules; + } + + @Override + public boolean matches(int itemId, int meta, Supplier oreNamesSupplier, + Supplier displayNameSupplier) { + String[] oreNames = getOreNames(oreNamesSupplier); + for (RegexRule rule : oreDictRules) { + if (rule.matchesAny(oreNames)) { + return true; + } + } + return false; + } + } + + private static final class DisplayOnlyMatchPlan extends BaseMatchPlan { + + private final List displayNameRules; + + private DisplayOnlyMatchPlan(List displayNameRules) { + this.displayNameRules = displayNameRules; + } + + @Override + public boolean matches(int itemId, int meta, Supplier oreNamesSupplier, + Supplier displayNameSupplier) { + String displayName = getDisplayName(displayNameSupplier); + for (RegexRule rule : displayNameRules) { + if (rule.matches(displayName)) { + return true; + } + } + return false; + } + } + + private static final class MixedMatchPlan extends BaseMatchPlan { + + private final List idRules; + private final List oreDictRules; + private final List displayNameRules; + + private MixedMatchPlan(List idRules, List oreDictRules, List displayNameRules) { + this.idRules = idRules; + this.oreDictRules = oreDictRules; + this.displayNameRules = displayNameRules; + } + + @Override + public boolean matches(int itemId, int meta, Supplier oreNamesSupplier, + Supplier displayNameSupplier) { + for (IdRule rule : idRules) { + if (rule.matches(itemId, meta)) { + return true; + } + } + + if (!oreDictRules.isEmpty()) { + String[] oreNames = getOreNames(oreNamesSupplier); + for (RegexRule rule : oreDictRules) { + if (rule.matchesAny(oreNames)) { + return true; + } + } + } + + if (!displayNameRules.isEmpty()) { + String displayName = getDisplayName(displayNameSupplier); + for (RegexRule rule : displayNameRules) { + if (rule.matches(displayName)) { + return true; + } + } + } + + return false; + } + } + private static final class ParseResult { private final String content; diff --git a/src/main/java/com/github/ae2patterngen/filter/InputOreDictFilter.java b/src/main/java/com/github/ae2patterngen/filter/InputOreDictFilter.java index c9e1e5f..085d559 100644 --- a/src/main/java/com/github/ae2patterngen/filter/InputOreDictFilter.java +++ b/src/main/java/com/github/ae2patterngen/filter/InputOreDictFilter.java @@ -13,8 +13,12 @@ public class InputOreDictFilter implements IRecipeFilter { private final ExplicitStackMatcher matcher; public InputOreDictFilter(String matchSource) { + this(matchSource, new ExplicitStackMatcher.StackMatchCache()); + } + + InputOreDictFilter(String matchSource, ExplicitStackMatcher.StackMatchCache stackMatchCache) { this.matchSource = matchSource; - this.matcher = new ExplicitStackMatcher(matchSource); + this.matcher = new ExplicitStackMatcher(matchSource, stackMatchCache); } @Override diff --git a/src/main/java/com/github/ae2patterngen/filter/NCItemFilter.java b/src/main/java/com/github/ae2patterngen/filter/NCItemFilter.java index 9ccf97b..b6bd4b1 100644 --- a/src/main/java/com/github/ae2patterngen/filter/NCItemFilter.java +++ b/src/main/java/com/github/ae2patterngen/filter/NCItemFilter.java @@ -13,8 +13,12 @@ public class NCItemFilter implements IRecipeFilter { private final ExplicitStackMatcher matcher; public NCItemFilter(String matchSource) { + this(matchSource, new ExplicitStackMatcher.StackMatchCache()); + } + + NCItemFilter(String matchSource, ExplicitStackMatcher.StackMatchCache stackMatchCache) { this.matchSource = matchSource; - this.matcher = new ExplicitStackMatcher(matchSource); + this.matcher = new ExplicitStackMatcher(matchSource, stackMatchCache); } @Override diff --git a/src/main/java/com/github/ae2patterngen/filter/OutputOreDictFilter.java b/src/main/java/com/github/ae2patterngen/filter/OutputOreDictFilter.java index 725cfdb..328e71d 100644 --- a/src/main/java/com/github/ae2patterngen/filter/OutputOreDictFilter.java +++ b/src/main/java/com/github/ae2patterngen/filter/OutputOreDictFilter.java @@ -13,8 +13,12 @@ public class OutputOreDictFilter implements IRecipeFilter { private final ExplicitStackMatcher matcher; public OutputOreDictFilter(String matchSource) { + this(matchSource, new ExplicitStackMatcher.StackMatchCache()); + } + + OutputOreDictFilter(String matchSource, ExplicitStackMatcher.StackMatchCache stackMatchCache) { this.matchSource = matchSource; - this.matcher = new ExplicitStackMatcher(matchSource); + this.matcher = new ExplicitStackMatcher(matchSource, stackMatchCache); } @Override diff --git a/src/main/java/com/github/ae2patterngen/filter/RecipeFilterFactory.java b/src/main/java/com/github/ae2patterngen/filter/RecipeFilterFactory.java index 1b31bc4..97f20c9 100644 --- a/src/main/java/com/github/ae2patterngen/filter/RecipeFilterFactory.java +++ b/src/main/java/com/github/ae2patterngen/filter/RecipeFilterFactory.java @@ -10,24 +10,26 @@ private RecipeFilterFactory() {} public static CompositeFilter build(String outputOreDict, String inputOreDict, String ncItem, String blacklistInput, String blacklistOutput, int targetTier) { CompositeFilter filter = new CompositeFilter(); + ExplicitStackMatcher.StackMatchCache stackMatchCache = new ExplicitStackMatcher.StackMatchCache(); + + if (targetTier >= 0) { + filter.addFilter(new TierFilter(targetTier)); + } if (isEnabled(outputOreDict)) { - filter.addFilter(new OutputOreDictFilter(outputOreDict)); + filter.addFilter(new OutputOreDictFilter(outputOreDict, stackMatchCache)); } if (isEnabled(inputOreDict)) { - filter.addFilter(new InputOreDictFilter(inputOreDict)); + filter.addFilter(new InputOreDictFilter(inputOreDict, stackMatchCache)); } if (isEnabled(ncItem)) { - filter.addFilter(new NCItemFilter(ncItem)); + filter.addFilter(new NCItemFilter(ncItem, stackMatchCache)); } if (isEnabled(blacklistInput)) { - filter.addFilter(new BlacklistFilter(blacklistInput, true, false)); + filter.addFilter(new BlacklistFilter(blacklistInput, true, false, stackMatchCache)); } if (isEnabled(blacklistOutput)) { - filter.addFilter(new BlacklistFilter(blacklistOutput, false, true)); - } - if (targetTier >= 0) { - filter.addFilter(new TierFilter(targetTier)); + filter.addFilter(new BlacklistFilter(blacklistOutput, false, true, stackMatchCache)); } return filter; diff --git a/src/main/java/com/github/ae2patterngen/recipe/GTRecipeSource.java b/src/main/java/com/github/ae2patterngen/recipe/GTRecipeSource.java index dee1b45..cf62179 100644 --- a/src/main/java/com/github/ae2patterngen/recipe/GTRecipeSource.java +++ b/src/main/java/com/github/ae2patterngen/recipe/GTRecipeSource.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -23,6 +24,8 @@ */ public class GTRecipeSource { + private static final RecipeCollectionCache> COLLECTED_RECIPE_CACHE = new RecipeCollectionCache>(); + /** * 获取所有已注册的 GT 配方表名称 * @@ -104,14 +107,20 @@ public static List findMatchingRecipeMaps(String keyword) { * @return 所有匹配配方表的 RecipeEntry 列表 */ public static List collectRecipes(String keyword) { - List entries = new ArrayList<>(); - Set processedKeys = new java.util.HashSet<>(); - List matchedMaps = findMatchingRecipeMaps(keyword); if (matchedMaps.isEmpty()) { - return entries; + return new ArrayList<>(); } + String cacheKey = buildCollectionCacheKey(matchedMaps); + return COLLECTED_RECIPE_CACHE + .getOrCompute(cacheKey, () -> Collections.unmodifiableList(collectRecipesForMatchedMaps(matchedMaps))); + } + + private static List collectRecipesForMatchedMaps(List matchedMaps) { + List entries = new ArrayList<>(); + Set processedKeys = new java.util.HashSet<>(); + for (String mapId : matchedMaps) { RecipeMap targetMap = RecipeMap.ALL_RECIPE_MAPS.get(mapId); if (targetMap == null) continue; @@ -155,6 +164,13 @@ public static List collectRecipes(String keyword) { return entries; } + private static String buildCollectionCacheKey(List matchedMaps) { + if (matchedMaps == null || matchedMaps.isEmpty()) { + return ""; + } + return String.join("\u001F", matchedMaps); + } + /** * GT 的 furnace / microwave 使用 NonGTBackend 动态查配方,不会预先填充到 getAllRecipes()。 * 因此这里回退到原版 FurnaceRecipes 枚举并构造 RecipeEntry。 diff --git a/src/main/java/com/github/ae2patterngen/recipe/RecipeCollectionCache.java b/src/main/java/com/github/ae2patterngen/recipe/RecipeCollectionCache.java new file mode 100644 index 0000000..3b46588 --- /dev/null +++ b/src/main/java/com/github/ae2patterngen/recipe/RecipeCollectionCache.java @@ -0,0 +1,27 @@ +package com.github.ae2patterngen.recipe; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Small synchronized cache for recipe collection results. + */ +final class RecipeCollectionCache { + + private final Map cache = new HashMap(); + + synchronized V getOrCompute(K key, Supplier supplier) { + if (cache.containsKey(key)) { + return cache.get(key); + } + + V value = supplier.get(); + cache.put(key, value); + return value; + } + + synchronized void clear() { + cache.clear(); + } +} diff --git a/src/test/java/com/github/ae2patterngen/filter/ExplicitStackMatcherTest.java b/src/test/java/com/github/ae2patterngen/filter/ExplicitStackMatcherTest.java index 7c06583..ffd1d68 100644 --- a/src/test/java/com/github/ae2patterngen/filter/ExplicitStackMatcherTest.java +++ b/src/test/java/com/github/ae2patterngen/filter/ExplicitStackMatcherTest.java @@ -2,6 +2,12 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import org.junit.Test; @@ -61,4 +67,68 @@ public void explicitTokensSupportOrSemantics() { assertTrue(matcher.matches("Machine Part", 1, 0, new String[0])); assertFalse(matcher.matches("Other", 1, 0, new String[] { "dustCopper" })); } + + @Test + public void idOnlyMatcherDoesNotLoadOreOrDisplayData() { + ExplicitStackMatcher matcher = new ExplicitStackMatcher("[8119]"); + AtomicInteger oreLoads = new AtomicInteger(); + AtomicInteger displayLoads = new AtomicInteger(); + + assertTrue( + invokeLazyMatch(matcher, 8119, 12, oreLoads, displayLoads, new String[] { "dustCopper" }, "Copper Dust")); + assertTrue("id-only matcher should not load ore names", oreLoads.get() == 0); + assertTrue("id-only matcher should not load display name", displayLoads.get() == 0); + } + + @Test + public void oreOnlyMatcherDoesNotLoadDisplayData() { + ExplicitStackMatcher matcher = new ExplicitStackMatcher("(dustCopper)"); + AtomicInteger oreLoads = new AtomicInteger(); + AtomicInteger displayLoads = new AtomicInteger(); + + assertTrue( + invokeLazyMatch(matcher, 8119, 12, oreLoads, displayLoads, new String[] { "dustCopper" }, "Copper Dust")); + assertTrue("ore-only matcher should load ore names", oreLoads.get() > 0); + assertTrue("ore-only matcher should not load display name", displayLoads.get() == 0); + } + + @Test + public void displayOnlyMatcherDoesNotLoadOreData() { + ExplicitStackMatcher matcher = new ExplicitStackMatcher("{Copper Dust}"); + AtomicInteger oreLoads = new AtomicInteger(); + AtomicInteger displayLoads = new AtomicInteger(); + + assertTrue( + invokeLazyMatch(matcher, 8119, 12, oreLoads, displayLoads, new String[] { "dustCopper" }, "Copper Dust")); + assertTrue("display-only matcher should not load ore names", oreLoads.get() == 0); + assertTrue("display-only matcher should load display name", displayLoads.get() > 0); + } + + private boolean invokeLazyMatch(ExplicitStackMatcher matcher, int itemId, int meta, AtomicInteger oreLoads, + AtomicInteger displayLoads, String[] oreNames, String displayName) { + try { + Method method = ExplicitStackMatcher.class + .getDeclaredMethod("matches", int.class, int.class, Supplier.class, Supplier.class); + method.setAccessible(true); + + Supplier oreSupplier = () -> { + oreLoads.incrementAndGet(); + return oreNames; + }; + Supplier displaySupplier = () -> { + displayLoads.incrementAndGet(); + return displayName; + }; + + return ((Boolean) method.invoke(matcher, itemId, meta, oreSupplier, displaySupplier)).booleanValue(); + } catch (NoSuchMethodException e) { + fail("Expected lazy matching overload for token-specific matching optimization"); + } catch (IllegalAccessException e) { + fail("Unable to access lazy matching overload: " + e.getMessage()); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause() != null ? e.getCause() : e; + fail("Lazy matching overload threw unexpectedly: " + cause.getMessage()); + } + return false; + } } diff --git a/src/test/java/com/github/ae2patterngen/recipe/RecipeCollectionCacheTest.java b/src/test/java/com/github/ae2patterngen/recipe/RecipeCollectionCacheTest.java new file mode 100644 index 0000000..1329bbf --- /dev/null +++ b/src/test/java/com/github/ae2patterngen/recipe/RecipeCollectionCacheTest.java @@ -0,0 +1,80 @@ +package com.github.ae2patterngen.recipe; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import org.junit.Test; + +public class RecipeCollectionCacheTest { + + @Test + public void repeatedLookupsComputeOnlyOnce() throws Exception { + Object cache = newCacheInstance(); + Method getOrCompute = getOrComputeMethod(cache); + AtomicInteger computeCount = new AtomicInteger(); + + Supplier supplier = () -> { + computeCount.incrementAndGet(); + return "cached-value"; + }; + + Object first = getOrCompute.invoke(cache, "assembler", supplier); + Object second = getOrCompute.invoke(cache, "assembler", supplier); + + assertEquals("value should be computed once", 1, computeCount.get()); + assertSame("same cached instance should be returned", first, second); + } + + @Test + public void clearDropsCachedEntries() throws Exception { + Object cache = newCacheInstance(); + Method getOrCompute = getOrComputeMethod(cache); + Method clear = getMethod(cache.getClass(), "clear"); + AtomicInteger computeCount = new AtomicInteger(); + + Supplier supplier = () -> { + computeCount.incrementAndGet(); + return "cached-value-" + computeCount.get(); + }; + + Object first = getOrCompute.invoke(cache, "assembler", supplier); + clear.invoke(cache); + Object second = getOrCompute.invoke(cache, "assembler", supplier); + + assertEquals("value should be recomputed after clear", 2, computeCount.get()); + org.junit.Assert.assertNotSame("cleared cache should not reuse old entry", first, second); + } + + private Object newCacheInstance() throws Exception { + try { + Class cacheClass = Class.forName("com.github.ae2patterngen.recipe.RecipeCollectionCache"); + Constructor constructor = cacheClass.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (ClassNotFoundException e) { + fail("Expected RecipeCollectionCache helper for recipe pool reuse"); + } + return null; + } + + private Method getOrComputeMethod(Object cache) throws Exception { + return getMethod(cache.getClass(), "getOrCompute", Object.class, Supplier.class); + } + + private Method getMethod(Class type, String name, Class... parameterTypes) throws Exception { + try { + Method method = type.getDeclaredMethod(name, parameterTypes); + method.setAccessible(true); + return method; + } catch (NoSuchMethodException e) { + fail("Expected method missing: " + name); + } + return null; + } +}