diff --git a/internal/jobspec/spec_test.go b/internal/jobspec/spec_test.go index 5a75228..d1f0a3e 100644 --- a/internal/jobspec/spec_test.go +++ b/internal/jobspec/spec_test.go @@ -1,6 +1,9 @@ package jobspec import ( + "os" + "path/filepath" + "strings" "testing" ) @@ -58,3 +61,223 @@ task "no-cmd" {} t.Fatal("expected error for missing command") } } + +func TestParse_GoldenFiles(t *testing.T) { + cases := []struct { + name string + file string + wantJob string + wantJobType string + wantTasks int + checkTask func(t *testing.T, s *Spec) + }{ + { + name: "single_task", + file: "valid_single_task.hcl", + wantJob: "single", + wantTasks: 1, + wantJobType: "", + checkTask: func(t *testing.T, s *Spec) { + if s.Tasks[0].Name != "solo" { + t.Errorf("task name = %q, want solo", s.Tasks[0].Name) + } + if s.Tasks[0].Command != "/bin/true" { + t.Errorf("command = %q, want /bin/true", s.Tasks[0].Command) + } + }, + }, + { + name: "multi_task", + file: "valid_multi_task.hcl", + wantJob: "multi", + wantJobType: "batch", + wantTasks: 3, + checkTask: func(t *testing.T, s *Spec) { + byName := map[string]TaskSpec{} + for _, tk := range s.Tasks { + byName[tk.Name] = tk + } + if _, ok := byName["build"]; !ok { + t.Errorf("missing task 'build'") + } + if _, ok := byName["test"]; !ok { + t.Errorf("missing task 'test'") + } + if len(byName["test"].Env) != 2 { + t.Errorf("test env count = %d, want 2", len(byName["test"].Env)) + } + if _, ok := byName["deploy"]; !ok { + t.Errorf("missing task 'deploy'") + } + }, + }, + { + name: "env_vars", + file: "valid_env_vars.hcl", + wantJob: "envvars", + wantTasks: 1, + checkTask: func(t *testing.T, s *Spec) { + if len(s.Tasks[0].Env) != 3 { + t.Errorf("env count = %d, want 3", len(s.Tasks[0].Env)) + } + want := "FOO=bar" + if s.Tasks[0].Env[0] != want { + t.Errorf("env[0] = %q, want %q", s.Tasks[0].Env[0], want) + } + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + path := filepath.Join("testdata", tc.file) + spec, err := ParseFile(path) + if err != nil { + t.Fatalf("ParseFile(%s): %v", tc.file, err) + } + if spec.Job.Name != tc.wantJob { + t.Errorf("job name = %q, want %q", spec.Job.Name, tc.wantJob) + } + if tc.wantJobType != "" && spec.Job.Type != tc.wantJobType { + t.Errorf("job type = %q, want %q", spec.Job.Type, tc.wantJobType) + } + if len(spec.Tasks) != tc.wantTasks { + t.Fatalf("tasks = %d, want %d", len(spec.Tasks), tc.wantTasks) + } + if tc.checkTask != nil { + tc.checkTask(t, spec) + } + }) + } +} + +func TestParse_ErrorPaths(t *testing.T) { + cases := []struct { + name string + file string + wantErr string + useParse bool + hcl string + }{ + {name: "no_tasks", file: "err_no_tasks.hcl", wantErr: "at least one task"}, + {name: "missing_command", file: "err_missing_command.hcl", wantErr: "required"}, + {name: "malformed", file: "err_malformed.hcl", wantErr: "decode hcl"}, + {name: "missing_job", file: "err_missing_job.hcl", wantErr: "Missing job block"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + path := filepath.Join("testdata", tc.file) + _, err := ParseFile(path) + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.wantErr) + } + if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("error = %q, want it to contain %q", err.Error(), tc.wantErr) + } + }) + } +} + +func TestParse_EmptyFile(t *testing.T) { + _, err := Parse([]byte(""), "empty.hcl") + if err == nil { + t.Fatal("expected error for empty file") + } +} + +func TestParse_MalformedHCL(t *testing.T) { + _, err := Parse([]byte("job = "), "bad.hcl") + if err == nil { + t.Fatal("expected error for malformed HCL") + } + if !strings.Contains(err.Error(), "decode hcl") { + t.Errorf("error = %q, want it to contain 'decode hcl'", err.Error()) + } +} + +func TestParseFile_Nonexistent(t *testing.T) { + _, err := ParseFile(filepath.Join("testdata", "does_not_exist.hcl")) + if err == nil { + t.Fatal("expected error for nonexistent file") + } + if !strings.Contains(err.Error(), "read spec file") { + t.Errorf("error = %q, want it to contain 'read spec file'", err.Error()) + } +} + +func TestParseFile_ReadError(t *testing.T) { + // Directory exists but is not readable as a file. + _, err := ParseFile("testdata") + if err == nil { + t.Fatal("expected error when ParseFile target is a directory") + } +} + +func TestSpec_Validate(t *testing.T) { + cases := []struct { + name string + spec *Spec + wantErr string + }{ + { + name: "empty_job_name", + spec: &Spec{Job: JobSpec{Name: " "}, Tasks: []TaskSpec{{Name: "t", Command: "/bin/echo"}}}, + wantErr: "job name is required", + }, + { + name: "no_tasks", + spec: &Spec{Job: JobSpec{Name: "x"}}, + wantErr: "at least one task is required", + }, + { + name: "valid", + spec: &Spec{Job: JobSpec{Name: "x"}, Tasks: []TaskSpec{{Name: "t", Command: "/bin/echo"}}}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := tc.spec.Validate() + if tc.wantErr == "" { + if err != nil { + t.Errorf("Validate: got %v, want nil", err) + } + return + } + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.wantErr) + } + if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("error = %q, want it to contain %q", err.Error(), tc.wantErr) + } + }) + } +} + +func TestSpec_Validate_RoundTripFromParse(t *testing.T) { + path := filepath.Join("testdata", "valid_single_task.hcl") + spec, err := ParseFile(path) + if err != nil { + t.Fatalf("ParseFile: %v", err) + } + if err := spec.Validate(); err != nil { + t.Errorf("Validate on parsed spec: %v", err) + } +} + +func TestParseFile_GoldenFilesExist(t *testing.T) { + // Guard against accidentally removing testdata fixtures. + files := []string{ + "valid_single_task.hcl", + "valid_multi_task.hcl", + "valid_env_vars.hcl", + "err_no_tasks.hcl", + "err_missing_command.hcl", + "err_malformed.hcl", + "err_missing_job.hcl", + } + for _, f := range files { + path := filepath.Join("testdata", f) + if _, err := os.Stat(path); err != nil { + t.Errorf("missing testdata fixture %s: %v", f, err) + } + } +} diff --git a/internal/jobspec/testdata/err_malformed.hcl b/internal/jobspec/testdata/err_malformed.hcl new file mode 100644 index 0000000..cd64b18 --- /dev/null +++ b/internal/jobspec/testdata/err_malformed.hcl @@ -0,0 +1 @@ +job "x" { command = invalid } \ No newline at end of file diff --git a/internal/jobspec/testdata/err_missing_command.hcl b/internal/jobspec/testdata/err_missing_command.hcl new file mode 100644 index 0000000..d273506 --- /dev/null +++ b/internal/jobspec/testdata/err_missing_command.hcl @@ -0,0 +1,3 @@ +job "x" {} + +task "nocmd" {} \ No newline at end of file diff --git a/internal/jobspec/testdata/err_missing_job.hcl b/internal/jobspec/testdata/err_missing_job.hcl new file mode 100644 index 0000000..420f89b --- /dev/null +++ b/internal/jobspec/testdata/err_missing_job.hcl @@ -0,0 +1 @@ +task "x" { command = "/bin/echo" } \ No newline at end of file diff --git a/internal/jobspec/testdata/err_no_tasks.hcl b/internal/jobspec/testdata/err_no_tasks.hcl new file mode 100644 index 0000000..49f06d3 --- /dev/null +++ b/internal/jobspec/testdata/err_no_tasks.hcl @@ -0,0 +1 @@ +job "empty" {} \ No newline at end of file diff --git a/internal/jobspec/testdata/valid_env_vars.hcl b/internal/jobspec/testdata/valid_env_vars.hcl new file mode 100644 index 0000000..aeac2ea --- /dev/null +++ b/internal/jobspec/testdata/valid_env_vars.hcl @@ -0,0 +1,6 @@ +job "envvars" {} + +task "runner" { + command = "/bin/printenv" + env = ["FOO=bar", "BAZ=qux", "EMPTY="] +} \ No newline at end of file diff --git a/internal/jobspec/testdata/valid_multi_task.hcl b/internal/jobspec/testdata/valid_multi_task.hcl new file mode 100644 index 0000000..b4d3aa5 --- /dev/null +++ b/internal/jobspec/testdata/valid_multi_task.hcl @@ -0,0 +1,19 @@ +job "multi" { + type = "batch" +} + +task "build" { + command = "/bin/echo" + args = ["build", "done"] +} + +task "test" { + command = "/usr/bin/go" + args = ["test", "./..."] + env = ["GOCACHE=/tmp/gocache", "GOFLAGS=-v"] +} + +task "deploy" { + command = "/bin/sh" + args = ["-c", "echo deploying"] +} \ No newline at end of file diff --git a/internal/jobspec/testdata/valid_single_task.hcl b/internal/jobspec/testdata/valid_single_task.hcl new file mode 100644 index 0000000..c990b36 --- /dev/null +++ b/internal/jobspec/testdata/valid_single_task.hcl @@ -0,0 +1,5 @@ +job "single" {} + +task "solo" { + command = "/bin/true" +} \ No newline at end of file