Skip to content

Commit 16f878d

Browse files
committed
Add some more unit tests for recipe scheduler logic
1 parent 9ade1aa commit 16f878d

3 files changed

Lines changed: 175 additions & 3 deletions

File tree

rewrite-core/src/main/java/org/openrewrite/RecipeScheduler.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@ private LargeSourceSet runRecipeCycles(Recipe recipe, LargeSourceSet sourceSet,
7373
// use cases like sharing a `JavaTypeCache` between `JavaTemplate` parsers).
7474
Cursor rootCursor = new Cursor(null, Cursor.ROOT_VALUE);
7575
try {
76-
RecipeRunCycle<LargeSourceSet> cycle = new RecipeRunCycle<>(recipe, i, rootCursor, ctxWithWatch,
77-
recipeRunStats, searchResults, sourceFileResults, errorsTable, LargeSourceSet::edit);
76+
RecipeRunCycle<LargeSourceSet> cycle = createRecipeRunCycle(recipe, i, rootCursor, ctxWithWatch, recipeRunStats, searchResults, sourceFileResults, errorsTable);
7877
ctxWithWatch.putCycle(cycle);
7978
after.beforeCycle(i == maxCycles);
8079

@@ -117,6 +116,11 @@ private LargeSourceSet runRecipeCycles(Recipe recipe, LargeSourceSet sourceSet,
117116
return after;
118117
}
119118

119+
protected RecipeRunCycle<LargeSourceSet> createRecipeRunCycle(Recipe recipe, int cycle, Cursor rootCursor, WatchableExecutionContext ctxWithWatch, RecipeRunStats recipeRunStats, SearchResults searchResults, SourcesFileResults sourceFileResults, SourcesFileErrors errorsTable) {
120+
return new RecipeRunCycle<>(recipe, cycle, rootCursor, ctxWithWatch,
121+
recipeRunStats, searchResults, sourceFileResults, errorsTable, LargeSourceSet::edit);
122+
}
123+
120124
private void recursiveOnComplete(Recipe recipe, ExecutionContext ctx) {
121125
recipe.onComplete(ctx);
122126
for (Recipe r : recipe.getRecipeList()) {

rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeRunCycle.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ public LSS editSources(LSS sourceSet) {
238238
);
239239
}
240240

241-
private void recordSourceFileResultAndSearchResults(@Nullable SourceFile before, @Nullable SourceFile after, Stack<Recipe> recipeStack, ExecutionContext ctx) {
241+
protected void recordSourceFileResultAndSearchResults(@Nullable SourceFile before, @Nullable SourceFile after, Stack<Recipe> recipeStack, ExecutionContext ctx) {
242242
String beforePath = (before == null) ? "" : before.getSourcePath().toString();
243243
String afterPath = (after == null) ? "" : after.getSourcePath().toString();
244244
Recipe recipe = recipeStack.peek();

rewrite-core/src/test/java/org/openrewrite/RecipeSchedulerTest.java

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,16 @@
2323
import org.junit.jupiter.api.Test;
2424
import org.junit.jupiter.api.io.TempDir;
2525
import org.openrewrite.config.DeclarativeRecipe;
26+
import org.openrewrite.internal.InMemoryLargeSourceSet;
2627
import org.openrewrite.internal.RecipeRunException;
2728
import org.openrewrite.marker.Markup;
29+
import org.openrewrite.scheduling.RecipeRunCycle;
30+
import org.openrewrite.scheduling.WatchableExecutionContext;
2831
import org.openrewrite.scheduling.WorkingDirectoryExecutionContextView;
32+
import org.openrewrite.table.RecipeRunStats;
33+
import org.openrewrite.table.SearchResults;
2934
import org.openrewrite.table.SourcesFileErrors;
35+
import org.openrewrite.table.SourcesFileResults;
3036
import org.openrewrite.test.RewriteTest;
3137
import org.openrewrite.text.PlainText;
3238
import org.openrewrite.text.PlainTextVisitor;
@@ -165,6 +171,133 @@ void managedWorkingDirectoryWithMultipleRecipes(@TempDir Path path) {
165171
);
166172
assertThat(path).doesNotExist();
167173
}
174+
175+
@Test
176+
void verifyCycleInvariantsDuringMultipleCycles() {
177+
List<Integer> cyclesFromFactory = new java.util.ArrayList<>();
178+
List<Integer> cyclesFromContext = new java.util.ArrayList<>();
179+
AtomicInteger visitCount = new AtomicInteger(0);
180+
181+
RecipeScheduler trackingScheduler = new RecipeScheduler() {
182+
@Override
183+
protected RecipeRunCycle<LargeSourceSet> createRecipeRunCycle(
184+
Recipe recipe, int cycle, Cursor rootCursor,
185+
WatchableExecutionContext ctxWithWatch,
186+
RecipeRunStats recipeRunStats, SearchResults searchResults,
187+
SourcesFileResults sourceFileResults, SourcesFileErrors errorsTable) {
188+
cyclesFromFactory.add(cycle);
189+
return super.createRecipeRunCycle(recipe, cycle, rootCursor, ctxWithWatch,
190+
recipeRunStats, searchResults, sourceFileResults, errorsTable);
191+
}
192+
};
193+
194+
// Recipe that causes another cycle by returning different content each time (up to 2 times)
195+
Recipe multiCycleRecipe = toRecipe(() -> new PlainTextVisitor<>() {
196+
@Override
197+
public PlainText visitText(PlainText text, ExecutionContext ctx) {
198+
// Verify cycle is accessible from context during visitor execution
199+
cyclesFromContext.add(ctx.getCycle());
200+
int count = visitCount.incrementAndGet();
201+
if (count <= 2) {
202+
return text.withText(text.getText() + count);
203+
}
204+
return text;
205+
}
206+
}).withCausesAnotherCycle(true);
207+
208+
InMemoryExecutionContext ctx = new InMemoryExecutionContext();
209+
List<SourceFile> sources = List.of(PlainText.builder().text("v").sourcePath(Path.of("test.txt")).build());
210+
trackingScheduler.scheduleRun(multiCycleRecipe, new InMemoryLargeSourceSet(sources), ctx, 5, 1);
211+
212+
// Verify cycle numbers increment correctly: Cycle 1, 2, 3 (stops after no change in cycle 3)
213+
assertThat(cyclesFromFactory).containsExactly(1, 2, 3);
214+
// Verify cycle is correctly registered in context and accessible during visitor execution
215+
assertThat(cyclesFromContext).containsExactly(1, 2, 3);
216+
}
217+
218+
@Test
219+
void recordsBeforeAndAfterSourceFilesCorrectly() {
220+
List<String> beforeContents = new java.util.ArrayList<>();
221+
List<String> afterContents = new java.util.ArrayList<>();
222+
223+
RecipeScheduler trackingScheduler = new RecipeScheduler() {
224+
@Override
225+
protected RecipeRunCycle<LargeSourceSet> createRecipeRunCycle(
226+
Recipe recipe, int cycle, Cursor rootCursor,
227+
WatchableExecutionContext ctxWithWatch,
228+
RecipeRunStats recipeRunStats, SearchResults searchResults,
229+
SourcesFileResults sourceFileResults, SourcesFileErrors errorsTable) {
230+
return new RecipeRunCycle<>(recipe, cycle, rootCursor, ctxWithWatch,
231+
recipeRunStats, searchResults, sourceFileResults, errorsTable, LargeSourceSet::edit) {
232+
@Override
233+
protected void recordSourceFileResultAndSearchResults(
234+
@Nullable SourceFile before, @Nullable SourceFile after,
235+
java.util.Stack<Recipe> recipeStack, ExecutionContext ctx) {
236+
if (before instanceof PlainText) {
237+
beforeContents.add(((PlainText) before).getText());
238+
}
239+
if (after instanceof PlainText) {
240+
afterContents.add(((PlainText) after).getText());
241+
}
242+
super.recordSourceFileResultAndSearchResults(before, after, recipeStack, ctx);
243+
}
244+
};
245+
}
246+
};
247+
248+
Recipe recipe = toRecipe(() -> new PlainTextVisitor<>() {
249+
@Override
250+
public PlainText visitText(PlainText text, ExecutionContext ctx) {
251+
return text.withText("modified:" + text.getText());
252+
}
253+
});
254+
255+
InMemoryExecutionContext ctx = new InMemoryExecutionContext();
256+
List<SourceFile> sources = List.of(
257+
PlainText.builder().text("a").sourcePath(Path.of("a.txt")).build(),
258+
PlainText.builder().text("b").sourcePath(Path.of("b.txt")).build()
259+
);
260+
trackingScheduler.scheduleRun(recipe, new InMemoryLargeSourceSet(sources), ctx, 3, 1);
261+
262+
assertThat(beforeContents).containsExactlyInAnyOrder("a", "b");
263+
assertThat(afterContents).containsExactlyInAnyOrder("modified:a", "modified:b");
264+
}
265+
266+
@Test
267+
void recordsGeneratedSourceFiles() {
268+
List<String> generatedPaths = new java.util.ArrayList<>();
269+
270+
RecipeScheduler trackingScheduler = new RecipeScheduler() {
271+
@Override
272+
protected RecipeRunCycle<LargeSourceSet> createRecipeRunCycle(
273+
Recipe recipe, int cycle, Cursor rootCursor,
274+
WatchableExecutionContext ctxWithWatch,
275+
RecipeRunStats recipeRunStats, SearchResults searchResults,
276+
SourcesFileResults sourceFileResults, SourcesFileErrors errorsTable) {
277+
return new RecipeRunCycle<>(recipe, cycle, rootCursor, ctxWithWatch,
278+
recipeRunStats, searchResults, sourceFileResults, errorsTable, LargeSourceSet::edit) {
279+
@Override
280+
protected void recordSourceFileResultAndSearchResults(
281+
@Nullable SourceFile before, @Nullable SourceFile after,
282+
java.util.Stack<Recipe> recipeStack, ExecutionContext ctx) {
283+
// Track files that were generated (before is null)
284+
if (before == null && after != null) {
285+
generatedPaths.add(after.getSourcePath().toString());
286+
}
287+
super.recordSourceFileResultAndSearchResults(before, after, recipeStack, ctx);
288+
}
289+
};
290+
}
291+
};
292+
293+
Recipe generatingRecipe = new GeneratingRecipe();
294+
295+
InMemoryExecutionContext ctx = new InMemoryExecutionContext();
296+
List<SourceFile> sources = List.of(PlainText.builder().text("existing").sourcePath(Path.of("existing.txt")).build());
297+
trackingScheduler.scheduleRun(generatingRecipe, new InMemoryLargeSourceSet(sources), ctx, 3, 1);
298+
299+
assertThat(generatedPaths).containsExactly("generated.txt");
300+
}
168301
}
169302

170303
@AllArgsConstructor
@@ -228,6 +361,41 @@ public StackTraceElement[] getStackTrace() {
228361
}
229362
}
230363

364+
class GeneratingRecipe extends ScanningRecipe<AtomicInteger> {
365+
@Getter
366+
final String displayName = "Generating recipe";
367+
368+
@Getter
369+
final String description = "Generates a new file.";
370+
371+
@Override
372+
public AtomicInteger getInitialValue(ExecutionContext ctx) {
373+
return new AtomicInteger(0);
374+
}
375+
376+
@Override
377+
public TreeVisitor<?, ExecutionContext> getScanner(AtomicInteger acc) {
378+
return new TreeVisitor<>() {
379+
@Override
380+
public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) {
381+
acc.incrementAndGet();
382+
return tree;
383+
}
384+
};
385+
}
386+
387+
@Override
388+
public Collection<? extends SourceFile> generate(AtomicInteger acc, ExecutionContext ctx) {
389+
if (acc.get() > 0) {
390+
return List.of(PlainText.builder()
391+
.text("generated content")
392+
.sourcePath(Path.of("generated.txt"))
393+
.build());
394+
}
395+
return List.of();
396+
}
397+
}
398+
231399
@AllArgsConstructor
232400
class RecipeWritingToFile extends ScanningRecipe<RecipeWritingToFile.Accumulator> {
233401

0 commit comments

Comments
 (0)