package build import ( "context" "encoding/json" "errors" "strings" "sync" "testing" "time" "git.novox.be/novox/mesh-sdk/go/delivery" ) type call struct { node, key string body any } // fakeCaller answers a long tool as the build modules do: at once, with the step and its key. type fakeCaller struct { calls []call answer string err error } func (f *fakeCaller) CallOn(node, key string, body any) (json.RawMessage, error) { f.calls = append(f.calls, call{node, key, body}) if f.err != nil { return nil, f.err } return json.RawMessage(f.answer), nil } // fakeSteps holds the steps values by `/`; Wait on a key it does not hold blocks until the context ends. type fakeSteps struct { mu sync.Mutex steps map[string]delivery.Step // during, when set, is called as a wait starts and the function it answers as the wait ends. during func(key string) func() } func (f *fakeSteps) Wait(ctx context.Context, module, key string) (delivery.Step, error) { f.mu.Lock() s, ok := f.steps[module+"/"+key] during := f.during f.mu.Unlock() if ok { if during != nil { defer during(key)() } return s, nil } <-ctx.Done() return delivery.Step{}, context.Cause(ctx) } func stepAt(node, id, state, reason string, result string) delivery.Step { s := delivery.Step{ID: id, Tool: "build_go_fetch", Node: node, State: state, Reason: reason} if result != "" { s.Result = json.RawMessage(result) } return s } func TestRunCallsTheToolOnTheNodeAndReadsTheStepsKey(t *testing.T) { // 1607/AC5 caller := &fakeCaller{answer: `{"step":"abc1","key":"ace.abc1","state":"running"}`} steps := &fakeSteps{steps: map[string]delivery.Step{"mesh-build-go/ace.abc1": stepAt("ace", "abc1", delivery.StepPassed, "", `{"workspace":"w1","commit":"c"}`)}} res, err := Runner{Caller: caller, Steps: steps, Bound: time.Second}.Run(context.Background(), "mesh-build-go", "ace", "build_go_fetch", map[string]any{"repository": "novox/r", "commit": "c"}) if err != nil { t.Fatal(err) } if len(caller.calls) != 1 || caller.calls[0].node != "ace" || caller.calls[0].key != "mesh-build-go.build_go_fetch" { t.Errorf("calls = %+v, want one on ace for mesh-build-go.build_go_fetch", caller.calls) } var fr struct{ Workspace string } if err := json.Unmarshal(res, &fr); err != nil || fr.Workspace != "w1" { t.Errorf("result = %s (%v), want the step's result", res, err) } } func TestRunFailsOnAFailedOrRefusedStepNamingItsReason(t *testing.T) { // 1607/AC5 for _, state := range []string{delivery.StepFailed, delivery.StepRefused} { caller := &fakeCaller{answer: `{"step":"abc1","key":"ace.abc1","state":"running"}`} steps := &fakeSteps{steps: map[string]delivery.Step{"mesh-build-go/ace.abc1": stepAt("ace", "abc1", state, "go is go1.26.0, the recipe builds with go1.27.1", "")}} _, err := Runner{Caller: caller, Steps: steps, Bound: time.Second}.Run(context.Background(), "mesh-build-go", "ace", "build_go_build", nil) if err == nil || !strings.Contains(err.Error(), "go1.27.1") || !strings.Contains(err.Error(), state) || !strings.Contains(err.Error(), "ace") { t.Errorf("%s: error = %v, want the node, the state and the reason", state, err) } } } func TestRunStopsWaitingAtTheBound(t *testing.T) { // 1607/AC5 caller := &fakeCaller{answer: `{"step":"abc1","key":"ace.abc1","state":"running"}`} _, err := Runner{Caller: caller, Steps: &fakeSteps{}, Bound: 20 * time.Millisecond}.Run(context.Background(), "mesh-build-go", "ace", "build_go_build", nil) if err == nil || !strings.Contains(err.Error(), "ace.abc1") || !strings.Contains(err.Error(), "did not end") { t.Errorf("error = %v, want the step key and that it did not end within its bound", err) } } func TestRunRefusesAnAnswerWithNoKeyAndAKeyOfAnotherNode(t *testing.T) { // 1607/AC5 for _, answer := range []string{`{"state":"running"}`, `{"step":"abc1","key":"novox.abc1","state":"running"}`, `not json`} { _, err := Runner{Caller: &fakeCaller{answer: answer}, Steps: &fakeSteps{}, Bound: time.Second}.Run(context.Background(), "mesh-build-go", "ace", "build_go_fetch", nil) if err == nil { t.Errorf("answer %s: no error", answer) } } } func TestRunReturnsTheCallsOwnError(t *testing.T) { // 1607/AC5 boom := errors.New("no responders") _, err := Runner{Caller: &fakeCaller{err: boom}, Steps: &fakeSteps{}, Bound: time.Second}.Run(context.Background(), "mesh-build-go", "ace", "build_go_fetch", nil) if !errors.Is(err, boom) || !strings.Contains(err.Error(), "ace") { t.Errorf("error = %v, want the call's error naming the node", err) } }