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  }