| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package fxicfg |
| |
| import ( |
| "fmt" |
| "reflect" |
| "strings" |
| "testing" |
| |
| "fuchsia.googlesource.com/infra/infra/fxicfg/errs" |
| protos "fuchsia.googlesource.com/infra/infra/fxicfg/starlark/protos/recipes" |
| "github.com/golang/protobuf/proto" |
| luci "go.chromium.org/luci/starlark/interpreter" |
| "go.chromium.org/luci/starlark/starlarkproto" |
| ) |
| |
| // Default name of the starlark file to execute first. |
| const entrypoint = "main.star" |
| |
| // This is a minimal test to verify that the Generator keeps working. |
| func TestSmokeTestGenerate(t *testing.T) { |
| srcs := make(map[string]string) |
| srcs[entrypoint] = ` |
| load("//configs/example.star", example="example") |
| output = example` |
| |
| srcs["configs/example.star"] = ` |
| load("@proto//recipes/example.proto", example_pb="recipes.example") |
| |
| # See fxicfg/starlark/protos/recipes/example.proto |
| example = example_pb.Example( |
| field_a = example_pb.Example.FieldA( |
| value = 3, |
| ))` |
| |
| _, globals, err := Generate(luci.MemoryLoader(srcs), entrypoint) |
| if err != nil { |
| t.Fatal(err) |
| } |
| message := globals["output"].(*starlarkproto.Message) |
| pb, err := message.ToProto() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| actual := pb.(*protos.Example) |
| expected := &protos.Example{ |
| FieldA: &protos.Example_FieldA{ |
| Value: 3, |
| }, |
| } |
| |
| if !proto.Equal(actual, expected) { |
| t.Errorf("wanted:\n%v\ngot:\n%v", expected, actual) |
| } |
| } |
| |
| // TODO(kjharland): Refactor so that this test's dependency on Generate() is acyclic and |
| // this test case can live in the builtins/ package. |
| func TestBuiltinSetOutputDir(t *testing.T) { |
| tests := []struct { |
| name string |
| sources map[string]string |
| expectErr bool |
| expectedOutputDir string |
| }{ |
| { |
| name: "should err if set_output_dir is called twice", |
| sources: map[string]string{ |
| entrypoint: ` |
| fxicfg.set_output_dir("output_a") |
| fxicfg.set_output_dir("output_b") |
| `, |
| }, |
| expectErr: true, |
| }, |
| { |
| name: "should set the output directory", |
| sources: map[string]string{entrypoint: ` |
| fxicfg.set_output_dir("output_a") |
| `, |
| }, |
| expectedOutputDir: "output_a", |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| state, _, err := Generate(luci.MemoryLoader(tt.sources), entrypoint) |
| switch { |
| case err != nil && !tt.expectErr: |
| t.Errorf("unexpected error: %v", err) |
| return |
| case err == nil && tt.expectErr: |
| t.Error("wanted an error but got nil") |
| return |
| case err != nil && tt.expectErr: |
| return |
| } |
| |
| expected := tt.expectedOutputDir |
| actual := state.OutputDir |
| if expected != actual { |
| t.Errorf("wanted output dir %q but got %q", expected, actual) |
| } |
| }) |
| } |
| } |
| |
| // TODO(kjharland): Refactor so that this test's dependency on Generate() is acyclic and |
| // this test case can live in the builtins/ package. |
| func TestBuiltinAddOutputFile(t *testing.T) { |
| |
| // A dummy starlark module that constructs an Example proto message for testing. |
| textprotoMod := ` |
| load("@proto//recipes/example.proto", example_pb="recipes.example") |
| |
| # See fxicfg/starlark/protos/recipes/example.proto |
| |
| _one = example_pb.Example( |
| field_a = example_pb.Example.FieldA( |
| value = 1, |
| ), |
| ) |
| |
| _two = example_pb.Example( |
| field_a = example_pb.Example.FieldA( |
| value = 2, |
| ), |
| ) |
| |
| textprotos = struct( |
| one = proto.to_textpb(_one), |
| two = proto.to_textpb(_two), |
| ) |
| ` |
| |
| tests := []struct { |
| name string |
| sources map[string]string |
| // Expect a specific error reason because a boolean is not enough; Starlark |
| // interpretation may fail for various reasons, and (true|false) tests are unreliable. |
| expectedErrReason errs.Reason |
| expectedOutputs map[string]proto.Message |
| }{ |
| { |
| name: "should err if duplicate paths are added", |
| sources: map[string]string{ |
| "textprotos.star": textprotoMod, |
| entrypoint: ` |
| load("//textprotos.star", textprotos="textprotos") |
| |
| fxicfg.add_output_file(path="example/either.txt", data=textprotos.one) |
| fxicfg.add_output_file(path="example/either.txt", data=textprotos.two) |
| `, |
| }, |
| expectedErrReason: errs.ErrInvalidArg, |
| }, |
| { |
| name: "should err if an absolute path is given", |
| sources: map[string]string{ |
| "textprotos.star": textprotoMod, |
| entrypoint: ` |
| load("//textprotos.star", textprotos="textprotos") |
| |
| fxicfg.add_output_file(path="/one.txt", data=textprotos.one) |
| `, |
| }, |
| expectedErrReason: errs.ErrInvalidArg, |
| }, |
| { |
| name: "should add a file", |
| sources: map[string]string{ |
| "textprotos.star": textprotoMod, |
| entrypoint: ` |
| load("//textprotos.star", textprotos="textprotos") |
| |
| fxicfg.add_output_file(path="example/two.txt", data=textprotos.two) |
| `, |
| }, |
| expectedOutputs: map[string]proto.Message{ |
| "example/two.txt": &protos.Example{ |
| FieldA: &protos.Example_FieldA{ |
| Value: 2, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "should add multiple files", |
| sources: map[string]string{ |
| "textprotos.star": textprotoMod, |
| entrypoint: ` |
| load("//textprotos.star", textprotos="textprotos") |
| |
| fxicfg.add_output_file(path="example/one.txt", data=textprotos.one) |
| fxicfg.add_output_file(path="example/two.txt", data=textprotos.two) |
| `, |
| }, |
| expectedOutputs: map[string]proto.Message{ |
| "example/one.txt": &protos.Example{ |
| FieldA: &protos.Example_FieldA{ |
| Value: 1, |
| }, |
| }, |
| |
| "example/two.txt": &protos.Example{ |
| FieldA: &protos.Example_FieldA{ |
| Value: 2, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| state, _, err := Generate(luci.MemoryLoader(tt.sources), entrypoint) |
| if err := checkErrorReason(tt.expectedErrReason, err); err != nil { |
| t.Error(err) |
| return |
| } |
| if tt.expectedErrReason != "" { |
| return // Already checked the error. |
| } |
| |
| expected := make(map[string][]byte) |
| for k, v := range tt.expectedOutputs { |
| textproto := proto.MarshalTextString(v) |
| expected[k] = []byte(textproto) |
| } |
| |
| actual := state.OutputFiles |
| if !reflect.DeepEqual(expected, actual) { |
| t.Errorf("wanted\n%v\nbut got\n%v", expected, actual) |
| } |
| }) |
| } |
| } |
| |
| // Helper function to ensure an error emitted by the interpreter is correct. Returns an |
| // error if `actual` is a starlark syntax or semantic error. Returns nil If `actual` |
| // originates from within this interpreter and its message matches `reason`. |
| func checkErrorReason(reason errs.Reason, actual error) error { |
| switch { |
| case reason == "" && actual != nil: |
| return fmt.Errorf("unexpected error: %q", actual.Error()) |
| case actual == nil && reason != "": |
| return fmt.Errorf("wanted error with reason %q but got nil", reason) |
| case actual == nil && reason == "": |
| return nil // all ok |
| } |
| if strings.HasPrefix(actual.Error(), string(reason)) { |
| return nil |
| } |
| return fmt.Errorf("wanted error reason %q but got %q", reason, actual.Error()) |
| } |