package declaration import ( "strings" "testing" ) func aProcess() *Process { return &Process{ ID: "server", Type: TypeProcess, Name: "greeter", Source: "https://store.invalid/greeter/daemon", Digest: "sha256:" + strings.Repeat("a", 64), Run: []string{"node", "index.js"}, } } // A process is part of the vocabulary, or a declaration carrying one is refused whole. func TestAProcessIsSomethingTheHostSpeaks(t *testing.T) { var found bool for _, kind := range Vocabulary() { if kind == TypeProcess { found = true } } if !found { t.Fatal("a process cannot be declared, so a module that declares one is refused") } if newOf(TypeProcess) == nil { t.Fatal("the decoder has no daemon, so one would be refused as an unknown kind") } } // **Pinned by digest, like everything else that crosses a network.** A bundle fetched by a // reference somebody can repoint is not pinned, and it is the one thing on a machine that would // then be running code nobody reviewed. func TestAProcesssBundleMustBePinned(t *testing.T) { for _, bad := range []string{"", "latest", "sha256:short", strings.Repeat("a", 64)} { d := aProcess() d.Digest = bad if problems := d.validate("a process", false); len(problems) == 0 { t.Fatalf("a process pinned by %q was accepted", bad) } } } // What to run is named, never inferred. Guessing an entrypoint from which files are present makes // a process change what it runs when somebody adds a file. func TestAProcessMustSayWhatToRun(t *testing.T) { d := aProcess() d.Run = nil if problems := d.validate("a process", false); len(problems) == 0 { t.Fatal("a process with no command was accepted") } } // Its name becomes a unit name and a path, so a separator in it would write somewhere nobody meant. func TestAProcesssNameCannotEscapeItsUnit(t *testing.T) { for _, bad := range []string{"", "../escape", "two words", "a/b"} { d := aProcess() d.Name = bad if problems := d.validate("a process", false); len(problems) == 0 { t.Fatalf("a process called %q was accepted", bad) } } } // And a well-formed one is accepted, or the tests above prove only that everything is refused. func TestAWellFormedDaemonIsAccepted(t *testing.T) { if problems := aProcess().validate("a process", false); len(problems) != 0 { t.Fatalf("a well-formed daemon was refused: %v", problems) } } // **The modes are exclusive, and saying so is the point of having one kind.** Something that runs // once does not run on a schedule; something not running between fires cannot be restarted when a // file changes. A container's modes carry the same rule, and this is the same rule because it is // the same thing hosted differently. func TestTheModesAreExclusive(t *testing.T) { both := aProcess() both.RunOnce = true both.Schedule = "0 3 * * *" if problems := both.validate("a process", false); len(problems) == 0 { t.Fatal("a process that runs once and on a schedule was accepted") } watching := aProcess() watching.Schedule = "0 3 * * *" watching.RestartOn = []string{"some-file"} if problems := watching.validate("a process", false); len(problems) == 0 { t.Fatal("a scheduled process was given something to restart on, and it is never running") } } // A cadence that is not a cadence is refused near its author, rather than by a machine at the far // end of a declaration. func TestAScheduleMustBeACadence(t *testing.T) { for _, bad := range []string{"often", "0 3 * *", "99 3 * * *"} { p := aProcess() p.Schedule = bad if problems := p.validate("a process", false); len(problems) == 0 { t.Fatalf("a process scheduled %q was accepted", bad) } } } // And each mode on its own is accepted, or the tests above prove only that everything is refused. func TestEachModeOnItsOwnIsAccepted(t *testing.T) { once := aProcess() once.RunOnce = true if problems := once.validate("a process", false); len(problems) != 0 { t.Fatalf("a step was refused: %v", problems) } every := aProcess() every.Schedule = "0 3 * * *" if problems := every.validate("a process", false); len(problems) != 0 { t.Fatalf("a scheduled process was refused: %v", problems) } }