package gen import ( "bytes" "io" "path/filepath" "strings" "testing" "github.com/hashicorp/hcl/v2" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" "github.com/stretchr/testify/assert" ) type exprTestCase struct { hcl2Expr string goCode string } type environment map[string]interface{} func (e environment) scope() *model.Scope { s := model.NewRootScope(syntax.None) for name, typeOrFunction := range e { switch typeOrFunction := typeOrFunction.(type) { case *model.Function: s.DefineFunction(name, typeOrFunction) case model.Type: s.Define(name, &model.Variable{Name: name, VariableType: typeOrFunction}) } } return s } func TestLiteralExpression(t *testing.T) { t.Parallel() cases := []exprTestCase{ {hcl2Expr: "false", goCode: "false"}, {hcl2Expr: "true", goCode: "true"}, {hcl2Expr: "0", goCode: "0"}, {hcl2Expr: "3.14", goCode: "3.14"}, {hcl2Expr: "\"foo\"", goCode: "\"foo\""}, {hcl2Expr: `"foo: ${bar}"`, goCode: `fmt.Sprintf("foo: %v", bar)`}, {hcl2Expr: `"fizz${bar}buzz"`, goCode: `fmt.Sprintf("fizz%vbuzz", bar)`}, {hcl2Expr: `"foo ${bar} %baz"`, goCode: `fmt.Sprintf("foo %v%v", bar, " %baz")`}, {hcl2Expr: strings.ReplaceAll(`"{ \"Version\": \"2008-10-17\", \"Statement\": [ { ${Sid}: ${newpolicy}, ${Effect}: ${Allow}, \"Principal\": \"*\", } ] }"`, "\n", "\\n"), goCode: "fmt.Sprintf(`" + `{ "Version": "2008-10-17", "Statement": [ { %v: %v, %v: %v, "Principal": "*", } ] }` + "`, Sid, newpolicy, Effect, Allow)"}, } for _, c := range cases { c := c testGenerateExpression(t, c.hcl2Expr, c.goCode, nil, nil) } } func TestBinaryOpExpression(t *testing.T) { t.Parallel() env := environment(map[string]interface{}{ "a": model.BoolType, "b": model.BoolType, "c": model.NumberType, "d": model.NumberType, }) scope := env.scope() cases := []exprTestCase{ {hcl2Expr: "0 == 0", goCode: "0 == 0"}, {hcl2Expr: "0 != 0", goCode: "0 != 0"}, {hcl2Expr: "0 < 0", goCode: "0 < 0"}, {hcl2Expr: "0 > 0", goCode: "0 > 0"}, {hcl2Expr: "0 <= 0", goCode: "0 <= 0"}, {hcl2Expr: "0 >= 0", goCode: "0 >= 0"}, {hcl2Expr: "0 + 0", goCode: "0 + 0"}, {hcl2Expr: "0 - 0", goCode: "0 - 0"}, {hcl2Expr: "0 * 0", goCode: "0 * 0"}, {hcl2Expr: "0 / 0", goCode: "0 / 0"}, {hcl2Expr: "0 % 0", goCode: "0 % 0"}, {hcl2Expr: "false && false", goCode: "false && false"}, {hcl2Expr: "false || false", goCode: "false || false"}, {hcl2Expr: "a == true", goCode: "a == true"}, {hcl2Expr: "b == true", goCode: "b == true"}, {hcl2Expr: "c + 0", goCode: "c + 0"}, {hcl2Expr: "d + 0", goCode: "d + 0"}, {hcl2Expr: "a && true", goCode: "a && true"}, {hcl2Expr: "b && true", goCode: "b && true"}, } for _, c := range cases { testGenerateExpression(t, c.hcl2Expr, c.goCode, scope, nil) } } func TestUnaryOpExrepssion(t *testing.T) { t.Parallel() env := environment(map[string]interface{}{ "a": model.NumberType, "b": model.BoolType, }) scope := env.scope() cases := []exprTestCase{ {hcl2Expr: "-1", goCode: "-1"}, {hcl2Expr: "!true", goCode: "!true"}, {hcl2Expr: "-a", goCode: "-a"}, {hcl2Expr: "!b", goCode: "!b"}, } for _, c := range cases { testGenerateExpression(t, c.hcl2Expr, c.goCode, scope, nil) } } func TestArgumentTypeName(t *testing.T) { t.Parallel() g := newTestGenerator(t, filepath.Join("aws-s3-logging-pp", "aws-s3-logging.pp")) noneTypeName := g.argumentTypeName(nil, model.NoneType, false /*isInput*/) assert.Equal(t, "", noneTypeName) plainIntType := g.argumentTypeName(nil, model.IntType, false /*isInput*/) assert.Equal(t, "int", plainIntType) inputIntType := g.argumentTypeName(nil, model.IntType, true /*isInput*/) assert.Equal(t, "pulumi.Int", inputIntType) plainStringType := g.argumentTypeName(nil, model.StringType, false /*isInput*/) assert.Equal(t, "string", plainStringType) inputStringType := g.argumentTypeName(nil, model.StringType, true /*isInput*/) assert.Equal(t, "pulumi.String", inputStringType) plainBoolType := g.argumentTypeName(nil, model.BoolType, false /*isInput*/) assert.Equal(t, "bool", plainBoolType) inputBoolType := g.argumentTypeName(nil, model.BoolType, true /*isInput*/) assert.Equal(t, "pulumi.Bool", inputBoolType) plainNumberType := g.argumentTypeName(nil, model.NumberType, false /*isInput*/) assert.Equal(t, "float64", plainNumberType) inputNumberType := g.argumentTypeName(nil, model.NumberType, true /*isInput*/) assert.Equal(t, "pulumi.Float64", inputNumberType) plainDynamicType := g.argumentTypeName(nil, model.DynamicType, false /*isInput*/) assert.Equal(t, "interface{}", plainDynamicType) inputDynamicType := g.argumentTypeName(nil, model.DynamicType, true /*isInput*/) assert.Equal(t, "pulumi.Any", inputDynamicType) objectType := model.NewObjectType(map[string]model.Type{ "foo": model.StringType, "bar": model.IntType, }) plainObjectType := g.argumentTypeName(nil, objectType, false /*isInput*/) assert.Equal(t, "map[string]interface{}", plainObjectType) inputObjectType := g.argumentTypeName(nil, objectType, true /*isInput*/) assert.Equal(t, "pulumi.Map", inputObjectType) uniformObjectType := model.NewObjectType(map[string]model.Type{ "x": model.IntType, "y": model.IntType, }) plainUniformObjectType := g.argumentTypeName(nil, uniformObjectType, false /*isInput*/) assert.Equal(t, "map[string]interface{}", plainUniformObjectType) inputUniformObjectType := g.argumentTypeName(nil, uniformObjectType, true /*isInput*/) assert.Equal(t, "pulumi.IntMap", inputUniformObjectType) plainMapType := g.argumentTypeName(nil, model.NewMapType(model.StringType), false /*isInput*/) assert.Equal(t, "map[string]string", plainMapType) inputMapType := g.argumentTypeName(nil, model.NewMapType(model.StringType), true /*isInput*/) assert.Equal(t, "pulumi.StringMap", inputMapType) plainIntListType := g.argumentTypeName(nil, model.NewListType(model.IntType), false /*isInput*/) assert.Equal(t, "[]int", plainIntListType) inputIntListType := g.argumentTypeName(nil, model.NewListType(model.IntType), true /*isInput*/) assert.Equal(t, "pulumi.IntArray", inputIntListType) plainDynamicListType := g.argumentTypeName(nil, model.NewListType(model.DynamicType), false /*isInput*/) assert.Equal(t, "[]interface{}", plainDynamicListType) inputDynamicListType := g.argumentTypeName(nil, model.NewListType(model.DynamicType), true /*isInput*/) assert.Equal(t, "pulumi.Array", inputDynamicListType) // assert that the Output[T] + input=false is the same as T + input=true // in this case where T = string assert.Equal(t, g.argumentTypeName(nil, model.NewOutputType(model.StringType), false /*isInput*/), g.argumentTypeName(nil, model.StringType, true /*isInput*/)) } func TestNotYetImplementedEmittedWhenGeneratingFunctions(t *testing.T) { t.Parallel() g := newTestGenerator(t, filepath.Join("aws-s3-logging-pp", "aws-s3-logging.pp")) notYetImplementedFunctions := []string{ "split", "element", "entries", "lookup", "range", } for _, fn := range notYetImplementedFunctions { var content bytes.Buffer g.GenFunctionCallExpression(&content, &model.FunctionCallExpression{ Name: fn, }) assert.Contains(t, content.String(), "call "+fn) } } func TestGeneratingGoOptionalFunctions(t *testing.T) { t.Parallel() g := newTestGenerator(t, filepath.Join("aws-s3-logging-pp", "aws-s3-logging.pp")) testCases := []struct { expr *model.FunctionCallExpression generated string }{ { expr: &model.FunctionCallExpression{ Name: "goOptionalString", Args: []model.Expression{ model.VariableReference(&model.Variable{Name: "foo"}), }, }, generated: "pulumi.StringRef(foo)", }, { expr: &model.FunctionCallExpression{ Name: "goOptionalInt", Args: []model.Expression{ model.VariableReference(&model.Variable{Name: "foo"}), }, }, generated: "pulumi.IntRef(foo)", }, { expr: &model.FunctionCallExpression{ Name: "goOptionalBool", Args: []model.Expression{ model.VariableReference(&model.Variable{Name: "foo"}), }, }, generated: "pulumi.BoolRef(foo)", }, { expr: &model.FunctionCallExpression{ Name: "goOptionalFloat64", Args: []model.Expression{ model.VariableReference(&model.Variable{Name: "foo"}), }, }, generated: "pulumi.Float64Ref(foo)", }, } for _, test := range testCases { var content bytes.Buffer g.GenFunctionCallExpression(&content, test.expr) assert.Contains(t, content.String(), test.generated) } } //nolint:lll func TestConditionalExpression(t *testing.T) { t.Parallel() cases := []exprTestCase{ { hcl2Expr: "true ? 1 : 0", goCode: "var tmp0 float64\nif true {\ntmp0 = 1\n} else {\ntmp0 = 0\n}\ntmp0", }, { hcl2Expr: "true ? 1 : true ? 0 : -1", goCode: "var tmp0 float64\nif true {\ntmp0 = 0\n} else {\ntmp0 = -1\n}\nvar tmp1 float64\nif true {\ntmp1 = 1\n} else {\ntmp1 = tmp0\n}\ntmp1", }, { hcl2Expr: "true ? true ? 0 : -1 : 0", goCode: "var tmp0 float64\nif true {\ntmp0 = 0\n} else {\ntmp0 = -1\n}\nvar tmp1 float64\nif true {\ntmp1 = tmp0\n} else {\ntmp1 = 0\n}\ntmp1", }, { hcl2Expr: "{foo = true ? 2 : 0}", goCode: "var tmp0 float64\nif true {\ntmp0 = 2\n} else {\ntmp0 = 0\n}\nmap[string]interface{}{\n\"foo\": tmp0,\n}", }, } genFunc := func(w io.Writer, g *generator, e model.Expression) { e, temps := g.lowerExpression(e, e.Type()) g.genTemps(w, temps) g.Fgenf(w, "%v", e) } for _, c := range cases { testGenerateExpression(t, c.hcl2Expr, c.goCode, nil, genFunc) } } func TestObjectConsExpression(t *testing.T) { t.Parallel() env := environment(map[string]interface{}{ "a": model.StringType, }) scope := env.scope() cases := []exprTestCase{ { // TODO probably a bug in the binder. Single value objects should just be maps hcl2Expr: "{foo = 1}", goCode: "map[string]interface{}{\n\"foo\": 1,\n}", }, { hcl2Expr: "{\"foo\" = 1}", goCode: "map[string]interface{}{\n\"foo\": 1,\n}", }, { hcl2Expr: "{1 = 1}", goCode: "map[string]interface{}{\n\"1\": 1,\n}", }, { hcl2Expr: "{(a) = 1}", goCode: "map[string]float64{\na: 1,\n}", }, { hcl2Expr: "{(a+a) = 1}", goCode: "map[string]float64{\na + a: 1,\n}", }, } for _, c := range cases { testGenerateExpression(t, c.hcl2Expr, c.goCode, scope, nil) } } func TestTupleConsExpression(t *testing.T) { t.Parallel() env := environment(map[string]interface{}{ "a": model.StringType, }) scope := env.scope() cases := []exprTestCase{ { hcl2Expr: "[\"foo\"]", goCode: "[]string{\n\"foo\",\n}", }, { hcl2Expr: "[\"foo\", \"bar\", \"baz\"]", goCode: "[]string{\n\"foo\",\n\"bar\",\n\"baz\",\n}", }, { hcl2Expr: "[1]", goCode: "[]float64{\n1,\n}", }, { hcl2Expr: "[1,2,3]", goCode: "[]float64{\n1,\n2,\n3,\n}", }, { hcl2Expr: "[1,\"foo\"]", goCode: "[]interface{}{\n1,\n\"foo\",\n}", }, } for _, c := range cases { c := c testGenerateExpression(t, c.hcl2Expr, c.goCode, scope, nil) } } func testGenerateExpression( t *testing.T, hcl2Expr, goCode string, scope *model.Scope, gen func(w io.Writer, g *generator, e model.Expression), ) { t.Run(hcl2Expr, func(t *testing.T) { t.Parallel() // test program is only for schema info g := newTestGenerator(t, filepath.Join("aws-s3-logging-pp", "aws-s3-logging.pp")) var index bytes.Buffer expr, _ := model.BindExpressionText(hcl2Expr, scope, hcl.Pos{}) if gen != nil { gen(&index, g, expr) } else { g.Fgenf(&index, "%v", expr) } assert.Equal(t, goCode, index.String()) }) }