Files
silo/internal/jobdef/jobdef_test.go
Forbes f60c25983b feat: add YAML job definition parser and example definitions
New package internal/jobdef mirrors the schema package pattern:
- Load/LoadAll/Validate for YAML job definitions
- Supports trigger types: revision_created, bom_changed, manual, schedule
- Supports scope types: item, assembly, project
- Supports compute types: validate, rebuild, diff, export, custom
- Defaults: timeout=600s, max_retries=1, priority=100

Example definitions in jobdefs/:
- assembly-validate.yaml: incremental validation on revision_created
- part-export-step.yaml: STEP export on manual trigger

11 unit tests, all passing.
2026-02-14 13:06:24 -06:00

329 lines
6.9 KiB
Go

package jobdef
import (
"os"
"path/filepath"
"testing"
)
func TestLoadValid(t *testing.T) {
dir := t.TempDir()
content := `
job:
name: test-job
version: 1
description: "A test job"
trigger:
type: manual
scope:
type: item
compute:
type: validate
command: create-validate
runner:
tags: [create]
timeout: 300
max_retries: 2
priority: 50
`
path := filepath.Join(dir, "test-job.yaml")
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("writing test file: %v", err)
}
def, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if def.Name != "test-job" {
t.Errorf("name = %q, want %q", def.Name, "test-job")
}
if def.Version != 1 {
t.Errorf("version = %d, want 1", def.Version)
}
if def.Trigger.Type != "manual" {
t.Errorf("trigger type = %q, want %q", def.Trigger.Type, "manual")
}
if def.Scope.Type != "item" {
t.Errorf("scope type = %q, want %q", def.Scope.Type, "item")
}
if def.Compute.Type != "validate" {
t.Errorf("compute type = %q, want %q", def.Compute.Type, "validate")
}
if def.Compute.Command != "create-validate" {
t.Errorf("compute command = %q, want %q", def.Compute.Command, "create-validate")
}
if len(def.Runner.Tags) != 1 || def.Runner.Tags[0] != "create" {
t.Errorf("runner tags = %v, want [create]", def.Runner.Tags)
}
if def.Timeout != 300 {
t.Errorf("timeout = %d, want 300", def.Timeout)
}
if def.MaxRetries != 2 {
t.Errorf("max_retries = %d, want 2", def.MaxRetries)
}
if def.Priority != 50 {
t.Errorf("priority = %d, want 50", def.Priority)
}
}
func TestLoadDefaults(t *testing.T) {
dir := t.TempDir()
content := `
job:
name: minimal
trigger:
type: manual
scope:
type: item
compute:
type: custom
command: do-something
`
path := filepath.Join(dir, "minimal.yaml")
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("writing test file: %v", err)
}
def, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if def.Timeout != 600 {
t.Errorf("default timeout = %d, want 600", def.Timeout)
}
if def.MaxRetries != 1 {
t.Errorf("default max_retries = %d, want 1", def.MaxRetries)
}
if def.Priority != 100 {
t.Errorf("default priority = %d, want 100", def.Priority)
}
if def.Version != 1 {
t.Errorf("default version = %d, want 1", def.Version)
}
}
func TestLoadInvalidTriggerType(t *testing.T) {
dir := t.TempDir()
content := `
job:
name: bad-trigger
trigger:
type: invalid_trigger
scope:
type: item
compute:
type: validate
command: create-validate
`
path := filepath.Join(dir, "bad.yaml")
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("writing test file: %v", err)
}
_, err := Load(path)
if err == nil {
t.Fatal("expected error for invalid trigger type")
}
}
func TestLoadMissingName(t *testing.T) {
dir := t.TempDir()
content := `
job:
trigger:
type: manual
scope:
type: item
compute:
type: validate
command: create-validate
`
path := filepath.Join(dir, "no-name.yaml")
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("writing test file: %v", err)
}
_, err := Load(path)
if err == nil {
t.Fatal("expected error for missing name")
}
}
func TestLoadMissingCommand(t *testing.T) {
dir := t.TempDir()
content := `
job:
name: no-command
trigger:
type: manual
scope:
type: item
compute:
type: validate
`
path := filepath.Join(dir, "no-cmd.yaml")
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("writing test file: %v", err)
}
_, err := Load(path)
if err == nil {
t.Fatal("expected error for missing command")
}
}
func TestLoadAllDirectory(t *testing.T) {
dir := t.TempDir()
job1 := `
job:
name: job-one
trigger:
type: manual
scope:
type: item
compute:
type: validate
command: create-validate
`
job2 := `
job:
name: job-two
trigger:
type: revision_created
scope:
type: assembly
compute:
type: export
command: create-export
`
if err := os.WriteFile(filepath.Join(dir, "one.yaml"), []byte(job1), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "two.yml"), []byte(job2), 0644); err != nil {
t.Fatal(err)
}
// Non-YAML file should be ignored
if err := os.WriteFile(filepath.Join(dir, "readme.txt"), []byte("ignore me"), 0644); err != nil {
t.Fatal(err)
}
defs, err := LoadAll(dir)
if err != nil {
t.Fatalf("LoadAll: %v", err)
}
if len(defs) != 2 {
t.Fatalf("loaded %d definitions, want 2", len(defs))
}
if _, ok := defs["job-one"]; !ok {
t.Error("job-one not found")
}
if _, ok := defs["job-two"]; !ok {
t.Error("job-two not found")
}
}
func TestLoadAllEmptyDirectory(t *testing.T) {
dir := t.TempDir()
defs, err := LoadAll(dir)
if err != nil {
t.Fatalf("LoadAll: %v", err)
}
if len(defs) != 0 {
t.Errorf("loaded %d definitions from empty dir, want 0", len(defs))
}
}
func TestLoadWithFilter(t *testing.T) {
dir := t.TempDir()
content := `
job:
name: filtered-job
trigger:
type: revision_created
filter:
item_type: assembly
scope:
type: assembly
compute:
type: validate
command: create-validate
`
path := filepath.Join(dir, "filtered.yaml")
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("writing test file: %v", err)
}
def, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if def.Trigger.Filter["item_type"] != "assembly" {
t.Errorf("filter item_type = %q, want %q", def.Trigger.Filter["item_type"], "assembly")
}
}
func TestLoadWithArgs(t *testing.T) {
dir := t.TempDir()
content := `
job:
name: args-job
trigger:
type: manual
scope:
type: item
compute:
type: export
command: create-export
args:
format: step
include_mesh: true
`
path := filepath.Join(dir, "args.yaml")
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("writing test file: %v", err)
}
def, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if def.Compute.Args["format"] != "step" {
t.Errorf("args format = %v, want %q", def.Compute.Args["format"], "step")
}
if def.Compute.Args["include_mesh"] != true {
t.Errorf("args include_mesh = %v, want true", def.Compute.Args["include_mesh"])
}
}
func TestValidateInvalidScopeType(t *testing.T) {
d := &Definition{
Name: "test",
Trigger: TriggerConfig{Type: "manual"},
Scope: ScopeConfig{Type: "galaxy"},
Compute: ComputeConfig{Type: "validate", Command: "create-validate"},
}
if err := d.Validate(); err == nil {
t.Fatal("expected error for invalid scope type")
}
}
func TestValidateInvalidComputeType(t *testing.T) {
d := &Definition{
Name: "test",
Trigger: TriggerConfig{Type: "manual"},
Scope: ScopeConfig{Type: "item"},
Compute: ComputeConfig{Type: "teleport", Command: "beam-up"},
}
if err := d.Validate(); err == nil {
t.Fatal("expected error for invalid compute type")
}
}