github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/cgo/cgo_test.go (about) 1 package cgo 2 3 import ( 4 "bytes" 5 "flag" 6 "fmt" 7 "go/ast" 8 "go/format" 9 "go/parser" 10 "go/token" 11 "go/types" 12 "os" 13 "path/filepath" 14 "regexp" 15 "runtime" 16 "strings" 17 "testing" 18 ) 19 20 // Pass -update to go test to update the output of the test files. 21 var flagUpdate = flag.Bool("update", false, "Update images based on test output.") 22 23 // normalizeResult normalizes Go source code that comes out of tests across 24 // platforms and Go versions. 25 func normalizeResult(t *testing.T, result string) string { 26 result = strings.ReplaceAll(result, "\r\n", "\n") 27 28 // This changed to 'undefined:', in Go 1.20. 29 result = strings.ReplaceAll(result, ": undeclared name:", ": undefined:") 30 // Go 1.20 added a bit more detail 31 result = regexp.MustCompile(`(unknown field z in struct literal).*`).ReplaceAllString(result, "$1") 32 33 return result 34 } 35 36 func TestCGo(t *testing.T) { 37 var cflags = []string{"--target=armv6m-unknown-unknown-eabi"} 38 39 for _, name := range []string{ 40 "basic", 41 "errors", 42 "types", 43 "symbols", 44 "flags", 45 "const", 46 } { 47 name := name // avoid a race condition 48 t.Run(name, func(t *testing.T) { 49 // Read the AST in memory. 50 path := filepath.Join("testdata", name+".go") 51 fset := token.NewFileSet() 52 f, err := parser.ParseFile(fset, path, nil, parser.ParseComments) 53 if err != nil { 54 t.Fatal("could not parse Go source file:", err) 55 } 56 57 // Process the AST with CGo. 58 cgoFiles, _, _, _, _, cgoErrors := Process([]*ast.File{f}, "testdata", "main", fset, cflags) 59 60 // Check the AST for type errors. 61 var typecheckErrors []error 62 config := types.Config{ 63 Error: func(err error) { 64 typecheckErrors = append(typecheckErrors, err) 65 }, 66 Importer: simpleImporter{}, 67 Sizes: types.SizesFor("gccgo", "arm"), 68 } 69 _, err = config.Check("", fset, append([]*ast.File{f}, cgoFiles...), nil) 70 if err != nil && len(typecheckErrors) == 0 { 71 // Only report errors when no type errors are found (an 72 // unexpected condition). 73 t.Error(err) 74 } 75 76 // Store the (formatted) output in a buffer. Format it, so it 77 // becomes easier to read (and will hopefully change less with CGo 78 // changes). 79 buf := &bytes.Buffer{} 80 if len(cgoErrors) != 0 { 81 buf.WriteString("// CGo errors:\n") 82 for _, err := range cgoErrors { 83 buf.WriteString(formatDiagnostic(err)) 84 } 85 buf.WriteString("\n") 86 } 87 if len(typecheckErrors) != 0 { 88 buf.WriteString("// Type checking errors after CGo processing:\n") 89 for _, err := range typecheckErrors { 90 buf.WriteString(formatDiagnostic(err)) 91 } 92 buf.WriteString("\n") 93 } 94 err = format.Node(buf, fset, cgoFiles[0]) 95 if err != nil { 96 t.Errorf("could not write out CGo AST: %v", err) 97 } 98 actual := normalizeResult(t, buf.String()) 99 100 // Read the file with the expected output, to compare against. 101 outfile := filepath.Join("testdata", name+".out.go") 102 expectedBytes, err := os.ReadFile(outfile) 103 if err != nil { 104 t.Fatalf("could not read expected output: %v", err) 105 } 106 expected := strings.ReplaceAll(string(expectedBytes), "\r\n", "\n") 107 108 // Check whether the output is as expected. 109 if expected != actual { 110 // It is not. Test failed. 111 if *flagUpdate { 112 // Update the file with the expected data. 113 err := os.WriteFile(outfile, []byte(actual), 0666) 114 if err != nil { 115 t.Error("could not write updated output file:", err) 116 } 117 return 118 } 119 t.Errorf("output did not match:\n%s", string(actual)) 120 } 121 }) 122 } 123 } 124 125 func Test_cgoPackage_isEquivalentAST(t *testing.T) { 126 fieldA := &ast.Field{Type: &ast.BasicLit{Kind: token.STRING, Value: "a"}} 127 fieldB := &ast.Field{Type: &ast.BasicLit{Kind: token.STRING, Value: "b"}} 128 listOfFieldA := &ast.FieldList{List: []*ast.Field{fieldA}} 129 listOfFieldB := &ast.FieldList{List: []*ast.Field{fieldB}} 130 funcDeclA := &ast.FuncDecl{Name: &ast.Ident{Name: "a"}, Type: &ast.FuncType{Params: &ast.FieldList{}, Results: listOfFieldA}} 131 funcDeclB := &ast.FuncDecl{Name: &ast.Ident{Name: "b"}, Type: &ast.FuncType{Params: &ast.FieldList{}, Results: listOfFieldB}} 132 funcDeclNoResults := &ast.FuncDecl{Name: &ast.Ident{Name: "C"}, Type: &ast.FuncType{Params: &ast.FieldList{}}} 133 134 testCases := []struct { 135 name string 136 a, b ast.Node 137 expected bool 138 }{ 139 { 140 name: "both nil", 141 expected: true, 142 }, 143 { 144 name: "not same type", 145 a: fieldA, 146 b: &ast.FuncDecl{}, 147 expected: false, 148 }, 149 { 150 name: "Field same", 151 a: fieldA, 152 b: fieldA, 153 expected: true, 154 }, 155 { 156 name: "Field different", 157 a: fieldA, 158 b: fieldB, 159 expected: false, 160 }, 161 { 162 name: "FuncDecl Type Results nil", 163 a: funcDeclNoResults, 164 b: funcDeclNoResults, 165 expected: true, 166 }, 167 { 168 name: "FuncDecl Type Results same", 169 a: funcDeclA, 170 b: funcDeclA, 171 expected: true, 172 }, 173 { 174 name: "FuncDecl Type Results different", 175 a: funcDeclA, 176 b: funcDeclB, 177 expected: false, 178 }, 179 { 180 name: "FuncDecl Type Results a nil", 181 a: funcDeclNoResults, 182 b: funcDeclB, 183 expected: false, 184 }, 185 { 186 name: "FuncDecl Type Results b nil", 187 a: funcDeclA, 188 b: funcDeclNoResults, 189 expected: false, 190 }, 191 } 192 193 for _, tc := range testCases { 194 t.Run(tc.name, func(t *testing.T) { 195 p := &cgoPackage{} 196 if got := p.isEquivalentAST(tc.a, tc.b); tc.expected != got { 197 t.Errorf("expected %v, got %v", tc.expected, got) 198 } 199 }) 200 } 201 } 202 203 // simpleImporter implements the types.Importer interface, but only allows 204 // importing the unsafe package. 205 type simpleImporter struct { 206 } 207 208 // Import implements the Importer interface. For testing usage only: it only 209 // supports importing the unsafe package. 210 func (i simpleImporter) Import(path string) (*types.Package, error) { 211 switch path { 212 case "unsafe": 213 return types.Unsafe, nil 214 default: 215 return nil, fmt.Errorf("importer not implemented for package %s", path) 216 } 217 } 218 219 // formatDiagnostics formats the error message to be an indented comment. It 220 // also fixes Windows path name issues (backward slashes). 221 func formatDiagnostic(err error) string { 222 msg := err.Error() 223 if runtime.GOOS == "windows" { 224 // Fix Windows path slashes. 225 msg = strings.ReplaceAll(msg, "testdata\\", "testdata/") 226 } 227 return "// " + msg + "\n" 228 }