Skip to content

Commit f970086

Browse files
jcogilvieclaude
andcommitted
chore(render): align engine tests with package conventions per coderabbit
Both engine_docker_test.go and engine_local_test.go now follow the existing convention used by runtime_docker_test.go and the rest of cmd/crossplane/render: PascalCase test names without underscore (TestDockerRenderEngineRender, TestLocalRenderEngineRender), an args/want/reason struct shape, and cmp.Diff for response comparisons (via protocmp.Transform). While restructuring, also tighten the success / partial-output cases to compare the actual recovered *RenderResponse against the canned input rather than just asserting non-nil — a regression that returned an empty response would have slipped past the previous assertion. The canned response now carries a distinguishing CompositeResource struct so the round-trip exercises the unmarshal path. For symmetry, propagate wantSingleOccurrence to the local engine test. The local engine can't produce double-stderr today (its *exec.ExitError doesn't embed stderr the way *ContainerExitError does), but adding the assertion keeps the two test files structurally identical and guards against future drift. No production code changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
1 parent 734c36b commit f970086

2 files changed

Lines changed: 199 additions & 114 deletions

File tree

cmd/crossplane/render/engine_docker_test.go

Lines changed: 111 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import (
2121
"strings"
2222
"testing"
2323

24+
"github.com/google/go-cmp/cmp"
2425
"google.golang.org/protobuf/proto"
26+
"google.golang.org/protobuf/testing/protocmp"
27+
"google.golang.org/protobuf/types/known/structpb"
2528

2629
"github.com/crossplane/crossplane-runtime/v2/pkg/logging"
2730

@@ -39,85 +42,126 @@ func (m *mockContainerRunner) Run(ctx context.Context, img string, opts ...docke
3942

4043
var _ containerRunner = &mockContainerRunner{}
4144

42-
func TestDockerRenderEngine_Render(t *testing.T) {
43-
rsp := &renderv1alpha1.RenderResponse{
45+
func TestDockerRenderEngineRender(t *testing.T) {
46+
// A canned response with a distinguishing CompositeResource so a successful
47+
// (or partial) round-trip through Render asserts the unmarshal path, not
48+
// just that we got something non-nil back.
49+
xrStruct, err := structpb.NewStruct(map[string]any{
50+
"apiVersion": "example.org/v1",
51+
"kind": "XR",
52+
"metadata": map[string]any{"name": "test-xr"},
53+
})
54+
if err != nil {
55+
t.Fatalf("cannot construct canned XR struct: %v", err)
56+
}
57+
cannedRsp := &renderv1alpha1.RenderResponse{
4458
Output: &renderv1alpha1.RenderResponse_Composite{
45-
Composite: &renderv1alpha1.CompositeOutput{},
59+
Composite: &renderv1alpha1.CompositeOutput{
60+
CompositeResource: xrStruct,
61+
},
4662
},
4763
}
48-
rspBytes, err := proto.Marshal(rsp)
64+
cannedRspBytes, err := proto.Marshal(cannedRsp)
4965
if err != nil {
5066
t.Fatalf("cannot marshal canned response: %v", err)
5167
}
5268

53-
cases := map[string]struct {
54-
runFn func(ctx context.Context, img string, opts ...docker.RunContainerOption) ([]byte, []byte, error)
55-
wantRsp bool
69+
type args struct {
70+
runFn func(ctx context.Context, img string, opts ...docker.RunContainerOption) ([]byte, []byte, error)
71+
}
72+
73+
type want struct {
74+
rsp *renderv1alpha1.RenderResponse
5675
wantErr bool
5776
wantInErr []string
58-
wantSingleOccurrence []string // strings that must appear exactly once (catches double-stderr bugs)
77+
wantSingleOccurrence []string
78+
}
79+
80+
cases := map[string]struct {
81+
reason string
82+
args args
83+
want want
5984
}{
6085
"Success": {
61-
runFn: func(_ context.Context, _ string, _ ...docker.RunContainerOption) ([]byte, []byte, error) {
62-
return rspBytes, nil, nil
86+
reason: "Render returns the unmarshaled response and no error on a clean exit.",
87+
args: args{
88+
runFn: func(_ context.Context, _ string, _ ...docker.RunContainerOption) ([]byte, []byte, error) {
89+
return cannedRspBytes, nil, nil
90+
},
6391
},
64-
wantRsp: true,
92+
want: want{rsp: cannedRsp},
6593
},
6694
"FatalWithPartialOutput": {
67-
runFn: func(_ context.Context, _ string, _ ...docker.RunContainerOption) ([]byte, []byte, error) {
68-
return rspBytes, []byte("boom: pipeline step requested fatal"), &docker.ContainerExitError{
69-
ExitCode: ExitCodePipelineFatal,
70-
Stderr: []byte("boom: pipeline step requested fatal"),
71-
}
95+
reason: "On exit-3 with non-empty stdout, Render parses the partial response and returns it alongside a stderr-bearing error.",
96+
args: args{
97+
runFn: func(_ context.Context, _ string, _ ...docker.RunContainerOption) ([]byte, []byte, error) {
98+
return cannedRspBytes, []byte("boom: pipeline step requested fatal"), &docker.ContainerExitError{
99+
ExitCode: ExitCodePipelineFatal,
100+
Stderr: []byte("boom: pipeline step requested fatal"),
101+
}
102+
},
72103
},
73-
wantRsp: true,
74-
wantErr: true,
75-
wantInErr: []string{
76-
"pipeline returned fatal",
77-
"boom: pipeline step requested fatal",
104+
want: want{
105+
rsp: cannedRsp,
106+
wantErr: true,
107+
wantInErr: []string{
108+
"pipeline returned fatal",
109+
"boom: pipeline step requested fatal",
110+
},
78111
},
79112
},
80113
"FatalWithNoPartialOutput": {
81-
runFn: func(_ context.Context, _ string, _ ...docker.RunContainerOption) ([]byte, []byte, error) {
82-
return nil, []byte("boom: no partial"), &docker.ContainerExitError{
83-
ExitCode: ExitCodePipelineFatal,
84-
Stderr: []byte("boom: no partial"),
85-
}
114+
reason: "On exit-3 with empty stdout, Render falls back to the hard-fail path and surfaces stderr exactly once.",
115+
args: args{
116+
runFn: func(_ context.Context, _ string, _ ...docker.RunContainerOption) ([]byte, []byte, error) {
117+
return nil, []byte("boom: no partial"), &docker.ContainerExitError{
118+
ExitCode: ExitCodePipelineFatal,
119+
Stderr: []byte("boom: no partial"),
120+
}
121+
},
86122
},
87-
wantRsp: false,
88-
wantErr: true,
89-
wantInErr: []string{
90-
"cannot run crossplane internal render in Docker",
91-
"boom: no partial",
123+
want: want{
124+
wantErr: true,
125+
wantInErr: []string{
126+
"cannot run crossplane internal render in Docker",
127+
"boom: no partial",
128+
},
129+
wantSingleOccurrence: []string{"boom: no partial"},
92130
},
93-
wantSingleOccurrence: []string{"boom: no partial"},
94131
},
95132
"HardFailWithExitError": {
96-
runFn: func(_ context.Context, _ string, _ ...docker.RunContainerOption) ([]byte, []byte, error) {
97-
return nil, []byte("the container is sad"), &docker.ContainerExitError{
98-
ExitCode: 1,
99-
Stderr: []byte("the container is sad"),
100-
}
133+
reason: "Non-fatal exit codes wrap the *ContainerExitError (whose Error already embeds stderr) without doubling stderr.",
134+
args: args{
135+
runFn: func(_ context.Context, _ string, _ ...docker.RunContainerOption) ([]byte, []byte, error) {
136+
return nil, []byte("the container is sad"), &docker.ContainerExitError{
137+
ExitCode: 1,
138+
Stderr: []byte("the container is sad"),
139+
}
140+
},
101141
},
102-
wantRsp: false,
103-
wantErr: true,
104-
wantInErr: []string{
105-
"cannot run crossplane internal render in Docker",
106-
"the container is sad",
142+
want: want{
143+
wantErr: true,
144+
wantInErr: []string{
145+
"cannot run crossplane internal render in Docker",
146+
"the container is sad",
147+
},
148+
wantSingleOccurrence: []string{"the container is sad"},
107149
},
108-
wantSingleOccurrence: []string{"the container is sad"},
109150
},
110151
"HardFailNonExitError": {
111-
runFn: func(_ context.Context, _ string, _ ...docker.RunContainerOption) ([]byte, []byte, error) {
112-
// e.g. image-pull failure: not a *ContainerExitError.
113-
return nil, []byte("non-exit stderr"), &nonExitError{msg: "image pull failed"}
152+
reason: "Non-exit errors (e.g. image-pull failures) get the captured stderr buffer appended so its content isn't lost.",
153+
args: args{
154+
runFn: func(_ context.Context, _ string, _ ...docker.RunContainerOption) ([]byte, []byte, error) {
155+
return nil, []byte("non-exit stderr"), &nonExitError{msg: "image pull failed"}
156+
},
114157
},
115-
wantRsp: false,
116-
wantErr: true,
117-
wantInErr: []string{
118-
"cannot run crossplane internal render in Docker",
119-
"image pull failed",
120-
"non-exit stderr",
158+
want: want{
159+
wantErr: true,
160+
wantInErr: []string{
161+
"cannot run crossplane internal render in Docker",
162+
"image pull failed",
163+
"non-exit stderr",
164+
},
121165
},
122166
},
123167
}
@@ -127,43 +171,40 @@ func TestDockerRenderEngine_Render(t *testing.T) {
127171
e := &dockerRenderEngine{
128172
image: "test-image",
129173
log: logging.NewNopLogger(),
130-
runner: &mockContainerRunner{MockRun: tc.runFn},
174+
runner: &mockContainerRunner{MockRun: tc.args.runFn},
131175
}
132176

133177
rsp, err := e.Render(context.Background(), &renderv1alpha1.RenderRequest{})
134178

135179
switch {
136-
case tc.wantErr && err == nil:
137-
t.Fatalf("Render(): want error, got nil")
138-
case !tc.wantErr && err != nil:
139-
t.Fatalf("Render(): unexpected error: %v", err)
180+
case tc.want.wantErr && err == nil:
181+
t.Fatalf("\n%s\nRender(...): want error, got nil", tc.reason)
182+
case !tc.want.wantErr && err != nil:
183+
t.Fatalf("\n%s\nRender(...): unexpected error: %v", tc.reason, err)
140184
}
141185

142-
for _, want := range tc.wantInErr {
186+
for _, s := range tc.want.wantInErr {
143187
if err == nil {
144-
t.Errorf("Render(): error is nil but expected to contain %q", want)
188+
t.Errorf("\n%s\nRender(...): error is nil but expected to contain %q", tc.reason, s)
145189
continue
146190
}
147-
if !strings.Contains(err.Error(), want) {
148-
t.Errorf("Render(): error %q does not contain %q", err.Error(), want)
191+
if !strings.Contains(err.Error(), s) {
192+
t.Errorf("\n%s\nRender(...): error %q does not contain %q", tc.reason, err.Error(), s)
149193
}
150194
}
151195

152-
for _, want := range tc.wantSingleOccurrence {
196+
for _, s := range tc.want.wantSingleOccurrence {
153197
if err == nil {
154-
t.Errorf("Render(): error is nil but expected exactly one occurrence of %q", want)
198+
t.Errorf("\n%s\nRender(...): error is nil but expected exactly one occurrence of %q", tc.reason, s)
155199
continue
156200
}
157-
if got := strings.Count(err.Error(), want); got != 1 {
158-
t.Errorf("Render(): error %q contains %q %d times, want exactly 1 (double-formatting bug?)", err.Error(), want, got)
201+
if got := strings.Count(err.Error(), s); got != 1 {
202+
t.Errorf("\n%s\nRender(...): error %q contains %q %d times, want exactly 1 (double-formatting bug?)", tc.reason, err.Error(), s, got)
159203
}
160204
}
161205

162-
switch {
163-
case tc.wantRsp && rsp == nil:
164-
t.Errorf("Render(): want non-nil response, got nil")
165-
case !tc.wantRsp && rsp != nil:
166-
t.Errorf("Render(): want nil response, got %+v", rsp)
206+
if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" {
207+
t.Errorf("\n%s\nRender(...): -want, +got:\n%s", tc.reason, diff)
167208
}
168209
})
169210
}

0 commit comments

Comments
 (0)