@@ -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
4043var _ 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 \n Render(... ): want error, got nil" , tc . reason )
182+ case ! tc .want . wantErr && err != nil :
183+ t .Fatalf ("\n %s \n Render(... ): 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 \n Render(... ): 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 \n Render(... ): 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 \n Render(... ): 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 \n Render(... ): 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\n Render(...): -want, +got:\n %s" , tc .reason , diff )
167208 }
168209 })
169210 }
0 commit comments