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  }