Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ public static RecipeSpec defaults() {

boolean serializationValidation = true;

boolean recipeValidation = true;

PrintOutputCapture.@Nullable MarkerPrinter markerPrinter;

List<UncheckedConsumer<List<SourceFile>>> beforeRecipes = new ArrayList<>();
Expand Down Expand Up @@ -258,6 +260,11 @@ public RecipeSpec validateRecipeSerialization(boolean validate) {
return this;
}

public RecipeSpec validateRecipe(boolean validate) {
this.recipeValidation = validate;
return this;
}

public RecipeSpec sourceSet(Function<List<SourceFile>, LargeSourceSet> sourceSetBuilder) {
this.sourceSet = sourceSetBuilder;
return this;
Expand Down
20 changes: 14 additions & 6 deletions rewrite-test/src/main/java/org/openrewrite/test/RewriteTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.config.CompositeRecipe;
import org.openrewrite.config.DeclarativeRecipe;
import org.openrewrite.config.Environment;
import org.openrewrite.config.OptionDescriptor;
import org.openrewrite.internal.*;
Expand Down Expand Up @@ -90,10 +91,15 @@ default void assertRecipesConfigure(String packageName) {
// scanRuntimeClasspath picks up all recipes in META-INF/rewrite regardless of whether their
// names start with the package we intend to filter on here
if (recipe.getName().startsWith(packageName)) {
// Imperative recipes are loaded with no user-provided arguments, so all optional
// parameters are null. Skip recipe validation for these since custom validate()
// methods (e.g. requiring at least one of several optional parameters) would
// fail on an unconfigured instance. Declarative recipes have their options
// configured from YAML and should still be validated.
softly.assertThatCode(() -> {
try {
rewriteRun(
spec -> spec.recipe(recipe),
spec -> spec.recipe(recipe).validateRecipe(recipe instanceof DeclarativeRecipe),
Comment thread
MBoegers marked this conversation as resolved.
Outdated
new SourceSpecs[0]
);
} catch (Throwable t) {
Expand Down Expand Up @@ -226,11 +232,13 @@ default void rewriteRun(Consumer<RecipeSpec> spec, SourceSpec<?>... sourceSpecs)
for (SourceSpec<?> s : sourceSpecs) {
s.customizeExecutionContext.accept(ctx);
}
List<Validated<Object>> validations = new ArrayList<>();
recipe.validateAll(ctx, validations);
assertThat(validations.stream().filter(Validated::isInvalid))
.as("Recipe validation must have no failures")
.isEmpty();
if (testClassSpec.recipeValidation && testMethodSpec.recipeValidation) {
Comment thread
timtebeek marked this conversation as resolved.
Outdated
List<Validated<Object>> validations = new ArrayList<>();
recipe.validateAll(ctx, validations);
assertThat(validations.stream().filter(Validated::isInvalid))
.as("Recipe validation must have no failures")
.isEmpty();
}

Map<Parser.Builder, List<SourceSpec<?>>> sourceSpecsByParser = new HashMap<>();
List<Parser.Builder> methodSpecParsers = testMethodSpec.parsers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.openrewrite.test.internal;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Value;
Expand All @@ -24,6 +25,7 @@
import org.junit.jupiter.api.Test;
import org.openrewrite.*;
import org.openrewrite.test.RewriteTest;
import org.openrewrite.test.SourceSpecs;
import org.openrewrite.test.TypeValidation;
import org.openrewrite.text.PlainText;
import org.openrewrite.text.PlainTextVisitor;
Expand Down Expand Up @@ -152,6 +154,47 @@ void allowScannerEdit() {
text("foo")
);
}

@Test
void rejectUnconfiguredRecipeWithOptionalOrValidation() {
// A recipe with optional params and validate() requiring at least one
// fails when loaded with no arguments and default validation is enabled
assertThrows(AssertionError.class, () ->
rewriteRun(
spec -> spec.recipe(new RecipeWithOptionalOrValidation(null, null)),
new SourceSpecs[0]
));
}

@Test
void acceptUnconfiguredRecipeWithOptionalOrValidationWhenSkipped() {
// The same recipe succeeds when validation is disabled, as
// assertRecipesConfigure() does for imperative recipes
rewriteRun(
spec -> spec
.recipe(new RecipeWithOptionalOrValidation(null, null))
.validateRecipe(false),
new SourceSpecs[0]
);
}

@Test
void rejectConfiguredRecipeWithOptionalOrValidationStillValidated() {
// When the recipe IS configured (e.g. from YAML), validation should
// still catch real problems like referring to non-existent sub-recipes
assertThrows(AssertionError.class, () ->
rewriteRun(
spec -> spec.recipeFromYaml("""
type: specs.openrewrite.org/v1beta/recipe
name: org.openrewrite.test.internal.StillValidated
displayName: Still validated
description: Declarative recipe with a non-existent sub-recipe should still fail validation.
recipeList:
- org.openrewrite.DoesNotExist

""", "org.openrewrite.test.internal.StillValidated")
));
}
}

@EqualsAndHashCode(callSuper = false)
Expand Down Expand Up @@ -353,3 +396,47 @@ public Collection<? extends SourceFile> generate(AtomicBoolean acc, ExecutionCon
.build());
}
}

@EqualsAndHashCode(callSuper = false)
class RecipeWithOptionalOrValidation extends Recipe {

@Getter
final String displayName = "Recipe with optional OR validation";

@Getter
final String description = "Has two optional parameters where at least one must be set.";

@Option(displayName = "Option A",
description = "First optional parameter.",
example = "valueA",
required = false)
@Nullable
final String optionA;

@Option(displayName = "Option B",
description = "Second optional parameter.",
example = "valueB",
required = false)
@Nullable
final String optionB;

@JsonCreator
RecipeWithOptionalOrValidation(
@JsonProperty("optionA") @Nullable String optionA,
@JsonProperty("optionB") @Nullable String optionB) {
this.optionA = optionA;
this.optionB = optionB;
}

@Override
public Validated<Object> validate() {
return super.validate().and(
Validated.required("optionA", optionA)
.or(Validated.required("optionB", optionB)));
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return TreeVisitor.noop();
}
}