golang.org/x/arch@v0.17.0/riscv64/riscv64asm/ext_test.go (about) 1 // Copyright 2024 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Support for testing against external disassembler program. 6 7 package riscv64asm 8 9 import ( 10 "bufio" 11 "bytes" 12 "encoding/hex" 13 "flag" 14 "fmt" 15 "io" 16 "io/ioutil" 17 "log" 18 "math/rand" 19 "os" 20 "os/exec" 21 "path/filepath" 22 "strings" 23 "testing" 24 "time" 25 ) 26 27 var ( 28 dumpTest = flag.Bool("dump", false, "dump all encodings") 29 mismatch = flag.Bool("mismatch", false, "log allowed mismatches") 30 keep = flag.Bool("keep", false, "keep object files around") 31 debug = false 32 ) 33 34 // An ExtInst represents a single decoded instruction parsed 35 // from an external disassembler's output. 36 type ExtInst struct { 37 addr uint64 38 enc [4]byte 39 nenc int 40 text string 41 } 42 43 func (r ExtInst) String() string { 44 return fmt.Sprintf("%#x: % x: %s", r.addr, r.enc, r.text) 45 } 46 47 // An ExtDis is a connection between an external disassembler and a test. 48 type ExtDis struct { 49 Dec chan ExtInst 50 File *os.File 51 Size int 52 Cmd *exec.Cmd 53 } 54 55 // Run runs the given command - the external disassembler - and returns 56 // a buffered reader of its standard output. 57 func (ext *ExtDis) Run(cmd ...string) (*bufio.Reader, error) { 58 if *keep { 59 log.Printf("%s\n", strings.Join(cmd, " ")) 60 } 61 ext.Cmd = exec.Command(cmd[0], cmd[1:]...) 62 out, err := ext.Cmd.StdoutPipe() 63 if err != nil { 64 return nil, fmt.Errorf("stdoutpipe: %v", err) 65 } 66 if err := ext.Cmd.Start(); err != nil { 67 return nil, fmt.Errorf("exec: %v", err) 68 } 69 70 b := bufio.NewReaderSize(out, 1<<20) 71 return b, nil 72 } 73 74 // Wait waits for the command started with Run to exit. 75 func (ext *ExtDis) Wait() error { 76 return ext.Cmd.Wait() 77 } 78 79 // testExtDis tests a set of byte sequences against an external disassembler. 80 // The disassembler is expected to produce the given syntax and run 81 // in the given architecture mode (16, 32, or 64-bit). 82 // The extdis function must start the external disassembler 83 // and then parse its output, sending the parsed instructions on ext.Dec. 84 // The generate function calls its argument f once for each byte sequence 85 // to be tested. The generate function itself will be called twice, and it must 86 // make the same sequence of calls to f each time. 87 // When a disassembly does not match the internal decoding, 88 // allowedMismatch determines whether this mismatch should be 89 // allowed, or else considered an error. 90 func testExtDis( 91 t *testing.T, 92 syntax string, 93 extdis func(ext *ExtDis) error, 94 generate func(f func([]byte)), 95 allowedMismatch func(text string, inst *Inst, dec ExtInst) bool, 96 ) { 97 start := time.Now() 98 ext := &ExtDis{ 99 Dec: make(chan ExtInst), 100 } 101 errc := make(chan error) 102 103 // First pass: write instructions to input file for external disassembler. 104 file, f, size, err := writeInst(generate) 105 if err != nil { 106 t.Fatal(err) 107 } 108 ext.Size = size 109 ext.File = f 110 defer func() { 111 f.Close() 112 if !*keep { 113 os.Remove(file) 114 } 115 }() 116 117 // Second pass: compare disassembly against our decodings. 118 var ( 119 totalTests = 0 120 totalSkips = 0 121 totalErrors = 0 122 123 errors = make([]string, 0, 100) // Sampled errors, at most cap 124 ) 125 go func() { 126 errc <- extdis(ext) 127 }() 128 129 generate(func(enc []byte) { 130 dec, ok := <-ext.Dec 131 if !ok { 132 t.Errorf("decoding stream ended early") 133 return 134 } 135 inst, text := disasm(syntax, pad(enc)) 136 137 totalTests++ 138 if *dumpTest { 139 fmt.Printf("%x -> %s [%d]\n", enc, dec.text, dec.nenc) 140 } 141 142 if text != dec.text && !strings.Contains(dec.text, "unknown") && syntax == "gnu" { 143 suffix := "" 144 if allowedMismatch(text, &inst, dec) { 145 totalSkips++ 146 if !*mismatch { 147 return 148 } 149 suffix += " (allowed mismatch)" 150 } 151 if strings.Contains(text, "unknown") && strings.Contains(dec.text, ".insn") { 152 return 153 } 154 totalErrors++ 155 cmp := fmt.Sprintf("decode(%x) = %q, %d, want %q, %d%s\n", enc, text, len(enc), dec.text, dec.nenc, suffix) 156 157 if len(errors) >= cap(errors) { 158 j := rand.Intn(totalErrors) 159 if j >= cap(errors) { 160 return 161 } 162 errors = append(errors[:j], errors[j+1:]...) 163 } 164 errors = append(errors, cmp) 165 } 166 }) 167 168 if *mismatch { 169 totalErrors -= totalSkips 170 } 171 172 fmt.Printf("totalTest: %d total skip: %d total error: %d\n", totalTests, totalSkips, totalErrors) 173 // Here are some errors about mismatches(44) 174 for _, b := range errors { 175 t.Log(b) 176 } 177 178 if totalErrors > 0 { 179 t.Fail() 180 } 181 t.Logf("%d test cases, %d expected mismatches, %d failures; %.0f cases/second", totalTests, totalSkips, totalErrors, float64(totalTests)/time.Since(start).Seconds()) 182 t.Logf("decoder coverage: %.1f%%;\n", decodeCoverage()) 183 } 184 185 // Start address of text. 186 const start = 0x8000 187 188 // writeInst writes the generated byte sequences to a new file 189 // starting at offset start. That file is intended to be the input to 190 // the external disassembler. 191 func writeInst(generate func(func([]byte))) (file string, f *os.File, size int, err error) { 192 f, err = ioutil.TempFile("", "riscv64asm") 193 if err != nil { 194 return 195 } 196 197 file = f.Name() 198 199 f.Seek(start, io.SeekStart) 200 w := bufio.NewWriter(f) 201 defer w.Flush() 202 size = 0 203 generate(func(x []byte) { 204 if debug { 205 fmt.Printf("%#x: %x%x\n", start+size, x, zeros[len(x):]) 206 } 207 w.Write(x) 208 w.Write(zeros[len(x):]) 209 size += len(zeros) 210 }) 211 return file, f, size, nil 212 } 213 214 var zeros = []byte{0, 0, 0, 0} 215 216 // pad pads the code sequence with pops. 217 func pad(enc []byte) []byte { 218 if len(enc) < 4 { 219 enc = append(enc[:len(enc):len(enc)], zeros[:4-len(enc)]...) 220 } 221 return enc 222 } 223 224 // disasm returns the decoded instruction and text 225 // for the given source bytes, using the given syntax and mode. 226 func disasm(syntax string, src []byte) (inst Inst, text string) { 227 var err error 228 inst, err = Decode(src) 229 if err != nil { 230 text = "error: " + err.Error() 231 return 232 } 233 text = inst.String() 234 switch syntax { 235 case "gnu": 236 text = GNUSyntax(inst) 237 case "plan9": // [sic] 238 text = GoSyntax(inst, 0, nil, nil) 239 default: 240 text = "error: unknown syntax " + syntax 241 } 242 return 243 } 244 245 // decodeCoverage returns a floating point number denoting the 246 // decoder coverage. 247 func decodeCoverage() float64 { 248 n := 0 249 for _, t := range decoderCover { 250 if t { 251 n++ 252 } 253 } 254 return 100 * float64(1+n) / float64(1+len(decoderCover)) 255 } 256 257 // Helpers for writing disassembler output parsers. 258 259 // isHex reports whether b is a hexadecimal character (0-9a-fA-F). 260 func isHex(b byte) bool { 261 return ('0' <= b && b <= '9') || ('a' <= b && b <= 'f') || ('A' <= b && b <= 'F') 262 } 263 264 // parseHex parses the hexadecimal byte dump in src, 265 // appending the parsed bytes to raw and returning the updated slice. 266 // The returned bool reports whether any invalid hex was found. 267 // Spaces and tabs between bytes are okay but any other non-hex is not. 268 func parseHex(src []byte, raw []byte) ([]byte, bool) { 269 src = bytes.TrimSpace(src) 270 raw, err := hex.AppendDecode(raw, src) 271 if err != nil { 272 return nil, false 273 } 274 return raw, true 275 } 276 277 // Generators. 278 // 279 // The test cases are described as functions that invoke a callback repeatedly, 280 // with a new input sequence each time. These helpers make writing those 281 // a little easier. 282 283 // hexCases generates the cases written in hexadecimal in the encoded string. 284 // Spaces in 'encoded' separate entire test cases, not individual bytes. 285 func hexCases(t *testing.T, encoded string) func(func([]byte)) { 286 return func(try func([]byte)) { 287 for _, x := range strings.Fields(encoded) { 288 src, err := hex.DecodeString(x) 289 if err != nil { 290 t.Errorf("parsing %q: %v", x, err) 291 } 292 try(src) 293 } 294 } 295 } 296 297 // testdataCases generates the test cases recorded in testdata/cases.txt. 298 // It only uses the inputs; it ignores the answers recorded in that file. 299 func testdataCases(t *testing.T, syntax string) func(func([]byte)) { 300 var codes [][]byte 301 input := filepath.Join("testdata", syntax+"cases.txt") 302 data, err := ioutil.ReadFile(input) 303 if err != nil { 304 t.Fatal(err) 305 } 306 for _, line := range strings.Split(string(data), "\n") { 307 line = strings.TrimSpace(line) 308 if line == "" || strings.HasPrefix(line, "#") { 309 continue 310 } 311 f := strings.Fields(line)[0] 312 i := strings.Index(f, "|") 313 if i < 0 { 314 t.Errorf("parsing %q: missing | separator", f) 315 continue 316 } 317 if i%2 != 0 { 318 t.Errorf("parsing %q: misaligned | separator", f) 319 } 320 code, err := hex.DecodeString(f[:i] + f[i+1:]) 321 if err != nil { 322 t.Errorf("parsing %q: %v", f, err) 323 continue 324 } 325 codes = append(codes, code) 326 } 327 328 return func(try func([]byte)) { 329 for _, code := range codes { 330 try(code) 331 } 332 } 333 }