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 }