go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/recipe_wrapper/recipes/recipes_test.go (about)

     1  // Copyright 2023 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package recipes
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"os"
    11  	"path/filepath"
    12  	"testing"
    13  
    14  	"github.com/google/go-cmp/cmp"
    15  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    16  	"google.golang.org/protobuf/types/known/structpb"
    17  )
    18  
    19  func TestResolveBuildProperties(t *testing.T) {
    20  	t.Parallel()
    21  
    22  	tests := []struct {
    23  		name                string
    24  		inputProperties     map[string]any
    25  		versionedProperties map[string]any
    26  		expectProperties    map[string]any
    27  		expectErr           bool
    28  	}{
    29  		{
    30  			name:            "passes through versioned properties",
    31  			inputProperties: map[string]any{},
    32  			versionedProperties: map[string]any{
    33  				"foo": "bar",
    34  			},
    35  			expectProperties: map[string]any{
    36  				"foo": "bar",
    37  			},
    38  		},
    39  		{
    40  			name: "request properties override versioned properties",
    41  			inputProperties: map[string]any{
    42  				"foo": "request_value",
    43  			},
    44  			versionedProperties: map[string]any{
    45  				"foo": "versioned_value",
    46  				"bar": "baz",
    47  			},
    48  			expectProperties: map[string]any{
    49  				"foo": "request_value",
    50  				"bar": "baz",
    51  			},
    52  		},
    53  		{
    54  			// "recipe" is a special case, and the only property for which the
    55  			// versioned value should always override the value in the build
    56  			// input.
    57  			name: "versioned recipe property overrides request property",
    58  			inputProperties: map[string]any{
    59  				"foo":    "request_foo",
    60  				"recipe": "request_recipe",
    61  			},
    62  			versionedProperties: map[string]any{
    63  				"foo":    "versioned_foo",
    64  				"bar":    "versioned_bar",
    65  				"recipe": "versioned_recipe",
    66  			},
    67  			expectProperties: map[string]any{
    68  				"foo":    "request_foo",
    69  				"bar":    "versioned_bar",
    70  				"recipe": "versioned_recipe",
    71  			},
    72  		},
    73  		{
    74  			// On the off chance that the versioned properties don't specify a
    75  			// recipe, we should respect the recipe from the build input.
    76  			name: "request recipe used if no versioned property",
    77  			inputProperties: map[string]any{
    78  				"recipe": "request_recipe",
    79  			},
    80  			versionedProperties: map[string]any{},
    81  			expectProperties: map[string]any{
    82  				"recipe": "request_recipe",
    83  			},
    84  		},
    85  	}
    86  
    87  	for _, test := range tests {
    88  		t.Run(test.name, func(t *testing.T) {
    89  			ctx := context.Background()
    90  			builder := &buildbucketpb.BuilderID{
    91  				Project: "fuchsia", Bucket: "ci", Builder: "bldr",
    92  			}
    93  			build := &buildbucketpb.Build{
    94  				Builder: builder,
    95  				Input:   &buildbucketpb.Build_Input{},
    96  			}
    97  			var err error
    98  			build.Input.Properties, err = structpb.NewStruct(test.inputProperties)
    99  			if err != nil {
   100  				t.Fatal(err)
   101  			}
   102  			integrationDir := t.TempDir()
   103  			propertiesPath := filepath.Join(
   104  				integrationDir,
   105  				propertiesFileForBuilder(builder))
   106  			jsonProps, err := json.Marshal(test.versionedProperties)
   107  			if err != nil {
   108  				t.Fatal(err)
   109  			}
   110  			writeFile(t, propertiesPath, []byte(jsonProps))
   111  
   112  			if err := resolveBuildProperties(ctx, integrationDir, build); err != nil {
   113  				if !test.expectErr {
   114  					t.Fatalf("got unexpected error: %s", err)
   115  				}
   116  				return
   117  			} else if test.expectErr {
   118  				t.Fatalf("expected error, got nil")
   119  			}
   120  			if diff := cmp.Diff(test.expectProperties, build.Input.Properties.AsMap()); diff != "" {
   121  				t.Errorf("resolveBuildProperties(): output differs (-want, +got):\n%s", diff)
   122  			}
   123  		})
   124  	}
   125  }
   126  
   127  func TestReadVersionedProperties(t *testing.T) {
   128  	builder := &buildbucketpb.BuilderID{
   129  		Project: "fuchsia", Bucket: "ci", Builder: "bldr",
   130  	}
   131  
   132  	props := map[string]any{
   133  		"nested_map": map[string]any{
   134  			"server": "url",
   135  		},
   136  		"number": float64(1234),
   137  		"bool":   true,
   138  	}
   139  
   140  	integrationDir := t.TempDir()
   141  
   142  	writeFile(
   143  		t,
   144  		filepath.Join(integrationDir, propertiesFileForBuilder(builder)),
   145  		mustMarshalJSON(t, props))
   146  
   147  	got, err := versionedProperties(context.Background(), integrationDir, builder)
   148  	if err != nil {
   149  		t.Fatal(err)
   150  	}
   151  
   152  	if diff := cmp.Diff(props, got.AsMap()); diff != "" {
   153  		t.Errorf("readVersionedProperties() output differs (-want, +got):\n%s", diff)
   154  	}
   155  
   156  	// Shadow builders should use same properties file as non-shadow counterparts.
   157  	shadowBuilder := &buildbucketpb.BuilderID{
   158  		Project: "fuchsia", Bucket: "ci.shadow", Builder: "bldr",
   159  	}
   160  	got, err = versionedProperties(context.Background(), integrationDir, shadowBuilder)
   161  	if err != nil {
   162  		t.Fatal(err)
   163  	}
   164  
   165  	if diff := cmp.Diff(props, got.AsMap()); diff != "" {
   166  		t.Errorf("readVersionedProperties() output differs (-want, +got):\n%s", diff)
   167  	}
   168  }
   169  
   170  func writeFile(t *testing.T, path string, contents []byte) {
   171  	t.Helper()
   172  	if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
   173  		t.Fatal(err)
   174  	}
   175  	if err := os.WriteFile(path, contents, 0o600); err != nil {
   176  		t.Fatal(err)
   177  	}
   178  }
   179  
   180  func mustMarshalJSON(t *testing.T, obj any) []byte {
   181  	res, err := json.Marshal(obj)
   182  	if err != nil {
   183  		t.Fatal(err)
   184  	}
   185  	return res
   186  }