// +build !windows package template import ( "bytes" "encoding/json" "path/filepath" "reflect" "strings" "testing" "time" "github.com/google/go-cmp/cmp" ) func boolPointer(tf bool) *bool { return &tf } func TestParse(t *testing.T) { cases := []struct { File string Result *Template Err bool }{ /* * Builders */ { "parse-basic.json", &Template{ Builders: map[string]*Builder{ "something": { Name: "something", Type: "something", }, }, }, false, }, { "parse-basic-config.json", &Template{ Builders: map[string]*Builder{ "something": { Name: "something", Type: "something", Config: map[string]interface{}{ "foo": "bar", }, }, }, }, false, }, { "parse-builder-no-type.json", nil, true, }, { "parse-builder-repeat.json", nil, true, }, /* * Provisioners */ { "parse-provisioner-basic.json", &Template{ Provisioners: []*Provisioner{ { Type: "something", }, }, }, false, }, { "parse-provisioner-config.json", &Template{ Provisioners: []*Provisioner{ { Type: "something", Config: map[string]interface{}{ "inline": "echo 'foo'", }, }, }, }, false, }, { "parse-provisioner-pause-before.json", &Template{ Provisioners: []*Provisioner{ { Type: "something", PauseBefore: 1 * time.Second, }, }, }, false, }, { "parse-provisioner-retry.json", &Template{ Provisioners: []*Provisioner{ { Type: "something", MaxRetries: "5", }, }, }, false, }, { "parse-provisioner-timeout.json", &Template{ Provisioners: []*Provisioner{ { Type: "something", Timeout: 5 * time.Minute, }, }, }, false, }, { "parse-provisioner-only.json", &Template{ Provisioners: []*Provisioner{ { Type: "something", OnlyExcept: OnlyExcept{ Only: []string{"foo"}, }, }, }, }, false, }, { "parse-provisioner-except.json", &Template{ Provisioners: []*Provisioner{ { Type: "something", OnlyExcept: OnlyExcept{ Except: []string{"foo"}, }, }, }, }, false, }, { "parse-provisioner-override.json", &Template{ Provisioners: []*Provisioner{ { Type: "something", Override: map[string]interface{}{ "foo": map[string]interface{}{}, }, }, }, }, false, }, { "parse-provisioner-no-type.json", nil, true, }, { "parse-variable-default.json", &Template{ Variables: map[string]*Variable{ "foo": { Default: "foo", Key: "foo", }, }, }, false, }, { "parse-variable-required.json", &Template{ Variables: map[string]*Variable{ "foo": { Required: true, Key: "foo", }, }, }, false, }, { "parse-pp-basic.json", &Template{ PostProcessors: [][]*PostProcessor{ { { Name: "foo", Type: "foo", Config: map[string]interface{}{ "foo": "bar", }, }, }, }, }, false, }, { "parse-pp-keep.json", &Template{ PostProcessors: [][]*PostProcessor{ { { Name: "foo", Type: "foo", KeepInputArtifact: boolPointer(true), }, }, }, }, false, }, { "parse-pp-only.json", &Template{ PostProcessors: [][]*PostProcessor{ { { Name: "foo", Type: "foo", OnlyExcept: OnlyExcept{ Only: []string{"bar"}, }, }, }, }, }, false, }, { "parse-pp-except.json", &Template{ PostProcessors: [][]*PostProcessor{ { { Name: "foo", Type: "foo", OnlyExcept: OnlyExcept{ Except: []string{"bar"}, }, }, }, }, }, false, }, { "parse-pp-string.json", &Template{ PostProcessors: [][]*PostProcessor{ { { Name: "foo", Type: "foo", }, }, }, }, false, }, { "parse-pp-map.json", &Template{ PostProcessors: [][]*PostProcessor{ { { Name: "foo", Type: "foo", }, }, }, }, false, }, { "parse-pp-slice.json", &Template{ PostProcessors: [][]*PostProcessor{ { { Name: "foo", Type: "foo", }, }, { { Name: "bar", Type: "bar", }, }, }, }, false, }, { "parse-pp-multi.json", &Template{ PostProcessors: [][]*PostProcessor{ { { Name: "foo", Type: "foo", }, { Name: "bar", Type: "bar", }, }, }, }, false, }, { "parse-pp-no-type.json", nil, true, }, { "parse-description.json", &Template{ Description: "foo", }, false, }, { "parse-min-version.json", &Template{ MinVersion: "1.2", }, false, }, { "parse-comment.json", &Template{ Builders: map[string]*Builder{ "something": { Name: "something", Type: "something", }, }, Comments: map[string]string{ "_info": "foo", }, }, false, }, { "parse-monolithic.json", &Template{ Comments: map[string]string{ "_comment": "comment", }, Description: "Description Test", MinVersion: "1.3.0", SensitiveVariables: []*Variable{ { Required: false, Key: "one", Default: "1", }, }, Variables: map[string]*Variable{ "one": { Required: false, Key: "one", Default: "1", }, "two": { Required: false, Key: "two", Default: "2", }, "three": { Required: true, Key: "three", Default: "", }, }, Builders: map[string]*Builder{ "amazon-ebs": { Name: "amazon-ebs", Type: "amazon-ebs", Config: map[string]interface{}{ "ami_name": "AMI Name", "instance_type": "t2.micro", "ssh_username": "ec2-user", "source_ami": "ami-aaaaaaaaaaaaaa", }, }, "docker": { Name: "docker", Type: "docker", Config: map[string]interface{}{ "image": "ubuntu", "export_path": "image.tar", }, }, }, Provisioners: []*Provisioner{ { Type: "shell", Config: map[string]interface{}{ "script": "script.sh", }, }, { Type: "shell", Config: map[string]interface{}{ "script": "script.sh", }, Override: map[string]interface{}{ "docker": map[string]interface{}{ "execute_command": "echo 'override'", }, }, }, }, PostProcessors: [][]*PostProcessor{ { { Name: "compress", Type: "compress", }, { Name: "vagrant", Type: "vagrant", OnlyExcept: OnlyExcept{ Only: []string{"docker"}, }, }, }, { { Name: "shell-local", Type: "shell-local", Config: map[string]interface{}{ "inline": []interface{}{"echo foo"}, }, OnlyExcept: OnlyExcept{ Except: []string{"amazon-ebs"}, }, }, }, }, }, false, }, } for i, tc := range cases { path, _ := filepath.Abs(fixtureDir(tc.File)) tpl, err := ParseFile(fixtureDir(tc.File)) if (err != nil) != tc.Err { t.Fatalf("%s\n\nerr: %s", tc.File, err) } if tc.Result != nil { tc.Result.Path = path } if tpl != nil { tpl.RawContents = nil } if diff := cmp.Diff(tpl, tc.Result); diff != "" { t.Fatalf("[%d]bad: %s\n%v", i, tc.File, diff) } // Only test template writing if the template is valid if tc.Err == false { // Get rawTemplate raw, err := tpl.Raw() if err != nil { t.Fatalf("Failed to convert back to raw template: %s\n\n%v\n\n%s", tc.File, tpl, err) } out, _ := json.MarshalIndent(raw, "", " ") if err != nil { t.Fatalf("Failed to marshal raw template: %s\n\n%v\n\n%s", tc.File, raw, err) } // Write JSON to a buffer (emulates filesystem write without dirtying the workspace) fileBuf := bytes.NewBuffer(out) // Parse the JSON template we wrote to our buffer tplRewritten, err := Parse(fileBuf) if err != nil { t.Fatalf("Failed to re-read marshalled template: %s\n\n%v\n\n%s\n\n%s", tc.File, tpl, out, err) } // Override the metadata we don't care about (file path, raw file contents) tplRewritten.Path = path tplRewritten.RawContents = nil // Test that our output raw template is functionally equal if !reflect.DeepEqual(tpl, tplRewritten) { t.Fatalf("Data lost when writing template to file: %s\n\n%v\n\n%v\n\n%s", tc.File, tpl, tplRewritten, out) } } } } func TestParse_contents(t *testing.T) { tpl, err := ParseFile(fixtureDir("parse-contents.json")) if err != nil { t.Fatalf("err: %s", err) } actual := strings.TrimSpace(string(tpl.RawContents)) expected := `{"builders":[{"type":"test"}]}` if actual != expected { t.Fatalf("bad: %s\n\n%s", actual, expected) } } func TestParse_bad(t *testing.T) { cases := []struct { File string Expected string }{ {"error-beginning.json", "line 1, column 1 (offset 1)"}, {"error-middle.json", "line 5, column 6 (offset 50)"}, {"error-end.json", "line 1, column 30 (offset 30)"}, {"malformed.json", "line 16, column 3 (offset 433)"}, } for _, tc := range cases { _, err := ParseFile(fixtureDir(tc.File)) if err == nil { t.Fatalf("expected error") } if !strings.Contains(err.Error(), tc.Expected) { t.Fatalf("file: %s\nExpected: %s\n%s\n", tc.File, tc.Expected, err.Error()) } } } func TestParse_checkForDuplicateFields(t *testing.T) { cases := []struct { File string Expected string }{ {"error-duplicate-variables.json", "template has duplicate field: variables"}, {"error-duplicate-config.json", "template has duplicate field: foo"}, } for _, tc := range cases { _, err := ParseFile(fixtureDir(tc.File)) if err == nil { t.Fatalf("expected error") } if !strings.Contains(err.Error(), tc.Expected) { t.Fatalf("file: %s\nExpected: %s\n%s\n", tc.File, tc.Expected, err.Error()) } } }