| // Copyright 2026 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| package main | |
| import ( | |
| "path/filepath" | |
| "strings" | |
| "testing" | |
| "text/template" | |
| "dawn.googlesource.com/dawn/tools/src/oswrapper" | |
| "github.com/ben-clayton/webidlparser/ast" | |
| "github.com/ben-clayton/webidlparser/parser" | |
| "github.com/stretchr/testify/require" | |
| ) | |
| func TestRun_MissingArgs(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| args []string | |
| }{ | |
| { | |
| name: "No args", | |
| args: []string{}, | |
| }, | |
| { | |
| name: "Missing template", | |
| args: []string{"--output=out.go", "input.idl"}, | |
| }, | |
| { | |
| name: "Missing input file", | |
| args: []string{"--template=tmpl.go", "--output=out.go"}, | |
| }, | |
| } | |
| for _, tt := range tests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| err := run(tt.args, wrapper) | |
| require.ErrorContains(t, err, "idlgen is a tool used to generate code") | |
| }) | |
| } | |
| } | |
| func TestRun_OutputToStdout(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| require.NoError(t, wrapper.WriteFile("template.tmpl", []byte("{{.}}"), 0644)) | |
| require.NoError(t, wrapper.WriteFile("test.idl", []byte("interface Test {}; interface GPUDevice {};"), 0644)) | |
| args := []string{"--template=template.tmpl", "test.idl"} | |
| err := run(args, wrapper) | |
| require.NoError(t, err) | |
| } | |
| func TestRun_OutputToFile(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| require.NoError(t, wrapper.WriteFile("template.tmpl", []byte("{{range .Declarations}}{{.Name}} {{end}}"), 0644)) | |
| require.NoError(t, wrapper.WriteFile("test.idl", []byte("interface Test {}; interface GPUDevice {};"), 0644)) | |
| outputPath := "subdir/out.txt" | |
| args := []string{"--template=template.tmpl", "--output=" + outputPath, "test.idl"} | |
| err := run(args, wrapper) | |
| require.NoError(t, err) | |
| // Verify file exists and has content | |
| content, err := wrapper.ReadFile(outputPath) | |
| require.NoError(t, err) | |
| require.Contains(t, string(content), "Test GPUDevice") | |
| } | |
| func TestRun_OutputDirectoryCreationFailure(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| require.NoError(t, wrapper.WriteFile("template.tmpl", []byte("{{.}}"), 0644)) | |
| require.NoError(t, wrapper.WriteFile("test.idl", []byte("interface Test {};"), 0644)) | |
| // Create a file named "subdir" to block directory creation | |
| require.NoError(t, wrapper.WriteFile("subdir", []byte("i am a file"), 0644)) | |
| outputPath := "subdir/out.txt" | |
| args := []string{"--template=template.tmpl", "--output=" + outputPath, "test.idl"} | |
| err := run(args, wrapper) | |
| require.Error(t, err) | |
| require.ErrorContains(t, err, "failed to create output directory") | |
| } | |
| func TestRun_OutputFileCreationFailure(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| require.NoError(t, wrapper.WriteFile("template.tmpl", []byte("{{.}}"), 0644)) | |
| require.NoError(t, wrapper.WriteFile("test.idl", []byte("interface Test {};"), 0644)) | |
| outputPath := "out.txt" | |
| // Create a directory at the output path to cause Create to fail | |
| require.NoError(t, wrapper.Mkdir(outputPath, 0755)) | |
| args := []string{"--template=template.tmpl", "--output=" + outputPath, "test.idl"} | |
| err := run(args, wrapper) | |
| require.Error(t, err) | |
| require.ErrorContains(t, err, "failed to open output file") | |
| } | |
| func TestRun_TemplateReadFailure(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| // Do not create the template file | |
| require.NoError(t, wrapper.WriteFile("test.idl", []byte("interface Test {};"), 0644)) | |
| args := []string{"--template=template.tmpl", "test.idl"} | |
| err := run(args, wrapper) | |
| require.ErrorContains(t, err, "failed to open template file") | |
| } | |
| func TestRun_IDLReadFailure(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| require.NoError(t, wrapper.WriteFile("template.tmpl", []byte("{{.}}"), 0644)) | |
| // Provide a non-existent IDL file | |
| args := []string{"--template=template.tmpl", "missing.idl"} | |
| err := run(args, wrapper) | |
| require.ErrorContains(t, err, "failed to open file 'missing.idl'") | |
| } | |
| func TestRun_IDLParseFailure(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| require.NoError(t, wrapper.WriteFile("template.tmpl", []byte("{{.}}"), 0644)) | |
| // Provide invalid IDL content | |
| require.NoError(t, wrapper.WriteFile("invalid.idl", []byte("this is definitely not valid idl"), 0644)) | |
| args := []string{"--template=template.tmpl", "invalid.idl"} | |
| err := run(args, wrapper) | |
| require.ErrorContains(t, err, "errors found while parsing invalid.idl") | |
| } | |
| func TestRun_TemplateParseFailure(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| // Provide invalid template content (unclosed action) | |
| require.NoError(t, wrapper.WriteFile("invalid.tmpl", []byte("{{.Name"), 0644)) | |
| require.NoError(t, wrapper.WriteFile("test.idl", []byte("interface Test {}; interface GPUDevice {};"), 0644)) | |
| args := []string{"--template=invalid.tmpl", "test.idl"} | |
| err := run(args, wrapper) | |
| require.ErrorContains(t, err, "failed to parse template file") | |
| } | |
| func TestRun_SuccessfulExecution(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| // A simple template that iterates over declarations and prints their names | |
| templateContent := `{{range .Declarations}}{{.Name}};{{end}}` | |
| require.NoError(t, wrapper.WriteFile("template.tmpl", []byte(templateContent), 0644)) | |
| // A simple IDL file. GPUDevice is required to avoid a panic in the patch function. | |
| idlContent := ` | |
| interface MyInterface { | |
| void myMethod(); | |
| }; | |
| interface GPUDevice {}; | |
| ` | |
| require.NoError(t, wrapper.WriteFile("test.idl", []byte(idlContent), 0644)) | |
| outputPath := "output.txt" | |
| args := []string{"--template=template.tmpl", "--output=" + outputPath, "test.idl"} | |
| err := run(args, wrapper) | |
| require.NoError(t, err) | |
| // Verify the output content | |
| content, err := wrapper.ReadFile(outputPath) | |
| require.NoError(t, err) | |
| outputStr := string(content) | |
| require.Contains(t, outputStr, "File generated by tools/cmd/idlgen.go") | |
| require.Contains(t, outputStr, "\n\nMyInterface;GPUDevice;") | |
| } | |
| func TestSimplify(t *testing.T) { | |
| idl := ` | |
| interface Dependent : Base { | |
| void method(); | |
| }; | |
| interface Base {}; | |
| partial interface Partial { | |
| void method2(); | |
| }; | |
| interface Partial { | |
| void method1(); | |
| }; | |
| interface mixin MyMixin { | |
| const long mixinConst = 1; | |
| }; | |
| interface WithMixin { | |
| void originalMethod(); | |
| }; | |
| WithMixin includes MyMixin; | |
| enum MyEnum { | |
| "value1" | |
| }; | |
| enum MyEnum { | |
| "value2" | |
| }; | |
| dictionary MyDict { | |
| long member1; | |
| }; | |
| partial dictionary MyDict { | |
| long member2; | |
| }; | |
| typedef long MyTypedef; | |
| dictionary BaseDict { | |
| MyTypedef val; | |
| }; | |
| dictionary DerivedDict : BaseDict { | |
| long otherVal; | |
| }; | |
| interface InterfaceUsingDict { | |
| void method(DerivedDict d); | |
| }; | |
| ` | |
| parsed := parser.Parse(idl) | |
| require.Empty(t, parsed.Errors) | |
| simplified, decls := simplify(parsed) | |
| // Check Partial Interface Merging | |
| partialDecl := decls["Partial"] | |
| require.NotNil(t, partialDecl) | |
| partialIface, ok := partialDecl.(*ast.Interface) | |
| require.True(t, ok) | |
| require.Equal(t, 2, len(partialIface.Members)) | |
| memberNames := make(map[string]bool) | |
| for _, m := range partialIface.Members { | |
| if member, ok := m.(*ast.Member); ok { | |
| memberNames[member.Name] = true | |
| } | |
| } | |
| require.True(t, memberNames["method1"]) | |
| require.True(t, memberNames["method2"]) | |
| // Check Mixin Embedding | |
| withMixinDecl := decls["WithMixin"] | |
| require.NotNil(t, withMixinDecl) | |
| withMixinIface, ok := withMixinDecl.(*ast.Interface) | |
| require.True(t, ok) | |
| require.Equal(t, 2, len(withMixinIface.Members)) | |
| memberNames = make(map[string]bool) | |
| for _, m := range withMixinIface.Members { | |
| if member, ok := m.(*ast.Member); ok { | |
| memberNames[member.Name] = true | |
| } | |
| } | |
| require.True(t, memberNames["originalMethod"]) | |
| require.True(t, memberNames["mixinConst"]) | |
| // Check Partial Enum Merging | |
| enumDecl := decls["MyEnum"] | |
| require.NotNil(t, enumDecl) | |
| enum, ok := enumDecl.(*ast.Enum) | |
| require.True(t, ok) | |
| require.Equal(t, 2, len(enum.Values)) | |
| enumValues := make(map[string]bool) | |
| for _, v := range enum.Values { | |
| if val, ok := v.(*ast.BasicLiteral); ok { | |
| enumValues[val.Value] = true | |
| } | |
| } | |
| require.True(t, enumValues["\"value1\""]) | |
| require.True(t, enumValues["\"value2\""]) | |
| // Check Partial Dictionary Merging | |
| dictDecl := decls["MyDict"] | |
| require.NotNil(t, dictDecl) | |
| dict, ok := dictDecl.(*ast.Dictionary) | |
| require.True(t, ok) | |
| require.Equal(t, 2, len(dict.Members)) | |
| dictMembers := make(map[string]bool) | |
| for _, m := range dict.Members { | |
| dictMembers[m.Name] = true | |
| } | |
| require.True(t, dictMembers["member1"]) | |
| require.True(t, dictMembers["member2"]) | |
| // Check Dependency Ordering | |
| // Base must precede Dependent | |
| var names []string | |
| for _, d := range simplified.Declarations { | |
| names = append(names, nameOf(d)) | |
| } | |
| checkOrder := func(first, second string) { | |
| firstIdx := -1 | |
| secondIdx := -1 | |
| for i, name := range names { | |
| if name == first { | |
| firstIdx = i | |
| } | |
| if name == second { | |
| secondIdx = i | |
| } | |
| } | |
| require.NotEqual(t, -1, firstIdx, "%s not found in simplified declarations", first) | |
| require.NotEqual(t, -1, secondIdx, "%s not found in simplified declarations", second) | |
| require.True(t, firstIdx < secondIdx, "%s should precede %s, but found indices %d and %d", first, second, firstIdx, secondIdx) | |
| } | |
| checkOrder("Base", "Dependent") | |
| checkOrder("MyTypedef", "BaseDict") | |
| checkOrder("BaseDict", "DerivedDict") | |
| checkOrder("DerivedDict", "InterfaceUsingDict") | |
| } | |
| func TestEnumEntryName(t *testing.T) { | |
| tests := []struct { | |
| input string | |
| expected string | |
| }{ | |
| {"rgba8unorm", "kRgba8Unorm"}, | |
| {"\"bgra8unorm\"", "kBgra8Unorm"}, | |
| {"depth-stencil", "kDepthStencil"}, | |
| {"triangle-list", "kTriangleList"}, | |
| {"line-strip", "kLineStrip"}, | |
| } | |
| for _, tt := range tests { | |
| t.Run(tt.input, func(t *testing.T) { | |
| result := enumEntryName(tt.input) | |
| require.Equal(t, result, tt.expected) | |
| }) | |
| } | |
| } | |
| func TestMemberExtraction(t *testing.T) { | |
| idl := ` | |
| interface Base { | |
| const long BaseConst = 1; | |
| attribute long BaseAttr; | |
| void baseMethod(); | |
| }; | |
| interface Derived : Base { | |
| const long DerivedConst = 2; | |
| attribute long DerivedAttr; | |
| void derivedMethod(); | |
| }; | |
| namespace MyNamespace { | |
| const long NamespaceConst = 3; | |
| attribute long NamespaceAttr; | |
| void namespaceMethod(); | |
| }; | |
| ` | |
| parsed := parser.Parse(idl) | |
| require.Empty(t, parsed.Errors) | |
| _, decls := simplify(parsed) | |
| g := generator{ | |
| declarations: decls, | |
| } | |
| base := decls["Base"] | |
| derived := decls["Derived"] | |
| namespace := decls["MyNamespace"] | |
| t.Run("constantsOf", func(t *testing.T) { | |
| check := func(obj ast.Decl, expected []string) { | |
| consts := constantsOf(obj) | |
| require.Len(t, consts, len(expected)) | |
| for i, name := range expected { | |
| require.Equal(t, name, consts[i].Name) | |
| } | |
| } | |
| check(base, []string{"BaseConst"}) | |
| check(derived, []string{"DerivedConst"}) | |
| check(namespace, []string{"NamespaceConst"}) | |
| }) | |
| t.Run("flattenedConstantsOf", func(t *testing.T) { | |
| consts := g.flattenedConstantsOf(derived) | |
| require.Len(t, consts, 2) | |
| require.Equal(t, "DerivedConst", consts[0].Name) | |
| require.Equal(t, "BaseConst", consts[1].Name) | |
| }) | |
| t.Run("attributesOf", func(t *testing.T) { | |
| check := func(obj ast.Decl, expected []string) { | |
| attrs := attributesOf(obj) | |
| require.Len(t, attrs, len(expected)) | |
| for i, name := range expected { | |
| require.Equal(t, name, attrs[i].Name) | |
| } | |
| } | |
| check(base, []string{"BaseAttr"}) | |
| check(derived, []string{"DerivedAttr"}) | |
| check(namespace, []string{"NamespaceAttr"}) | |
| }) | |
| t.Run("flattenedAttributesOf", func(t *testing.T) { | |
| attrs := g.flattenedAttributesOf(derived) | |
| require.Len(t, attrs, 2) | |
| require.Equal(t, "DerivedAttr", attrs[0].Name) | |
| require.Equal(t, "BaseAttr", attrs[1].Name) | |
| }) | |
| t.Run("methodsOf", func(t *testing.T) { | |
| check := func(obj ast.Decl, expected []string) { | |
| methods := methodsOf(obj) | |
| require.Len(t, methods, len(expected)) | |
| for i, name := range expected { | |
| require.Equal(t, name, methods[i].Name) | |
| } | |
| } | |
| check(base, []string{"baseMethod"}) | |
| check(derived, []string{"derivedMethod"}) | |
| // methodsOf currently returns nil for namespaces | |
| check(namespace, []string{}) | |
| }) | |
| t.Run("flattenedMethodsOf", func(t *testing.T) { | |
| methods := g.flattenedMethodsOf(derived) | |
| require.Len(t, methods, 2) | |
| require.Equal(t, "derivedMethod", methods[0].Name) | |
| require.Equal(t, "baseMethod", methods[1].Name) | |
| }) | |
| } | |
| func TestPatch(t *testing.T) { | |
| lostMember := &ast.Member{ | |
| Name: "lost", | |
| Type: &ast.TypeName{Name: "Promise"}, | |
| Annotations: []*ast.Annotation{}, | |
| } | |
| otherMember := &ast.Member{ | |
| Name: "other", | |
| Type: &ast.TypeName{Name: "void"}, | |
| Annotations: []*ast.Annotation{}, | |
| } | |
| gpuDevice := &ast.Interface{ | |
| Name: "GPUDevice", | |
| Members: []ast.InterfaceMember{lostMember, otherMember}, | |
| } | |
| decls := declarations{ | |
| "GPUDevice": gpuDevice, | |
| } | |
| // Execute patch | |
| patch(nil, decls) | |
| // Verify [SameObject] was added to 'lost' | |
| require.True(t, hasAnnotation(lostMember, "SameObject")) | |
| // Verify 'other' was not touched | |
| require.False(t, hasAnnotation(otherMember, "SameObject")) | |
| } | |
| func TestGeneratorEval(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| templateName string | |
| args []interface{} | |
| expected string | |
| expectError string | |
| setup func(*template.Template) | |
| }{ | |
| { | |
| name: "SingleArgument", | |
| templateName: "Simple", | |
| args: []interface{}{"Hello"}, | |
| expected: "Hello", | |
| }, | |
| { | |
| name: "MapArguments", | |
| templateName: "WithMap", | |
| args: []interface{}{"Key", "foo", "Value", "bar"}, | |
| expected: "foo=bar", | |
| }, | |
| { | |
| name: "TemplateNotFound", | |
| templateName: "Missing", | |
| args: []interface{}{"arg"}, | |
| expectError: "template 'Missing' not found", | |
| }, | |
| { | |
| name: "InvalidArgCount", | |
| templateName: "WithMap", | |
| args: []interface{}{"Key", "foo", "Value"}, | |
| expectError: "Eval expects a single argument or list name-value pairs", | |
| }, | |
| { | |
| name: "InvalidArgType", | |
| templateName: "WithMap", | |
| args: []interface{}{123, "foo", "Value", "bar"}, | |
| expectError: "Eval argument 0 is not a string", | |
| }, | |
| { | |
| name: "ExecutionError", | |
| templateName: "Fail", | |
| args: []interface{}{map[string]string{}}, | |
| expectError: "while evaluating 'Fail'", | |
| setup: func(t *template.Template) { | |
| t.Option("missingkey=error") | |
| }, | |
| }, | |
| } | |
| for _, tt := range tests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| tmpl := template.New("root") | |
| _, err := tmpl.Parse(` | |
| {{define "Simple"}}{{.}}{{end}} | |
| {{define "WithMap"}}{{.Key}}={{.Value}}{{end}} | |
| {{define "Fail"}}{{ .MissingField }}{{end}} | |
| `) | |
| require.NoError(t, err) | |
| if tt.setup != nil { | |
| tt.setup(tmpl) | |
| } | |
| g := generator{ | |
| t: tmpl, | |
| } | |
| out, err := g.eval(tt.templateName, tt.args...) | |
| if tt.expectError != "" { | |
| require.Error(t, err) | |
| require.ErrorContains(t, err, tt.expectError) | |
| } else { | |
| require.NoError(t, err) | |
| require.Equal(t, tt.expected, out) | |
| } | |
| }) | |
| } | |
| } | |
| func TestGeneratorInclude(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| fileName string | |
| fileContent string | |
| includePath string | |
| expectError string | |
| verifyTemplate string // Template to execute to verify the include worked | |
| verifyOutput string // Expected output from verifyTemplate | |
| }{ | |
| { | |
| name: "Success", | |
| fileName: "included.tmpl", | |
| fileContent: `{{define "Included"}}Included Content{{end}}`, | |
| includePath: "included.tmpl", | |
| verifyTemplate: `{{template "Included"}}`, | |
| verifyOutput: "Included Content", | |
| }, | |
| { | |
| name: "MissingFile", | |
| includePath: "missing.tmpl", | |
| expectError: "open templates/missing.tmpl", | |
| }, | |
| { | |
| name: "ParseError", | |
| fileName: "invalid.tmpl", | |
| fileContent: "{{", | |
| includePath: "invalid.tmpl", | |
| expectError: "template: invalid.tmpl:1: unclosed action", | |
| }, | |
| } | |
| for _, tt := range tests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| workingDir := "templates" | |
| require.NoError(t, wrapper.Mkdir(workingDir, 0755)) | |
| if tt.fileName != "" { | |
| err := wrapper.WriteFile(filepath.Join(workingDir, tt.fileName), []byte(tt.fileContent), 0644) | |
| require.NoError(t, err) | |
| } | |
| tmpl := template.New("root") | |
| g := generator{ | |
| t: tmpl, | |
| workingDir: workingDir, | |
| osWrapper: wrapper, | |
| funcs: map[string]interface{}{}, | |
| } | |
| out, err := g.include(tt.includePath) | |
| if tt.expectError != "" { | |
| require.Error(t, err) | |
| require.ErrorContains(t, err, tt.expectError) | |
| } else { | |
| require.NoError(t, err) | |
| require.Empty(t, out) | |
| if tt.verifyTemplate != "" { | |
| t2, err := tmpl.Parse(tt.verifyTemplate) | |
| require.NoError(t, err) | |
| sb := strings.Builder{} | |
| err = t2.Execute(&sb, nil) | |
| require.NoError(t, err) | |
| require.Equal(t, tt.verifyOutput, sb.String()) | |
| } | |
| } | |
| }) | |
| } | |
| } | |
| func TestMapGet(t *testing.T) { | |
| m := newMap() | |
| m.Put("key1", "value1") | |
| m.Put("key2", 123) | |
| m.Put(456, "value3") | |
| require.Equal(t, "value1", m.Get("key1")) | |
| require.Equal(t, 123, m.Get("key2")) | |
| require.Equal(t, "value3", m.Get(456)) | |
| require.Nil(t, m.Get("missing")) | |
| } | |
| func TestIs(t *testing.T) { | |
| isInterface := is(ast.Interface{}) | |
| isInterfaceOrNamespace := is(ast.Interface{}, ast.Namespace{}) | |
| iface := &ast.Interface{} | |
| namespace := &ast.Namespace{} | |
| dictionary := &ast.Dictionary{} | |
| require.True(t, isInterface(iface)) | |
| require.True(t, isInterface(*iface)) | |
| require.False(t, isInterface(namespace)) | |
| require.False(t, isInterface(dictionary)) | |
| require.True(t, isInterfaceOrNamespace(iface)) | |
| require.True(t, isInterfaceOrNamespace(namespace)) | |
| require.False(t, isInterfaceOrNamespace(dictionary)) | |
| } | |
| func TestIsInitializer(t *testing.T) { | |
| constructor := &ast.Member{ | |
| Type: &ast.TypeName{Name: "constructor"}, | |
| } | |
| regularMember := &ast.Member{ | |
| Type: &ast.TypeName{Name: "void"}, | |
| } | |
| notMember := &ast.Interface{} | |
| require.True(t, isInitializer(constructor)) | |
| require.False(t, isInitializer(regularMember)) | |
| require.False(t, isInitializer(notMember)) | |
| } | |
| func TestIsUndefinedType(t *testing.T) { | |
| undefined := &ast.TypeName{Name: "undefined"} | |
| notUndefined := &ast.TypeName{Name: "void"} | |
| notTypeName := &ast.SequenceType{} | |
| require.True(t, isUndefinedType(undefined)) | |
| require.False(t, isUndefinedType(notUndefined)) | |
| require.False(t, isUndefinedType(notTypeName)) | |
| } | |
| func TestHasAnnotation(t *testing.T) { | |
| annotated := []*ast.Annotation{ | |
| {Name: "Annotated"}, | |
| {Name: "WithValue"}, | |
| } | |
| t.Run("Interface", func(t *testing.T) { | |
| node := &ast.Interface{Annotations: annotated} | |
| require.True(t, hasAnnotation(node, "Annotated")) | |
| require.True(t, hasAnnotation(node, "WithValue")) | |
| require.False(t, hasAnnotation(node, "Missing")) | |
| }) | |
| t.Run("Member", func(t *testing.T) { | |
| node := &ast.Member{Annotations: annotated} | |
| require.True(t, hasAnnotation(node, "Annotated")) | |
| require.True(t, hasAnnotation(node, "WithValue")) | |
| require.False(t, hasAnnotation(node, "Missing")) | |
| }) | |
| t.Run("Namespace", func(t *testing.T) { | |
| node := &ast.Namespace{Annotations: annotated} | |
| require.True(t, hasAnnotation(node, "Annotated")) | |
| require.True(t, hasAnnotation(node, "WithValue")) | |
| require.False(t, hasAnnotation(node, "Missing")) | |
| }) | |
| t.Run("Parameter", func(t *testing.T) { | |
| node := &ast.Parameter{Annotations: annotated} | |
| require.True(t, hasAnnotation(node, "Annotated")) | |
| require.True(t, hasAnnotation(node, "WithValue")) | |
| require.False(t, hasAnnotation(node, "Missing")) | |
| }) | |
| t.Run("Typedef", func(t *testing.T) { | |
| // Test Annotations field | |
| node := &ast.Typedef{Annotations: annotated} | |
| require.True(t, hasAnnotation(node, "Annotated")) | |
| require.True(t, hasAnnotation(node, "WithValue")) | |
| require.False(t, hasAnnotation(node, "Missing")) | |
| // Test TypeAnnotations field | |
| nodeTypeAnnote := &ast.Typedef{TypeAnnotations: annotated} | |
| require.True(t, hasAnnotation(nodeTypeAnnote, "Annotated")) | |
| require.True(t, hasAnnotation(node, "WithValue")) | |
| require.False(t, hasAnnotation(nodeTypeAnnote, "Missing")) | |
| // Test both | |
| nodeBoth := &ast.Typedef{ | |
| Annotations: []*ast.Annotation{{Name: "A1"}}, | |
| TypeAnnotations: []*ast.Annotation{{Name: "A2"}}, | |
| } | |
| require.True(t, hasAnnotation(nodeBoth, "A1")) | |
| require.True(t, hasAnnotation(nodeBoth, "A2")) | |
| require.False(t, hasAnnotation(nodeBoth, "Missing")) | |
| }) | |
| } | |
| func TestHasConstructor(t *testing.T) { | |
| constructorMember := &ast.Member{ | |
| Type: &ast.TypeName{Name: "constructor"}, | |
| } | |
| regularMember := &ast.Member{ | |
| Type: &ast.TypeName{Name: "void"}, | |
| } | |
| withConstructor := &ast.Interface{ | |
| Members: []ast.InterfaceMember{regularMember, constructorMember}, | |
| } | |
| withoutConstructor := &ast.Interface{ | |
| Members: []ast.InterfaceMember{regularMember}, | |
| } | |
| require.True(t, hasConstructor(withConstructor)) | |
| require.False(t, hasConstructor(withoutConstructor)) | |
| } | |
| func TestPromiseDetection_HappyPath(t *testing.T) { | |
| tests := []struct { | |
| method string | |
| isPromise bool | |
| }{ | |
| {"asyncMethod", true}, | |
| {"syncMethod", false}, | |
| {"asyncValue", true}, | |
| {"syncValue", false}, | |
| } | |
| idl := ` | |
| interface Test { | |
| Promise<void> asyncMethod(); | |
| void syncMethod(); | |
| Promise<long> asyncValue(); | |
| long syncValue(); | |
| }; | |
| ` | |
| parsed := parser.Parse(idl) | |
| require.Empty(t, parsed.Errors) | |
| _, decls := simplify(parsed) | |
| testIface := decls["Test"].(*ast.Interface) | |
| methods := methodsOf(testIface) | |
| methodMap := make(map[string]*Method) | |
| for _, m := range methods { | |
| methodMap[m.Name] = m | |
| } | |
| for _, tt := range tests { | |
| t.Run(tt.method, func(t *testing.T) { | |
| method := methodMap[tt.method] | |
| require.NotNil(t, method) | |
| retType := method.Overloads[0].Type | |
| require.Equal(t, tt.isPromise, isPromiseType(retType), "isPromiseType failed for %s", tt.method) | |
| require.Equal(t, tt.isPromise, returnsPromise(method), "returnsPromise failed for %s", tt.method) | |
| }) | |
| } | |
| } | |
| func TestPromiseDetection_InconsistentOverloads(t *testing.T) { | |
| // Manually construct a method with inconsistent overloads | |
| method := &Method{ | |
| Name: "mixedMethod", | |
| Overloads: []*ast.Member{ | |
| {Type: &ast.ParametrizedType{Name: "Promise", Elems: []ast.Type{&ast.TypeName{Name: "void"}}}}, | |
| {Type: &ast.TypeName{Name: "void"}}, | |
| }, | |
| } | |
| require.Panics(t, func() { | |
| returnsPromise(method) | |
| }) | |
| } | |
| func TestSetlikeOf(t *testing.T) { | |
| setlikePattern := &ast.Pattern{Type: ast.Setlike} | |
| otherPattern := &ast.Pattern{Type: ast.Iterable} | |
| withSetlike := &ast.Interface{ | |
| Patterns: []*ast.Pattern{otherPattern, setlikePattern}, | |
| } | |
| withoutSetlike := &ast.Interface{ | |
| Patterns: []*ast.Pattern{otherPattern}, | |
| } | |
| notInterface := &ast.Dictionary{} | |
| require.Equal(t, setlikePattern, setlikeOf(withSetlike)) | |
| require.Nil(t, setlikeOf(withoutSetlike)) | |
| require.Nil(t, setlikeOf(notInterface)) | |
| } | |
| func TestPascalCase(t *testing.T) { | |
| tests := []struct { | |
| input string | |
| expected string | |
| }{ | |
| {"foo_bar", "FooBar"}, | |
| {"gl-Position", "GlPosition"}, | |
| {"texture_2d", "Texture2D"}, | |
| {"simple", "Simple"}, | |
| {"camelCase", "CamelCase"}, | |
| {"_leading_underscore", "LeadingUnderscore"}, | |
| {"trailing_underscore_", "TrailingUnderscore"}, | |
| {"multiple___underscores", "MultipleUnderscores"}, | |
| {"123_numbers", "123Numbers"}, | |
| {"mixed-separators_test", "MixedSeparatorsTest"}, | |
| } | |
| for _, tt := range tests { | |
| t.Run(tt.input, func(t *testing.T) { | |
| result := pascalCase(tt.input) | |
| require.Equal(t, result, tt.expected) | |
| }) | |
| } | |
| } |