diff --git a/fixtures-code/process-compose-circular.yaml b/fixtures-code/process-compose-circular.yaml index 83a46c7..72cee67 100644 --- a/fixtures-code/process-compose-circular.yaml +++ b/fixtures-code/process-compose-circular.yaml @@ -8,18 +8,13 @@ processes: process2: command: "echo process2" - availability: - restart: "on_failure" depends_on: - process3: + process1: condition: process_completed_successfully process3: command: "echo process3" - availability: - restart: "on_failure" - backoff_seconds: 2 -# depends_on: -# process1: -# condition: process_completed_successfully + depends_on: + process1: + condition: process_completed_successfully diff --git a/fixtures-code/process-compose-non-circular.yaml b/fixtures-code/process-compose-non-circular.yaml new file mode 100644 index 0000000..be072b0 --- /dev/null +++ b/fixtures-code/process-compose-non-circular.yaml @@ -0,0 +1,11 @@ +version: "0.5" +processes: + process1: + command: "echo process1" + + process3: + command: "echo process3" + depends_on: + process1: + condition: process_completed_successfully + diff --git a/src/app/system_test.go b/src/app/system_test.go index 81aaba7..a5682e0 100644 --- a/src/app/system_test.go +++ b/src/app/system_test.go @@ -130,28 +130,27 @@ func TestSystem_TestComposeChainExit(t *testing.T) { }) } -//func TestSystem_TestComposeCircular(t *testing.T) { -// fixture := filepath.Join("..", "..", "fixtures", "process-compose-circular.yaml") -// t.Run(fixture, func(t *testing.T) { -// project, err := loader.Load(&loader.LoaderOptions{ -// FileNames: []string{fixture}, -// }) -// if err != nil { -// t.Errorf(err.Error()) -// return -// } -// runner, err := NewProjectRunner(project, []string{}, false) -// if err != nil { -// t.Errorf(err.Error()) -// return -// } -// exitCode := runner.Run() -// want := 42 -// if want != exitCode { -// t.Errorf("Project.Run() = %v, want %v", exitCode, want) -// } -// }) -//} +func TestSystem_TestComposeCircular(t *testing.T) { + fixture1 := filepath.Join("..", "..", "fixtures-code", "process-compose-circular.yaml") + fixture2 := filepath.Join("..", "..", "fixtures-code", "process-compose-non-circular.yaml") + t.Run(fixture1, func(t *testing.T) { + _, err := loader.Load(&loader.LoaderOptions{ + FileNames: []string{fixture1}, + }) + if err == nil { + t.Errorf("should fail on cirlcular dependency") + return + } + + _, err = loader.Load(&loader.LoaderOptions{ + FileNames: []string{fixture2}, + }) + if err != nil { + t.Errorf(err.Error()) + return + } + }) +} func TestSystem_TestComposeScale(t *testing.T) { fixture := filepath.Join("..", "..", "fixtures-code", "process-compose-scale.yaml") diff --git a/src/loader/loader.go b/src/loader/loader.go index c1e21c2..769b783 100644 --- a/src/loader/loader.go +++ b/src/loader/loader.go @@ -27,7 +27,7 @@ func Load(opts *LoaderOptions) (*types.Project, error) { opts.projects = append(opts.projects, p) } mergedProject, err := merge(opts) - mergedProject.ValidateAfterMerge() + err = mergedProject.ValidateAfterMerge() return mergedProject, err } diff --git a/src/types/validators.go b/src/types/validators.go index 6a3aae1..582472f 100644 --- a/src/types/validators.go +++ b/src/types/validators.go @@ -1,6 +1,7 @@ package types import ( + "fmt" "github.com/f1bonacc1/process-compose/src/command" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -14,9 +15,10 @@ func (p *Project) Validate() { p.validateProcessConfig() } -func (p *Project) ValidateAfterMerge() { +func (p *Project) ValidateAfterMerge() error { p.assignDefaultProcessValues() p.cloneReplicas() + return p.validateNoCircularDependencies() } func (p *Project) validateLogLevel() { @@ -100,3 +102,41 @@ func (p *Project) cloneReplicas() { p.Processes[proc.ReplicaName] = proc } } + +func (p *Project) validateNoCircularDependencies() error { + visited := make(map[string]bool, len(p.Processes)) + stack := make(map[string]bool) + for name := range p.Processes { + if !visited[name] { + if p.isCyclicHelper(name, visited, stack) { + return fmt.Errorf("circular dependency found in %s", name) + } + } + } + return nil +} + +func (p *Project) isCyclicHelper(procName string, visited map[string]bool, stack map[string]bool) bool { + visited[procName] = true + stack[procName] = true + + processes, err := p.getProcesses(procName) + if err != nil { + return false + } + for _, process := range processes { + dependencies := process.GetDependencies() + for _, neighbor := range dependencies { + if !visited[neighbor] { + if p.isCyclicHelper(neighbor, visited, stack) { + return true + } + } else if stack[neighbor] { + return true + } + } + } + + stack[procName] = false + return false +}