Skip to content

Commit 07e5345

Browse files
committed
feat: validate-config
1 parent ab51206 commit 07e5345

2 files changed

Lines changed: 333 additions & 0 deletions

File tree

cmd/validate_config.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/boringsql/regresql/regresql"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var (
12+
validateConfigCwd string
13+
14+
validateConfigCmd = &cobra.Command{
15+
Use: "validate-config",
16+
Short: "Validate configuration for RegreSQL 2.0 compatibility",
17+
Long: `Checks configuration files for deprecated patterns and validates
18+
readiness for RegreSQL 2.0.
19+
20+
Validates:
21+
- Config file exists and is parseable
22+
- No deprecated 'fixtures:' or 'cleanup:' fields in plan files
23+
- All fixture files are valid
24+
- Snapshot paths exist (if configured)`,
25+
Run: runValidateConfig,
26+
}
27+
)
28+
29+
func init() {
30+
RootCmd.AddCommand(validateConfigCmd)
31+
validateConfigCmd.Flags().StringVarP(&validateConfigCwd, "cwd", "C", ".", "Change to Directory")
32+
}
33+
34+
func runValidateConfig(cmd *cobra.Command, args []string) {
35+
if err := checkDirectory(validateConfigCwd); err != nil {
36+
fmt.Print(err.Error())
37+
os.Exit(1)
38+
}
39+
40+
fmt.Println("Checking configuration...")
41+
fmt.Println()
42+
43+
result := regresql.ValidateForUpgrade(validateConfigCwd)
44+
45+
if result.ConfigValid {
46+
fmt.Printf("✓ Config file found: %s\n", result.ConfigFile)
47+
} else {
48+
fmt.Printf("✗ Config file error: %s\n", result.ConfigError)
49+
}
50+
51+
printPlanIssues(result.PlanIssues)
52+
printFixtureIssues(result.FixtureIssues, result.FixtureCount)
53+
printSnapshotIssues(result.SnapshotIssues)
54+
55+
fmt.Println()
56+
if result.Passed {
57+
fmt.Println("✓ Ready for RegreSQL 2.0")
58+
os.Exit(0)
59+
} else {
60+
fmt.Println("✗ Not ready for RegreSQL 2.0")
61+
fmt.Println(" Fix the issues above before upgrading.")
62+
os.Exit(1)
63+
}
64+
}
65+
66+
func printPlanIssues(issues []regresql.ValidationIssue) {
67+
fixtureIssues := filterByField(issues, "fixtures")
68+
cleanupIssues := filterByField(issues, "cleanup")
69+
70+
if len(fixtureIssues) == 0 {
71+
fmt.Println("✓ No deprecated per-test fixtures in plan files")
72+
} else {
73+
fmt.Println("✗ Deprecated 'fixtures:' found in plan files:")
74+
for _, issue := range fixtureIssues {
75+
fmt.Printf(" - %s\n", issue.File)
76+
}
77+
}
78+
79+
if len(cleanupIssues) == 0 {
80+
fmt.Println("✓ No deprecated cleanup strategies in plan files")
81+
} else {
82+
fmt.Println("✗ Deprecated 'cleanup:' found in plan files:")
83+
for _, issue := range cleanupIssues {
84+
fmt.Printf(" - %s\n", issue.File)
85+
}
86+
}
87+
}
88+
89+
func printFixtureIssues(issues []regresql.ValidationIssue, count int) {
90+
if len(issues) == 0 {
91+
if count > 0 {
92+
fmt.Printf("✓ Fixture files valid (%d files)\n", count)
93+
} else {
94+
fmt.Println("✓ No fixture files to validate")
95+
}
96+
} else {
97+
fmt.Println("✗ Fixture file issues:")
98+
for _, issue := range issues {
99+
fmt.Printf(" - %s: %s\n", issue.File, issue.Message)
100+
}
101+
}
102+
}
103+
104+
func printSnapshotIssues(issues []regresql.ValidationIssue) {
105+
if len(issues) == 0 {
106+
return
107+
}
108+
109+
fmt.Println("✗ Snapshot configuration issues:")
110+
for _, issue := range issues {
111+
fmt.Printf(" - %s: %s\n", issue.Field, issue.Message)
112+
}
113+
}
114+
115+
func filterByField(issues []regresql.ValidationIssue, field string) []regresql.ValidationIssue {
116+
var filtered []regresql.ValidationIssue
117+
for _, issue := range issues {
118+
if issue.Field == field {
119+
filtered = append(filtered, issue)
120+
}
121+
}
122+
return filtered
123+
}

regresql/validate.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package regresql
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"gopkg.in/yaml.v3"
9+
)
10+
11+
type (
12+
ValidationResult struct {
13+
ConfigFile string
14+
ConfigValid bool
15+
ConfigError string
16+
PlanIssues []ValidationIssue
17+
FixtureIssues []ValidationIssue
18+
SnapshotIssues []ValidationIssue
19+
FixtureCount int
20+
Passed bool
21+
}
22+
23+
ValidationIssue struct {
24+
File string
25+
Field string
26+
Message string
27+
}
28+
)
29+
30+
func ValidateForUpgrade(root string) ValidationResult {
31+
result := ValidationResult{Passed: true}
32+
33+
configFile := filepath.Join(root, "regresql", "regress.yaml")
34+
result.ConfigFile = configFile
35+
36+
cfg, err := ReadConfig(root)
37+
if err != nil {
38+
result.ConfigValid = false
39+
result.ConfigError = err.Error()
40+
result.Passed = false
41+
return result
42+
}
43+
result.ConfigValid = true
44+
45+
result.PlanIssues = scanPlanFilesForDeprecated(root)
46+
if len(result.PlanIssues) > 0 {
47+
result.Passed = false
48+
}
49+
50+
fixtureIssues, fixtureCount := validateFixtureFiles(root)
51+
result.FixtureIssues = fixtureIssues
52+
result.FixtureCount = fixtureCount
53+
if len(result.FixtureIssues) > 0 {
54+
result.Passed = false
55+
}
56+
57+
if cfg.Snapshot != nil {
58+
result.SnapshotIssues = validateSnapshotPaths(root, cfg.Snapshot)
59+
if len(result.SnapshotIssues) > 0 {
60+
result.Passed = false
61+
}
62+
}
63+
64+
return result
65+
}
66+
67+
func scanPlanFilesForDeprecated(root string) []ValidationIssue {
68+
var issues []ValidationIssue
69+
70+
planDir := filepath.Join(root, "regresql", "plans")
71+
if _, err := os.Stat(planDir); os.IsNotExist(err) {
72+
return issues
73+
}
74+
75+
planFiles, err := filepath.Glob(filepath.Join(planDir, "*.yaml"))
76+
if err != nil {
77+
return issues
78+
}
79+
80+
for _, pfile := range planFiles {
81+
data, err := os.ReadFile(pfile)
82+
if err != nil {
83+
continue
84+
}
85+
86+
var raw map[string]any
87+
if err := yaml.Unmarshal(data, &raw); err != nil {
88+
continue
89+
}
90+
91+
relPath, _ := filepath.Rel(root, pfile)
92+
if relPath == "" {
93+
relPath = pfile
94+
}
95+
96+
if _, hasFixtures := raw["fixtures"]; hasFixtures {
97+
issues = append(issues, ValidationIssue{
98+
File: relPath,
99+
Field: "fixtures",
100+
Message: "deprecated per-test fixtures",
101+
})
102+
}
103+
104+
if _, hasCleanup := raw["cleanup"]; hasCleanup {
105+
issues = append(issues, ValidationIssue{
106+
File: relPath,
107+
Field: "cleanup",
108+
Message: "deprecated cleanup strategy",
109+
})
110+
}
111+
}
112+
113+
return issues
114+
}
115+
116+
func validateFixtureFiles(root string) ([]ValidationIssue, int) {
117+
var issues []ValidationIssue
118+
count := 0
119+
120+
fixtureDir := filepath.Join(root, "regresql", "fixtures")
121+
if _, err := os.Stat(fixtureDir); os.IsNotExist(err) {
122+
return issues, count
123+
}
124+
125+
fixtureFiles, err := filepath.Glob(filepath.Join(fixtureDir, "*.yaml"))
126+
if err != nil {
127+
return issues, count
128+
}
129+
130+
for _, ffile := range fixtureFiles {
131+
count++
132+
data, err := os.ReadFile(ffile)
133+
if err != nil {
134+
relPath, _ := filepath.Rel(root, ffile)
135+
issues = append(issues, ValidationIssue{
136+
File: relPath,
137+
Message: fmt.Sprintf("cannot read: %v", err),
138+
})
139+
continue
140+
}
141+
142+
var fixture Fixture
143+
if err := yaml.Unmarshal(data, &fixture); err != nil {
144+
relPath, _ := filepath.Rel(root, ffile)
145+
issues = append(issues, ValidationIssue{
146+
File: relPath,
147+
Message: fmt.Sprintf("invalid YAML: %v", err),
148+
})
149+
continue
150+
}
151+
152+
if err := fixture.Validate(); err != nil {
153+
relPath, _ := filepath.Rel(root, ffile)
154+
issues = append(issues, ValidationIssue{
155+
File: relPath,
156+
Message: err.Error(),
157+
})
158+
}
159+
}
160+
161+
return issues, count
162+
}
163+
164+
func validateSnapshotPaths(root string, snap *SnapshotConfig) []ValidationIssue {
165+
var issues []ValidationIssue
166+
167+
if snap.Path != "" {
168+
snapPath := snap.Path
169+
if !filepath.IsAbs(snapPath) {
170+
snapPath = filepath.Join(root, snapPath)
171+
}
172+
if _, err := os.Stat(snapPath); os.IsNotExist(err) {
173+
issues = append(issues, ValidationIssue{
174+
File: snap.Path,
175+
Field: "snapshot.path",
176+
Message: "snapshot file does not exist",
177+
})
178+
}
179+
}
180+
181+
if snap.Schema != "" {
182+
schemaPath := snap.Schema
183+
if !filepath.IsAbs(schemaPath) {
184+
schemaPath = filepath.Join(root, schemaPath)
185+
}
186+
if _, err := os.Stat(schemaPath); os.IsNotExist(err) {
187+
issues = append(issues, ValidationIssue{
188+
File: snap.Schema,
189+
Field: "snapshot.schema",
190+
Message: "schema file does not exist",
191+
})
192+
}
193+
}
194+
195+
if snap.Migrations != "" {
196+
migrationsPath := snap.Migrations
197+
if !filepath.IsAbs(migrationsPath) {
198+
migrationsPath = filepath.Join(root, migrationsPath)
199+
}
200+
if _, err := os.Stat(migrationsPath); os.IsNotExist(err) {
201+
issues = append(issues, ValidationIssue{
202+
File: snap.Migrations,
203+
Field: "snapshot.migrations",
204+
Message: "migrations directory does not exist",
205+
})
206+
}
207+
}
208+
209+
return issues
210+
}

0 commit comments

Comments
 (0)