|
20 | 20 | import org.jspecify.annotations.Nullable; |
21 | 21 | import org.junit.jupiter.api.Test; |
22 | 22 | import org.openrewrite.*; |
| 23 | +import org.openrewrite.internal.InMemoryLargeSourceSet; |
23 | 24 | import org.openrewrite.marker.SearchResult; |
| 25 | +import org.openrewrite.scheduling.RecipeRunCycle; |
| 26 | +import org.openrewrite.scheduling.WatchableExecutionContext; |
| 27 | +import org.openrewrite.table.RecipeRunStats; |
| 28 | +import org.openrewrite.table.SearchResults; |
| 29 | +import org.openrewrite.table.SourcesFileErrors; |
| 30 | +import org.openrewrite.table.SourcesFileResults; |
24 | 31 | import org.openrewrite.test.RewriteTest; |
25 | 32 | import org.openrewrite.text.ChangeText; |
26 | 33 | import org.openrewrite.text.Find; |
|
29 | 36 |
|
30 | 37 | import java.net.URI; |
31 | 38 | import java.nio.file.Path; |
32 | | -import java.util.List; |
| 39 | +import java.nio.file.Paths; |
| 40 | +import java.util.*; |
33 | 41 | import java.util.concurrent.atomic.AtomicInteger; |
34 | 42 |
|
35 | 43 | import static java.util.Collections.emptyList; |
@@ -740,4 +748,95 @@ void deeperCyclicReferencesDetectedAsCycle() { |
740 | 748 | .hasMessageContaining("RecipeB") |
741 | 749 | .hasMessageContaining("RecipeC"); |
742 | 750 | } |
| 751 | + |
| 752 | + // Uses separate RecipeRunCycles for scan and edit to verify that the accumulator |
| 753 | + // survives when getRecipeList() is called again from a fresh RecipeStack. |
| 754 | + // This can't be tested via rewriteRun() which uses a single RecipeRunCycle per cycle. |
| 755 | + @Test |
| 756 | + void scanningRecipeAccumulatorSurvivesAcrossRecipeRunCycles() { |
| 757 | + DeclarativeRecipe recipe = new DeclarativeRecipe( |
| 758 | + "test.WithPrecondition", "With precondition", "Test with precondition.", |
| 759 | + emptySet(), null, URI.create("test"), true, emptyList() |
| 760 | + ); |
| 761 | + recipe.addPrecondition(new Singleton()); |
| 762 | + recipe.addUninitialized(new CountingRecipe()); |
| 763 | + recipe.initialize(List.of()); |
| 764 | + |
| 765 | + List<SourceFile> sources = List.of( |
| 766 | + PlainText.builder().id(Tree.randomId()).sourcePath(Paths.get("file1.txt")).text("hello").build(), |
| 767 | + PlainText.builder().id(Tree.randomId()).sourcePath(Paths.get("file2.txt")).text("world").build() |
| 768 | + ); |
| 769 | + |
| 770 | + Cursor rootCursor = new Cursor(null, Cursor.ROOT_VALUE); |
| 771 | + WatchableExecutionContext ctx = new WatchableExecutionContext(new InMemoryExecutionContext()); |
| 772 | + Recipe noop = Recipe.noop(); |
| 773 | + |
| 774 | + // Cycle 1: scan (simulates first yield batch on the platform) |
| 775 | + RecipeRunCycle<LargeSourceSet> scanCycle = new RecipeRunCycle<>( |
| 776 | + recipe, 1, rootCursor, ctx, |
| 777 | + new RecipeRunStats(noop), new SearchResults(noop), |
| 778 | + new SourcesFileResults(noop), new SourcesFileErrors(noop), |
| 779 | + LargeSourceSet::edit |
| 780 | + ); |
| 781 | + ctx.putCycle(scanCycle); |
| 782 | + scanCycle.scanSources(new InMemoryLargeSourceSet(sources)); |
| 783 | + |
| 784 | + // Cycle 2: edit (simulates new RecipeRunCycle after worker yield — new RecipeStack, fresh getRecipeList() calls) |
| 785 | + RecipeRunCycle<LargeSourceSet> editCycle = new RecipeRunCycle<>( |
| 786 | + recipe, 1, rootCursor, ctx, |
| 787 | + new RecipeRunStats(noop), new SearchResults(noop), |
| 788 | + new SourcesFileResults(noop), new SourcesFileErrors(noop), |
| 789 | + LargeSourceSet::edit |
| 790 | + ); |
| 791 | + ctx.putCycle(editCycle); |
| 792 | + LargeSourceSet edited = editCycle.editSources(new InMemoryLargeSourceSet(sources)); |
| 793 | + |
| 794 | + List<SourceFile> results = edited.getChangeset().getAllResults().stream() |
| 795 | + .map(Result::getAfter) |
| 796 | + .filter(Objects::nonNull) |
| 797 | + .toList(); |
| 798 | + |
| 799 | + assertThat(results).anyMatch(sf -> ((PlainText) sf).getText().contains("[scanned:")); |
| 800 | + } |
| 801 | + |
| 802 | + static class CountingRecipe extends ScanningRecipe<List<String>> { |
| 803 | + @Override |
| 804 | + public String getDisplayName() { |
| 805 | + return "Counting recipe"; |
| 806 | + } |
| 807 | + |
| 808 | + @Override |
| 809 | + public String getDescription() { |
| 810 | + return "Counts files during scan, appends count during edit."; |
| 811 | + } |
| 812 | + |
| 813 | + @Override |
| 814 | + public List<String> getInitialValue(ExecutionContext ctx) { |
| 815 | + return new ArrayList<>(); |
| 816 | + } |
| 817 | + |
| 818 | + @Override |
| 819 | + public TreeVisitor<?, ExecutionContext> getScanner(List<String> acc) { |
| 820 | + return new PlainTextVisitor<>() { |
| 821 | + @Override |
| 822 | + public PlainText visitText(PlainText text, ExecutionContext ctx) { |
| 823 | + acc.add(text.getText()); |
| 824 | + return text; |
| 825 | + } |
| 826 | + }; |
| 827 | + } |
| 828 | + |
| 829 | + @Override |
| 830 | + public TreeVisitor<?, ExecutionContext> getVisitor(List<String> acc) { |
| 831 | + return new PlainTextVisitor<>() { |
| 832 | + @Override |
| 833 | + public PlainText visitText(PlainText text, ExecutionContext ctx) { |
| 834 | + if (!acc.isEmpty()) { |
| 835 | + return text.withText(text.getText() + " [scanned:" + acc.size() + "]"); |
| 836 | + } |
| 837 | + return text; |
| 838 | + } |
| 839 | + }; |
| 840 | + } |
| 841 | + } |
743 | 842 | } |
0 commit comments