Table.Columns/Constraints/Indexes/Relationships are Go maps, and every writer, reader, diff, inspector, and merge code path that iterated them directly was subject to Go's randomized map order, so identical input could produce different output (or a different in-report violation/diff order) on every run. Most visibly this showed up as bun/gorm `unique:` struct tags changing order across consecutive `make models` runs with no source change. Fixed by sorting map iteration (by Sequence then Name, or alphabetically for string-keyed maps) everywhere the order affects generated output or first-match tie-break logic, across the bun, gorm, sqlite, dbml, drawdb, pgsql, prisma, graphql, typeorm, drizzle, and dctx writers; the dctx, prisma, and typeorm readers; the shared models.GetPrimaryKey/ GetForeignKeys helpers; pkg/diff, pkg/inspector, and pkg/merge; and the TUI column/relationship pickers in pkg/ui.
278 lines
6.9 KiB
Go
278 lines
6.9 KiB
Go
package inspector
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestNewInspector(t *testing.T) {
|
|
db := createTestDatabase()
|
|
config := GetDefaultConfig()
|
|
|
|
inspector := NewInspector(db, config)
|
|
|
|
if inspector == nil {
|
|
t.Fatal("NewInspector() returned nil")
|
|
}
|
|
|
|
if inspector.db != db {
|
|
t.Error("NewInspector() database not set correctly")
|
|
}
|
|
|
|
if inspector.config != config {
|
|
t.Error("NewInspector() config not set correctly")
|
|
}
|
|
}
|
|
|
|
func TestInspect(t *testing.T) {
|
|
db := createTestDatabase()
|
|
config := GetDefaultConfig()
|
|
|
|
inspector := NewInspector(db, config)
|
|
report, err := inspector.Inspect()
|
|
|
|
if err != nil {
|
|
t.Fatalf("Inspect() returned error: %v", err)
|
|
}
|
|
|
|
if report == nil {
|
|
t.Fatal("Inspect() returned nil report")
|
|
}
|
|
|
|
if report.Database != db.Name {
|
|
t.Errorf("Inspect() report.Database = %q, want %q", report.Database, db.Name)
|
|
}
|
|
|
|
if report.Summary.TotalRules != len(config.Rules) {
|
|
t.Errorf("Inspect() TotalRules = %d, want %d", report.Summary.TotalRules, len(config.Rules))
|
|
}
|
|
|
|
if len(report.Violations) == 0 {
|
|
t.Error("Inspect() returned no violations, expected some results")
|
|
}
|
|
}
|
|
|
|
// TestInspect_Deterministic verifies that repeated Inspect() calls against
|
|
// the same database and config produce violations in the same order, instead
|
|
// of following Go's randomized map iteration order over config.Rules and the
|
|
// per-table Columns/Constraints/Indexes maps.
|
|
func TestInspect_Deterministic(t *testing.T) {
|
|
db := createTestDatabase()
|
|
config := GetDefaultConfig()
|
|
|
|
inspector := NewInspector(db, config)
|
|
|
|
first, err := inspector.Inspect()
|
|
if err != nil {
|
|
t.Fatalf("Inspect() returned error: %v", err)
|
|
}
|
|
|
|
wantOrder := make([]string, len(first.Violations))
|
|
for i, v := range first.Violations {
|
|
wantOrder[i] = v.RuleName + "|" + v.Location
|
|
}
|
|
|
|
for i := 0; i < 25; i++ {
|
|
report, err := inspector.Inspect()
|
|
if err != nil {
|
|
t.Fatalf("Inspect() returned error on run %d: %v", i, err)
|
|
}
|
|
|
|
if len(report.Violations) != len(wantOrder) {
|
|
t.Fatalf("run %d: got %d violations, want %d", i, len(report.Violations), len(wantOrder))
|
|
}
|
|
|
|
for j, v := range report.Violations {
|
|
got := v.RuleName + "|" + v.Location
|
|
if got != wantOrder[j] {
|
|
t.Fatalf("run %d: violation[%d] = %q, want %q", i, j, got, wantOrder[j])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInspectWithDisabledRules(t *testing.T) {
|
|
db := createTestDatabase()
|
|
config := GetDefaultConfig()
|
|
|
|
// Disable all rules
|
|
for name := range config.Rules {
|
|
rule := config.Rules[name]
|
|
rule.Enabled = "off"
|
|
config.Rules[name] = rule
|
|
}
|
|
|
|
inspector := NewInspector(db, config)
|
|
report, err := inspector.Inspect()
|
|
|
|
if err != nil {
|
|
t.Fatalf("Inspect() with disabled rules returned error: %v", err)
|
|
}
|
|
|
|
if report.Summary.RulesChecked != 0 {
|
|
t.Errorf("Inspect() RulesChecked = %d, want 0 (all disabled)", report.Summary.RulesChecked)
|
|
}
|
|
|
|
if report.Summary.RulesSkipped != len(config.Rules) {
|
|
t.Errorf("Inspect() RulesSkipped = %d, want %d", report.Summary.RulesSkipped, len(config.Rules))
|
|
}
|
|
}
|
|
|
|
func TestInspectWithEnforcedRules(t *testing.T) {
|
|
db := createTestDatabase()
|
|
config := GetDefaultConfig()
|
|
|
|
// Enable only one rule and enforce it
|
|
for name := range config.Rules {
|
|
rule := config.Rules[name]
|
|
rule.Enabled = "off"
|
|
config.Rules[name] = rule
|
|
}
|
|
|
|
primaryKeyRule := config.Rules["primary_key_naming"]
|
|
primaryKeyRule.Enabled = "enforce"
|
|
primaryKeyRule.Pattern = "^id$"
|
|
config.Rules["primary_key_naming"] = primaryKeyRule
|
|
|
|
inspector := NewInspector(db, config)
|
|
report, err := inspector.Inspect()
|
|
|
|
if err != nil {
|
|
t.Fatalf("Inspect() returned error: %v", err)
|
|
}
|
|
|
|
if report.Summary.RulesChecked != 1 {
|
|
t.Errorf("Inspect() RulesChecked = %d, want 1", report.Summary.RulesChecked)
|
|
}
|
|
|
|
// All results should be at error level for enforced rules
|
|
for _, violation := range report.Violations {
|
|
if violation.Level != "error" {
|
|
t.Errorf("Enforced rule violation has Level = %q, want \"error\"", violation.Level)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateSummary(t *testing.T) {
|
|
db := createTestDatabase()
|
|
config := GetDefaultConfig()
|
|
inspector := NewInspector(db, config)
|
|
|
|
results := []ValidationResult{
|
|
{RuleName: "rule1", Passed: true, Level: "error"},
|
|
{RuleName: "rule2", Passed: false, Level: "error"},
|
|
{RuleName: "rule3", Passed: false, Level: "warning"},
|
|
{RuleName: "rule4", Passed: true, Level: "warning"},
|
|
}
|
|
|
|
summary := inspector.generateSummary(results)
|
|
|
|
if summary.PassedCount != 2 {
|
|
t.Errorf("generateSummary() PassedCount = %d, want 2", summary.PassedCount)
|
|
}
|
|
|
|
if summary.ErrorCount != 1 {
|
|
t.Errorf("generateSummary() ErrorCount = %d, want 1", summary.ErrorCount)
|
|
}
|
|
|
|
if summary.WarningCount != 1 {
|
|
t.Errorf("generateSummary() WarningCount = %d, want 1", summary.WarningCount)
|
|
}
|
|
}
|
|
|
|
func TestHasErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
report *InspectorReport
|
|
want bool
|
|
}{
|
|
{
|
|
name: "with errors",
|
|
report: &InspectorReport{
|
|
Summary: ReportSummary{
|
|
ErrorCount: 5,
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "without errors",
|
|
report: &InspectorReport{
|
|
Summary: ReportSummary{
|
|
ErrorCount: 0,
|
|
WarningCount: 3,
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.report.HasErrors(); got != tt.want {
|
|
t.Errorf("HasErrors() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetValidator(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
functionName string
|
|
wantExists bool
|
|
}{
|
|
{"primary_key_naming", "primary_key_naming", true},
|
|
{"primary_key_datatype", "primary_key_datatype", true},
|
|
{"foreign_key_column_naming", "foreign_key_column_naming", true},
|
|
{"table_regexpr", "table_regexpr", true},
|
|
{"column_regexpr", "column_regexpr", true},
|
|
{"reserved_words", "reserved_words", true},
|
|
{"have_primary_key", "have_primary_key", true},
|
|
{"orphaned_foreign_key", "orphaned_foreign_key", true},
|
|
{"circular_dependency", "circular_dependency", true},
|
|
{"unknown_function", "unknown_function", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, exists := getValidator(tt.functionName)
|
|
if exists != tt.wantExists {
|
|
t.Errorf("getValidator(%q) exists = %v, want %v", tt.functionName, exists, tt.wantExists)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateResult(t *testing.T) {
|
|
result := createResult(
|
|
"test_rule",
|
|
true,
|
|
"Test message",
|
|
"schema.table.column",
|
|
map[string]interface{}{
|
|
"key1": "value1",
|
|
"key2": 42,
|
|
},
|
|
)
|
|
|
|
if result.RuleName != "test_rule" {
|
|
t.Errorf("createResult() RuleName = %q, want \"test_rule\"", result.RuleName)
|
|
}
|
|
|
|
if !result.Passed {
|
|
t.Error("createResult() Passed = false, want true")
|
|
}
|
|
|
|
if result.Message != "Test message" {
|
|
t.Errorf("createResult() Message = %q, want \"Test message\"", result.Message)
|
|
}
|
|
|
|
if result.Location != "schema.table.column" {
|
|
t.Errorf("createResult() Location = %q, want \"schema.table.column\"", result.Location)
|
|
}
|
|
|
|
if len(result.Context) != 2 {
|
|
t.Errorf("createResult() Context length = %d, want 2", len(result.Context))
|
|
}
|
|
}
|