diff --git a/main.go b/main.go index 799172a0..40130f3a 100644 --- a/main.go +++ b/main.go @@ -139,14 +139,7 @@ var validateCmd = &cobra.Command{ return runValidation(yamlRecipe) } - // Check base recipes - for _, recipe := range recipes { - if recipe.Name() == recipeName { - return runValidation(recipe) - } - } - - return fmt.Errorf("recipe '%s' not found", recipeName) + return fmt.Errorf("recipe file '%s' not found", recipeName) }, } @@ -523,11 +516,7 @@ var testCmd = &cobra.Command{ }, } -var recipes = []playground.Recipe{ - &playground.L1Recipe{}, - &playground.OpRecipe{}, - &playground.BuilderNetRecipe{}, -} +var recipes = playground.GetBaseRecipes() func main() { // Set the embedded custom recipes filesystem for the playground package diff --git a/playground/cmd_validate.go b/playground/cmd_validate.go index 32aac4ac..03b68d97 100644 --- a/playground/cmd_validate.go +++ b/playground/cmd_validate.go @@ -33,12 +33,27 @@ func ValidateRecipe(recipe Recipe, baseRecipes []Recipe) *ValidationResult { validateYAMLRecipe(yamlRecipe, baseRecipes, result) } + // Create a temp output directory for validation + tmpDir, err := os.MkdirTemp("", "playground-validate-") + if err != nil { + result.AddError("failed to create temp directory: %v", err) + return result + } + defer os.RemoveAll(tmpDir) + + out, err := NewOutput(tmpDir) + if err != nil { + result.AddError("failed to create output: %v", err) + return result + } + // Build a minimal manifest to validate structure exCtx := &ExContext{ LogLevel: LevelInfo, Contender: &ContenderContext{ Enabled: false, }, + Output: out, } component := recipe.Apply(exCtx) diff --git a/playground/custom_recipes_test.go b/playground/custom_recipes_test.go index a91ddb0c..d82e7eaa 100644 --- a/playground/custom_recipes_test.go +++ b/playground/custom_recipes_test.go @@ -1,8 +1,11 @@ package playground import ( + "io/fs" "os" "path/filepath" + "runtime" + "strings" "testing" "testing/fstest" @@ -10,6 +13,16 @@ import ( "github.com/stretchr/testify/require" ) +// getRepoRootFS returns an fs.FS rooted at the repository root for testing real custom-recipes +func getRepoRootFS(t *testing.T) fs.FS { + t.Helper() + _, filename, _, ok := runtime.Caller(0) + require.True(t, ok, "failed to get caller info") + // Go up from playground/ to repo root + repoRoot := filepath.Dir(filepath.Dir(filename)) + return os.DirFS(repoRoot) +} + func newTestCustomRecipesFS() fstest.MapFS { return fstest.MapFS{ // Recipes in group/variant/playground.yaml format @@ -439,3 +452,47 @@ func (m *mockRecipe) Apply(ctx *ExContext) *Component { func (m *mockRecipe) Output(manifest *Manifest) map[string]interface{} { return nil } + +func TestValidateBaseRecipes(t *testing.T) { + baseRecipes := GetBaseRecipes() + require.NotEmpty(t, baseRecipes) + + for _, recipe := range baseRecipes { + t.Run(recipe.Name(), func(t *testing.T) { + result := ValidateRecipe(recipe, baseRecipes) + require.Empty(t, result.Errors, "base recipe %s has validation errors: %v", recipe.Name(), result.Errors) + }) + } +} + +func TestValidateShippedCustomRecipes(t *testing.T) { + // Validate real custom-recipes that ship with the binary + original := CustomRecipesFS + CustomRecipesFS = getRepoRootFS(t) + defer func() { CustomRecipesFS = original }() + + baseRecipes := GetBaseRecipes() + + customRecipes, err := GetEmbeddedCustomRecipes() + require.NoError(t, err) + require.NotEmpty(t, customRecipes) + + for _, name := range customRecipes { + t.Run(name, func(t *testing.T) { + recipe, cleanup, err := LoadCustomRecipe(name, baseRecipes) + require.NoError(t, err) + defer cleanup() + + result := ValidateRecipe(recipe, baseRecipes) + + // Filter host_path errors (environment-specific) + var errs []string + for _, e := range result.Errors { + if !strings.Contains(e, "host_path does not exist") { + errs = append(errs, e) + } + } + require.Empty(t, errs, "validation errors: %v", errs) + }) + } +} diff --git a/playground/manifest.go b/playground/manifest.go index d0997210..9f3ace2f 100644 --- a/playground/manifest.go +++ b/playground/manifest.go @@ -29,6 +29,15 @@ type Recipe interface { Output(manifest *Manifest) map[string]interface{} } +// GetBaseRecipes returns all available base recipes +func GetBaseRecipes() []Recipe { + return []Recipe{ + &L1Recipe{}, + &OpRecipe{}, + &BuilderNetRecipe{}, + } +} + // Manifest describes a list of services and their dependencies type Manifest struct { ID string `json:"session_id"` diff --git a/playground/recipe_yaml_test.go b/playground/recipe_yaml_test.go index 29261fb9..57ff682c 100644 --- a/playground/recipe_yaml_test.go +++ b/playground/recipe_yaml_test.go @@ -433,7 +433,7 @@ recipe: yamlFile := filepath.Join(tmpDir, "recipe.yaml") require.NoError(t, os.WriteFile(yamlFile, []byte(yamlContent), 0o644)) - baseRecipes := []Recipe{&L1Recipe{}} + baseRecipes := GetBaseRecipes() recipe, err := ParseYAMLRecipe(yamlFile, baseRecipes) @@ -457,7 +457,7 @@ func TestParseYAMLRecipe_MissingBase(t *testing.T) { yamlFile := filepath.Join(tmpDir, "recipe.yaml") require.NoError(t, os.WriteFile(yamlFile, []byte(yamlContent), 0o644)) - baseRecipes := []Recipe{&L1Recipe{}} + baseRecipes := GetBaseRecipes() _, err = ParseYAMLRecipe(yamlFile, baseRecipes) @@ -476,7 +476,7 @@ recipe: {} yamlFile := filepath.Join(tmpDir, "recipe.yaml") require.NoError(t, os.WriteFile(yamlFile, []byte(yamlContent), 0o644)) - baseRecipes := []Recipe{&L1Recipe{}} + baseRecipes := GetBaseRecipes() _, err = ParseYAMLRecipe(yamlFile, baseRecipes) @@ -485,7 +485,7 @@ recipe: {} } func TestParseYAMLRecipe_FileNotFound(t *testing.T) { - baseRecipes := []Recipe{&L1Recipe{}} + baseRecipes := GetBaseRecipes() _, err := ParseYAMLRecipe("/nonexistent/path/recipe.yaml", baseRecipes) @@ -503,7 +503,7 @@ recipe: {} yamlFile := filepath.Join(tmpDir, "recipe.yaml") require.NoError(t, os.WriteFile(yamlFile, []byte(yamlContent), 0o644)) - baseRecipes := []Recipe{&L1Recipe{}} + baseRecipes := GetBaseRecipes() recipe, err := ParseYAMLRecipe(yamlFile, baseRecipes) require.NoError(t, err) @@ -527,7 +527,7 @@ recipe: yamlFile := filepath.Join(tmpDir, "recipe.yaml") require.NoError(t, os.WriteFile(yamlFile, []byte(yamlContent), 0o644)) - baseRecipes := []Recipe{&L1Recipe{}} + baseRecipes := GetBaseRecipes() recipe, err := ParseYAMLRecipe(yamlFile, baseRecipes) require.NoError(t, err) @@ -558,7 +558,7 @@ recipe: yamlFile := filepath.Join(tmpDir, "recipe.yaml") require.NoError(t, os.WriteFile(yamlFile, []byte(yamlContent), 0o644)) - baseRecipes := []Recipe{&L1Recipe{}} + baseRecipes := GetBaseRecipes() recipe, err := ParseYAMLRecipe(yamlFile, baseRecipes) require.NoError(t, err) @@ -592,7 +592,7 @@ recipe: yamlFile := filepath.Join(tmpDir, "recipe.yaml") require.NoError(t, os.WriteFile(yamlFile, []byte(yamlContent), 0o644)) - baseRecipes := []Recipe{&L1Recipe{}} + baseRecipes := GetBaseRecipes() recipe, err := ParseYAMLRecipe(yamlFile, baseRecipes) require.NoError(t, err) @@ -627,7 +627,7 @@ recipe: yamlFile := filepath.Join(tmpDir, "recipe.yaml") require.NoError(t, os.WriteFile(yamlFile, []byte(yamlContent), 0o644)) - baseRecipes := []Recipe{&L1Recipe{}} + baseRecipes := GetBaseRecipes() recipe, err := ParseYAMLRecipe(yamlFile, baseRecipes) require.NoError(t, err) @@ -708,7 +708,7 @@ recipe: {} yamlFile := filepath.Join(tmpDir, "recipe.yaml") require.NoError(t, os.WriteFile(yamlFile, []byte(yamlContent), 0o644)) - baseRecipes := []Recipe{&L1Recipe{}} + baseRecipes := GetBaseRecipes() recipe, err := ParseYAMLRecipe(yamlFile, baseRecipes) require.NoError(t, err) @@ -738,7 +738,7 @@ recipe: yamlFile := filepath.Join(tmpDir, "recipe.yaml") require.NoError(t, os.WriteFile(yamlFile, []byte(yamlContent), 0o644)) - baseRecipes := []Recipe{&L1Recipe{}} + baseRecipes := GetBaseRecipes() recipe, err := ParseYAMLRecipe(yamlFile, baseRecipes) require.NoError(t, err)