package steprun import ( "context" "encoding/json" "errors" "strings" "sync" "testing" "time" sdkstep "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/kvlimit" ) // Design 47 G A-254, A-290: a build seat's step-ended of a run carries the run's answer, the last line of its program's standard // output: inline as `output` up to the event bound, else as `output-object`, the object the holder keeps, which the verb `output` // answers. The step's key holds the answer only within what the steps state holds per value: over it, a check's whole answer or a // run's output is the object output. the key names, kept once; the key and step-ended always say the same outcome, a run // whose program exited other than 0 failed in both, its answer kept (A-290). func TestStepEndedOfARunCarriesItsAnswer(t *testing.T) { big := `"` + strings.Repeat("x", sdkstep.StepEndedInlineMost) + `"` overKey := `"` + strings.Repeat("y", kvlimit.ModuleStateValueBytes) + `"` // over the steps state, within the event checkAnswer := map[string]any{"fetch": map[string]any{"workspace": "/w"}, "levels": []any{map[string]any{"level": "unit", "said": strings.Repeat("z", kvlimit.ModuleStateValueBytes)}}} for _, c := range []struct { name string check bool // a check ask answering checkAnswer, not a run answer sdkstep.RunAnswer keepErr error outcome string inline string objectKept bool // step-ended names the object keyObject string // what the object the key names holds: "" none, else the answer's bytes said string }{ {name: "a small answer is inline", answer: sdkstep.RunAnswer{Output: json.RawMessage(`{"verdict":true}`)}, outcome: sdkstep.OutcomePassed, inline: `{"verdict":true}`}, {name: "an answer over the event bound is an object", answer: sdkstep.RunAnswer{Output: json.RawMessage(big)}, outcome: sdkstep.OutcomePassed, objectKept: true, keyObject: big}, {name: "an answer over the steps state within the event is inline and the object the key names", answer: sdkstep.RunAnswer{Built: true, Key: "k", Output: json.RawMessage(overKey)}, outcome: sdkstep.OutcomePassed, inline: overKey, keyObject: overKey}, {name: "a program that exits other than 0 failed, its answer kept", answer: sdkstep.RunAnswer{Exit: 3, Output: json.RawMessage(`"no"`), Said: "bad input"}, outcome: sdkstep.OutcomeFailed, inline: `"no"`, said: "the program exited 3: bad input"}, {name: "an answer the holder cannot keep is said", answer: sdkstep.RunAnswer{Output: json.RawMessage(big)}, keepErr: errors.New("store full"), outcome: sdkstep.OutcomeFailed, said: "store full"}, {name: "a check's answer over the steps state is the object the key names", check: true, outcome: sdkstep.OutcomePassed, keyObject: "check"}, {name: "a check's answer the holder cannot keep fails in its key and its end alike", check: true, keepErr: errors.New("store full"), outcome: sdkstep.OutcomeFailed, said: "store full"}, } { t.Run(c.name, func(t *testing.T) { src, st, sy := NewFakeAsks(), newSteps(), &sayer{} a := testAsk(sdkstep.AskCheck) var answer any = c.answer if c.check { answer = checkAnswer } else { a.Check = nil in, _ := sdkstep.RunInputOf("seat:mesh-catalog.input", []byte(`{"v":1}`)) commit := "0123456789abcdef0123456789abcdef01234567" a.Run = &sdkstep.RunJob{Program: sdkstep.RunProgram{Repository: "novox/mesh-delivery", Commit: commit, Dir: "modules/x", Package: "./cmd/x"}, Input: in} } _ = src.PublishAsk(a) var mu sync.Mutex kept, keeps := map[string][]byte{}, 0 p := &Puller{Source: src, Language: sdkstep.AskGo, Node: "shanks", Put: st.put, Capacity: func() int { return 1 }, PullWait: 10 * time.Millisecond, Heartbeat: time.Hour, Say: sy.say, KeepOutput: func(object string, body []byte) error { mu.Lock() defer mu.Unlock() keeps++ if c.keepErr != nil { return c.keepErr } kept[object] = body return nil }, Run: func(ctx context.Context, a sdkstep.StepAsk) (any, error) { return answer, nil }} ctx, cancel := context.WithCancel(context.Background()) defer cancel() go p.Pull(ctx) waitFor(t, "step-ended", func() bool { return len(sy.of("step-ended")) == 1 }) end := sy.of("step-ended")[0].body if end["outcome"] != c.outcome { t.Fatalf("outcome %v, want %s (%v)", end["outcome"], c.outcome, end) } if c.inline != "" && end["output"] != c.inline { t.Fatalf("output %.80v, want %.80s inline", end["output"], c.inline) } if c.said != "" && !strings.Contains(end["said"].(string), c.said) { t.Fatalf("said %v, want it to hold %q", end["said"], c.said) } key, _ := sdkstep.StepKey("shanks", a.ID) step, _ := st.get(key) if raw, err := sdkstep.EncodeStep(step); err != nil || kvlimit.Check("steps", key, len(raw), kvlimit.ModuleStateValueBytes) != nil { t.Fatalf("the step's key is %d bytes (%v), over what the steps state holds", len(raw), err) } if want := map[string]string{sdkstep.OutcomePassed: sdkstep.StepPassed, sdkstep.OutcomeFailed: sdkstep.StepFailed}[c.outcome]; step.State != want { t.Fatalf("the key ended %s (%s), step-ended %s: no step ends passed in one place and failed in another", step.State, step.Reason, c.outcome) } if c.answer.Exit != 0 && (!strings.Contains(step.Reason, c.said) || !strings.Contains(string(step.Result), c.inline)) { t.Fatalf("the key's reason %q and answer %s, want the exit said and the answer kept", step.Reason, step.Result) } mu.Lock() defer mu.Unlock() name := sdkstep.OutputObjectName(a.ID) if c.objectKept { if end["output-object"] != name || end["output"] != nil || string(kept[name]) != big { t.Fatalf("end %v and kept %d bytes, want the object %s holding the whole answer and no inline output", end["output-object"], len(kept[name]), name) } } else if end["output-object"] != nil { t.Fatalf("an object %v was named, want none", end["output-object"]) } switch { case c.keyObject == "": if step.OutputObject != "" || (c.keepErr == nil && keeps != 0) { t.Fatalf("the key names %q and %d objects were kept, want none", step.OutputObject, keeps) } case c.check: var whole map[string]any if step.OutputObject != name || len(step.Result) != 0 || keeps != 1 || json.Unmarshal(kept[name], &whole) != nil || whole["fetch"] == nil { t.Fatalf("the key names %q with result %.80s, %d keeps; want the object %s holding the whole check answer, kept once", step.OutputObject, step.Result, keeps, name) } default: var rest sdkstep.RunAnswer if step.OutputObject != name || keeps != 1 || string(kept[name]) != c.keyObject || json.Unmarshal(step.Result, &rest) != nil || rest.Output != nil || rest.Key != c.answer.Key { t.Fatalf("the key names %q with result %.80s, %d keeps; want the object %s holding the output, kept once, and the run answer without it", step.OutputObject, step.Result, keeps, name) } } }) } }