github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/transform/transform_test.go (about)

     1  package transform_test
     2  
     3  // This file defines some helper functions for testing transforms.
     4  
     5  import (
     6  	"flag"
     7  	"go/token"
     8  	"go/types"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/tinygo-org/tinygo/compileopts"
    15  	"github.com/tinygo-org/tinygo/compiler"
    16  	"github.com/tinygo-org/tinygo/loader"
    17  	"tinygo.org/x/go-llvm"
    18  )
    19  
    20  var update = flag.Bool("update", false, "update transform package tests")
    21  
    22  var defaultTestConfig = &compileopts.Config{
    23  	Target:  &compileopts.TargetSpec{},
    24  	Options: &compileopts.Options{Opt: "2"},
    25  }
    26  
    27  // testTransform runs a transformation pass on an input file (pathPrefix+".ll")
    28  // and checks whether it matches the expected output (pathPrefix+".out.ll"). The
    29  // output is compared with a fuzzy match that ignores some irrelevant lines such
    30  // as empty lines.
    31  func testTransform(t *testing.T, pathPrefix string, transform func(mod llvm.Module)) {
    32  	// Read the input IR.
    33  	ctx := llvm.NewContext()
    34  	defer ctx.Dispose()
    35  	buf, err := llvm.NewMemoryBufferFromFile(pathPrefix + ".ll")
    36  	os.Stat(pathPrefix + ".ll") // make sure this file is tracked by `go test` caching
    37  	if err != nil {
    38  		t.Fatalf("could not read file %s: %v", pathPrefix+".ll", err)
    39  	}
    40  	mod, err := ctx.ParseIR(buf)
    41  	if err != nil {
    42  		t.Fatalf("could not load module:\n%v", err)
    43  	}
    44  	defer mod.Dispose()
    45  
    46  	// Perform the transform.
    47  	transform(mod)
    48  
    49  	// Check for any incorrect IR.
    50  	err = llvm.VerifyModule(mod, llvm.PrintMessageAction)
    51  	if err != nil {
    52  		t.Fatal("IR verification failed")
    53  	}
    54  
    55  	// Get the output from the test and filter some irrelevant lines.
    56  	actual := mod.String()
    57  	actual = actual[strings.Index(actual, "\ntarget datalayout = ")+1:]
    58  
    59  	if *update {
    60  		err := os.WriteFile(pathPrefix+".out.ll", []byte(actual), 0666)
    61  		if err != nil {
    62  			t.Error("failed to write out new output:", err)
    63  		}
    64  	} else {
    65  		// Read the expected output IR.
    66  		out, err := os.ReadFile(pathPrefix + ".out.ll")
    67  		if err != nil {
    68  			t.Fatalf("could not read output file %s: %v", pathPrefix+".out.ll", err)
    69  		}
    70  
    71  		// See whether the transform output matches with the expected output IR.
    72  		expected := string(out)
    73  		if !fuzzyEqualIR(expected, actual) {
    74  			t.Logf("output does not match expected output:\n%s", actual)
    75  			t.Fail()
    76  		}
    77  	}
    78  }
    79  
    80  // fuzzyEqualIR returns true if the two LLVM IR strings passed in are roughly
    81  // equal. That means, only relevant lines are compared (excluding comments
    82  // etc.).
    83  func fuzzyEqualIR(s1, s2 string) bool {
    84  	lines1 := filterIrrelevantIRLines(strings.Split(s1, "\n"))
    85  	lines2 := filterIrrelevantIRLines(strings.Split(s2, "\n"))
    86  	if len(lines1) != len(lines2) {
    87  		return false
    88  	}
    89  	for i, line1 := range lines1 {
    90  		line2 := lines2[i]
    91  		if line1 != line2 {
    92  			return false
    93  		}
    94  	}
    95  
    96  	return true
    97  }
    98  
    99  // filterIrrelevantIRLines removes lines from the input slice of strings that
   100  // are not relevant in comparing IR. For example, empty lines and comments are
   101  // stripped out.
   102  func filterIrrelevantIRLines(lines []string) []string {
   103  	var out []string
   104  	for _, line := range lines {
   105  		line = strings.Split(line, ";")[0]    // strip out comments/info
   106  		line = strings.TrimRight(line, "\r ") // drop '\r' on Windows and remove trailing spaces from comments
   107  		if line == "" {
   108  			continue
   109  		}
   110  		if strings.HasPrefix(line, "source_filename = ") {
   111  			continue
   112  		}
   113  		out = append(out, line)
   114  	}
   115  	return out
   116  }
   117  
   118  // compileGoFileForTesting compiles the given Go file to run tests against.
   119  // Only the given Go file is compiled (no dependencies) and no optimizations are
   120  // run.
   121  // If there are any errors, they are reported via the *testing.T instance.
   122  func compileGoFileForTesting(t *testing.T, filename string) llvm.Module {
   123  	target, err := compileopts.LoadTarget(&compileopts.Options{GOOS: "linux", GOARCH: "386"})
   124  	if err != nil {
   125  		t.Fatal("failed to load target:", err)
   126  	}
   127  	config := &compileopts.Config{
   128  		Options: &compileopts.Options{},
   129  		Target:  target,
   130  	}
   131  	compilerConfig := &compiler.Config{
   132  		Triple:             config.Triple(),
   133  		GOOS:               config.GOOS(),
   134  		GOARCH:             config.GOARCH(),
   135  		CodeModel:          config.CodeModel(),
   136  		RelocationModel:    config.RelocationModel(),
   137  		Scheduler:          config.Scheduler(),
   138  		AutomaticStackSize: config.AutomaticStackSize(),
   139  		Debug:              true,
   140  		PanicStrategy:      config.PanicStrategy(),
   141  	}
   142  	machine, err := compiler.NewTargetMachine(compilerConfig)
   143  	if err != nil {
   144  		t.Fatal("failed to create target machine:", err)
   145  	}
   146  	defer machine.Dispose()
   147  
   148  	// Load entire program AST into memory.
   149  	lprogram, err := loader.Load(config, filename, types.Config{
   150  		Sizes: compiler.Sizes(machine),
   151  	})
   152  	if err != nil {
   153  		t.Fatal("failed to create target machine:", err)
   154  	}
   155  	err = lprogram.Parse()
   156  	if err != nil {
   157  		t.Fatal("could not parse", err)
   158  	}
   159  
   160  	// Compile AST to IR.
   161  	program := lprogram.LoadSSA()
   162  	pkg := lprogram.MainPkg()
   163  	mod, errs := compiler.CompilePackage(filename, pkg, program.Package(pkg.Pkg), machine, compilerConfig, false)
   164  	if errs != nil {
   165  		for _, err := range errs {
   166  			t.Error(err)
   167  		}
   168  		t.FailNow()
   169  	}
   170  	return mod
   171  }
   172  
   173  // getPosition returns the position information for the given value, as far as
   174  // it is available.
   175  func getPosition(val llvm.Value) token.Position {
   176  	if !val.IsAInstruction().IsNil() {
   177  		loc := val.InstructionDebugLoc()
   178  		if loc.IsNil() {
   179  			return token.Position{}
   180  		}
   181  		file := loc.LocationScope().ScopeFile()
   182  		return token.Position{
   183  			Filename: filepath.Join(file.FileDirectory(), file.FileFilename()),
   184  			Line:     int(loc.LocationLine()),
   185  			Column:   int(loc.LocationColumn()),
   186  		}
   187  	} else if !val.IsAFunction().IsNil() {
   188  		loc := val.Subprogram()
   189  		if loc.IsNil() {
   190  			return token.Position{}
   191  		}
   192  		file := loc.ScopeFile()
   193  		return token.Position{
   194  			Filename: filepath.Join(file.FileDirectory(), file.FileFilename()),
   195  			Line:     int(loc.SubprogramLine()),
   196  		}
   197  	} else {
   198  		return token.Position{}
   199  	}
   200  }