blob: 1fb20c9f54042aa9c4ecfa5c5297af7d59b4e14b [file] [log] [blame]
// 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())
}