package cgo import ( "bytes" "flag" "fmt" "go/ast" "go/format" "go/parser" "go/scanner" "go/token" "go/types" "os" "path/filepath" "regexp" "runtime" "strings" "testing" ) // Pass -update to go test to update the output of the test files. var flagUpdate = flag.Bool("update", false, "Update images based on test output.") // normalizeResult normalizes Go source code that comes out of tests across // platforms and Go versions. func normalizeResult(t *testing.T, result string) string { result = strings.ReplaceAll(result, "\r\n", "\n") // This changed to 'undefined:', in Go 1.20. result = strings.ReplaceAll(result, ": undeclared name:", ": undefined:") // Go 1.20 added a bit more detail result = regexp.MustCompile(`(unknown field z in struct literal).*`).ReplaceAllString(result, "$1") return result } func TestCGo(t *testing.T) { var cflags = []string{"--target=armv6m-unknown-unknown-eabi"} for _, name := range []string{ "basic", "errors", "types", "symbols", "flags", "const", } { name := name // avoid a race condition t.Run(name, func(t *testing.T) { // Read the AST in memory. path := filepath.Join("testdata", name+".go") fset := token.NewFileSet() f, err := parser.ParseFile(fset, path, nil, parser.ParseComments) if err != nil { t.Fatal("could not parse Go source file:", err) } // Process the AST with CGo. cgoFiles, _, _, _, _, cgoErrors := Process([]*ast.File{f}, "testdata", "main", fset, cflags) // Check the AST for type errors. var typecheckErrors []error config := types.Config{ Error: func(err error) { typecheckErrors = append(typecheckErrors, err) }, Importer: simpleImporter{}, Sizes: types.SizesFor("gccgo", "arm"), } _, err = config.Check("", fset, append([]*ast.File{f}, cgoFiles...), nil) if err != nil && len(typecheckErrors) == 0 { // Only report errors when no type errors are found (an // unexpected condition). t.Error(err) } // Store the (formatted) output in a buffer. Format it, so it // becomes easier to read (and will hopefully change less with CGo // changes). buf := &bytes.Buffer{} if len(cgoErrors) != 0 { buf.WriteString("// CGo errors:\n") for _, err := range cgoErrors { buf.WriteString(formatDiagnostic(err)) } buf.WriteString("\n") } if len(typecheckErrors) != 0 { buf.WriteString("// Type checking errors after CGo processing:\n") for _, err := range typecheckErrors { buf.WriteString(formatDiagnostic(err)) } buf.WriteString("\n") } err = format.Node(buf, fset, cgoFiles[0]) if err != nil { t.Errorf("could not write out CGo AST: %v", err) } actual := normalizeResult(t, buf.String()) // Read the file with the expected output, to compare against. outfile := filepath.Join("testdata", name+".out.go") expectedBytes, err := os.ReadFile(outfile) if err != nil { t.Fatalf("could not read expected output: %v", err) } expected := strings.ReplaceAll(string(expectedBytes), "\r\n", "\n") // Check whether the output is as expected. if expected != actual { // It is not. Test failed. if *flagUpdate { // Update the file with the expected data. err := os.WriteFile(outfile, []byte(actual), 0666) if err != nil { t.Error("could not write updated output file:", err) } return } t.Errorf("output did not match:\n%s", string(actual)) } }) } } func Test_cgoPackage_isEquivalentAST(t *testing.T) { fieldA := &ast.Field{Type: &ast.BasicLit{Kind: token.STRING, Value: "a"}} fieldB := &ast.Field{Type: &ast.BasicLit{Kind: token.STRING, Value: "b"}} listOfFieldA := &ast.FieldList{List: []*ast.Field{fieldA}} listOfFieldB := &ast.FieldList{List: []*ast.Field{fieldB}} funcDeclA := &ast.FuncDecl{Name: &ast.Ident{Name: "a"}, Type: &ast.FuncType{Params: &ast.FieldList{}, Results: listOfFieldA}} funcDeclB := &ast.FuncDecl{Name: &ast.Ident{Name: "b"}, Type: &ast.FuncType{Params: &ast.FieldList{}, Results: listOfFieldB}} funcDeclNoResults := &ast.FuncDecl{Name: &ast.Ident{Name: "C"}, Type: &ast.FuncType{Params: &ast.FieldList{}}} testCases := []struct { name string a, b ast.Node expected bool }{ { name: "both nil", expected: true, }, { name: "not same type", a: fieldA, b: &ast.FuncDecl{}, expected: false, }, { name: "Field same", a: fieldA, b: fieldA, expected: true, }, { name: "Field different", a: fieldA, b: fieldB, expected: false, }, { name: "FuncDecl Type Results nil", a: funcDeclNoResults, b: funcDeclNoResults, expected: true, }, { name: "FuncDecl Type Results same", a: funcDeclA, b: funcDeclA, expected: true, }, { name: "FuncDecl Type Results different", a: funcDeclA, b: funcDeclB, expected: false, }, { name: "FuncDecl Type Results a nil", a: funcDeclNoResults, b: funcDeclB, expected: false, }, { name: "FuncDecl Type Results b nil", a: funcDeclA, b: funcDeclNoResults, expected: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { p := &cgoPackage{} if got := p.isEquivalentAST(tc.a, tc.b); tc.expected != got { t.Errorf("expected %v, got %v", tc.expected, got) } }) } } // simpleImporter implements the types.Importer interface, but only allows // importing the unsafe package. type simpleImporter struct { } // Import implements the Importer interface. For testing usage only: it only // supports importing the unsafe package. func (i simpleImporter) Import(path string) (*types.Package, error) { switch path { case "unsafe": return types.Unsafe, nil default: return nil, fmt.Errorf("importer not implemented for package %s", path) } } // formatDiagnostic formats the error message to be an indented comment. It // also fixes Windows path name issues (backward slashes). func formatDiagnostic(err error) string { var msg string switch err := err.(type) { case scanner.Error: msg = err.Pos.String() + ": " + err.Msg default: msg = err.Error() } if runtime.GOOS == "windows" { // Fix Windows path slashes. msg = strings.ReplaceAll(msg, "testdata\\", "testdata/") } return "// " + msg + "\n" }