The relocation type number shifted between Go 1.26 (103) and Go 1.27 (106) because new LoongArch relocations were inserted. Detect the Go version at runtime and use the correct value.
514 lines
16 KiB
Go
514 lines
16 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
package asm
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/binary"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||
)
|
||
|
||
// goobjView is a minimal parsed view of a GOOBJ payload, enough to check
|
||
// the emitter's output block by block.
|
||
type goobjView struct {
|
||
t *testing.T
|
||
b []byte
|
||
offs [blkEnd + 1]uint32
|
||
strOff uint32
|
||
}
|
||
|
||
func openGoobj(t *testing.T, data []byte) *goobjView {
|
||
t.Helper()
|
||
i := bytes.Index(data, []byte(goobjMagic))
|
||
if i < 0 {
|
||
t.Fatal("no GOOBJ magic in output")
|
||
}
|
||
v := &goobjView{t: t, b: data[i:], strOff: uint32(i + 96)}
|
||
for j := 0; j <= blkEnd; j++ {
|
||
v.offs[j] = binary.LittleEndian.Uint32(v.b[20+4*j:])
|
||
}
|
||
return v
|
||
}
|
||
|
||
func (v *goobjView) blk(i int) []byte { return v.b[v.offs[i]:v.offs[i+1]] }
|
||
|
||
func (v *goobjView) str(off, ln uint32) string {
|
||
return string(v.b[off : off+ln])
|
||
}
|
||
|
||
type goobjSymView struct {
|
||
name string
|
||
abi uint16
|
||
typ uint8
|
||
flag uint8
|
||
flag2 uint8
|
||
size uint32
|
||
align uint32
|
||
}
|
||
|
||
func (v *goobjView) syms(i int) []goobjSymView {
|
||
var out []goobjSymView
|
||
for x := v.blk(i); len(x) >= 21; x = x[21:] {
|
||
le := binary.LittleEndian
|
||
out = append(out, goobjSymView{
|
||
name: v.str(le.Uint32(x[4:]), le.Uint32(x[0:])),
|
||
abi: le.Uint16(x[8:]),
|
||
typ: x[10],
|
||
flag: x[11],
|
||
flag2: x[12],
|
||
size: le.Uint32(x[13:]),
|
||
align: le.Uint32(x[17:]),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// TestGOObjectStructure checks the emitted object's blocks against the
|
||
// ground truth captured from go tool asm: the symbol tables, the FuncInfo
|
||
// contents, the pc-value tables, the relocation and the aux wiring.
|
||
func TestGOObjectStructure(t *testing.T) {
|
||
f, errs := parser.Parse("t_amd64.s", `
|
||
#include "textflag.h"
|
||
|
||
TEXT ·addq(SB), NOSPLIT, $0-24
|
||
MOVQ a+0(FP), AX
|
||
MOVQ b+8(FP), CX
|
||
ADDQ CX, AX
|
||
MOVQ AX, ret+16(FP)
|
||
RET
|
||
|
||
TEXT ·loadmask(SB), NOSPLIT, $0-8
|
||
VMOVDQU mask<>(SB), X0
|
||
VPMOVMSKB X0, AX
|
||
MOVQ AX, ret+0(FP)
|
||
RET
|
||
|
||
GLOBL mask<>(SB), RODATA, $16
|
||
DATA mask<>+0(SB)/8, $0x0807060504030201
|
||
DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFile(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %v", err)
|
||
}
|
||
obj, err := img.GOObject("testpkg", "t_amd64.s")
|
||
if err != nil {
|
||
t.Fatalf("GOObject: %v", err)
|
||
}
|
||
v := openGoobj(t, obj)
|
||
|
||
if flags := binary.LittleEndian.Uint32(v.b[16:]); flags != 4 {
|
||
t.Errorf("flags = %#x, want ObjFlagFromAssembly (4)", flags)
|
||
}
|
||
|
||
// Package defs: the static GLOBL, then per function the FuncInfo and the
|
||
// two DWARF symbols (debug_line program, subprogram DIE).
|
||
defs := v.syms(blkSymdef)
|
||
if len(defs) != 7 {
|
||
t.Fatalf("symdefs = %d, want 7", len(defs))
|
||
}
|
||
if defs[0].name != "mask" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 16 || defs[0].flag2 != symFlag2Link {
|
||
t.Errorf("mask symbol = %+v", defs[0])
|
||
}
|
||
if defs[1].name != "" || defs[1].typ != kindSDATA || defs[1].size != 28 {
|
||
t.Errorf("addq funcinfo symbol = %+v", defs[1])
|
||
}
|
||
if defs[2].name != "" || defs[2].typ != kindSDWARFLINES || defs[2].size == 0 {
|
||
t.Errorf("addq lines symbol = %+v", defs[2])
|
||
}
|
||
if defs[3].name != "" || defs[3].typ != kindSDWARFFCN || defs[3].size == 0 {
|
||
t.Errorf("addq DIE symbol = %+v", defs[3])
|
||
}
|
||
if defs[4].name != "" || defs[4].typ != kindSDATA || defs[4].size != 28 {
|
||
t.Errorf("loadmask funcinfo symbol = %+v", defs[4])
|
||
}
|
||
|
||
// Non-package defs: four pc tables and the function, per function.
|
||
nps := v.syms(blkNonpkgdef)
|
||
if len(nps) != 10 {
|
||
t.Fatalf("nonpkgdefs = %d, want 10", len(nps))
|
||
}
|
||
fn := nps[4]
|
||
if fn.name != "testpkg.addq" || fn.typ != kindSTEXT || fn.flag != symFlagNoSplit || fn.size != 19 {
|
||
t.Errorf("addq symbol = %+v", fn)
|
||
}
|
||
for i, s := range []int{0, 1, 2, 3, 5, 6, 7, 8} {
|
||
if nps[s].typ != kindSRODATA || nps[s].align != 1 || nps[s].name != "" {
|
||
t.Errorf("pc table %d = %+v", i, nps[s])
|
||
}
|
||
}
|
||
|
||
// FuncInfo: args 24, FuncFlag Asm, one file, no inline tree.
|
||
le := binary.LittleEndian
|
||
data := v.blk(blkData)
|
||
didx := v.blk(blkDataIdx)
|
||
fi := data[16:44]
|
||
if le.Uint32(fi[0:]) != 24 || le.Uint32(fi[4:]) != 0 || fi[8] != 0 || fi[9] != funcFlagAsm ||
|
||
le.Uint32(fi[16:]) != 1 || le.Uint32(fi[20:]) != 0 || le.Uint32(fi[24:]) != 0 {
|
||
t.Errorf("funcinfo bytes %x", fi)
|
||
}
|
||
|
||
// The pc-value tables of addq (non-package indices 0–3, so global
|
||
// indices 7–10): pcsp a flat zero over the whole function, pcinline a
|
||
// flat -1, both with the pc delta in MinLC (1) units.
|
||
pcsp := data[le.Uint32(didx[4*7:]):]
|
||
if got := pcsp[:3]; !bytes.Equal(got, []byte{0x02, 19, 0x00}) {
|
||
t.Errorf("pcsp = %x, want 021300", got)
|
||
}
|
||
pcinl := data[le.Uint32(didx[4*10:]):]
|
||
if got := pcinl[:3]; !bytes.Equal(got, []byte{0x00, 19, 0x00}) {
|
||
t.Errorf("pcinline = %x, want 001300", got)
|
||
}
|
||
|
||
// Relocations: the four DWARF address references (two per function, in
|
||
// definition order), then the loadmask code's R_PCREL against the
|
||
// GLOBL, with the field in the function code left zero. The loadmask
|
||
// code's offset comes from the data index (7 defs + 9 non-package).
|
||
relocs := v.blk(blkReloc)
|
||
if len(relocs) != 5*23 {
|
||
t.Fatalf("relocs = %d bytes, want 5 entries", len(relocs))
|
||
}
|
||
// addq's DWARF references (defs 2 and 3) against the function, which
|
||
// is non-package index 4.
|
||
lr := relocs[:23]
|
||
if int32(le.Uint32(lr[0:])) != 3 || lr[4] != 8 || le.Uint16(lr[5:]) != relocAddr ||
|
||
le.Uint32(lr[15:]) != pkgIdxNone || le.Uint32(lr[19:]) != 4 {
|
||
t.Errorf("addq lines reloc = %x", lr)
|
||
}
|
||
dr := relocs[23:46]
|
||
if dr[4] != 4 || le.Uint16(dr[5:]) != relocDWTXTADDRU4() ||
|
||
le.Uint32(dr[15:]) != pkgIdxNone || le.Uint32(dr[19:]) != 4 {
|
||
t.Errorf("addq DIE reloc = %x", dr)
|
||
}
|
||
cr := relocs[4*23:]
|
||
off := int32(le.Uint32(cr[0:]))
|
||
if off != 4 || cr[4] != 4 || le.Uint16(cr[5:]) != relocPCRel ||
|
||
le.Uint64(cr[7:]) != 0 || le.Uint32(cr[15:]) != pkgIdxSelf || le.Uint32(cr[19:]) != 0 {
|
||
t.Errorf("loadmask reloc = %x", cr)
|
||
}
|
||
lm := le.Uint32(didx[4*16:])
|
||
code := data[lm : lm+18]
|
||
if !bytes.Equal(code[4:8], []byte{0, 0, 0, 0}) {
|
||
t.Errorf("relocated field = %x, want zeroed", code[4:8])
|
||
}
|
||
|
||
// Aux wiring: FuncInfo, the two DWARF symbols (package symbols), then
|
||
// the four pc tables (non-package symbols).
|
||
auxs := v.blk(blkAux)
|
||
if len(auxs) != 2*7*9 {
|
||
t.Fatalf("aux = %d bytes, want 14 entries", len(auxs))
|
||
}
|
||
wantAux := []struct {
|
||
typ uint8
|
||
pkg uint32
|
||
idx uint32
|
||
}{
|
||
{auxFuncInfo, pkgIdxSelf, 1},
|
||
{auxDwarfInfo, pkgIdxSelf, 3},
|
||
{auxDwarfLines, pkgIdxSelf, 2},
|
||
{auxPcsp, pkgIdxNone, 0},
|
||
{auxPcfile, pkgIdxNone, 1},
|
||
{auxPcline, pkgIdxNone, 2},
|
||
{auxPcinline, pkgIdxNone, 3},
|
||
{auxFuncInfo, pkgIdxSelf, 4},
|
||
{auxDwarfInfo, pkgIdxSelf, 6},
|
||
{auxDwarfLines, pkgIdxSelf, 5},
|
||
{auxPcsp, pkgIdxNone, 5},
|
||
{auxPcfile, pkgIdxNone, 6},
|
||
{auxPcline, pkgIdxNone, 7},
|
||
{auxPcinline, pkgIdxNone, 8},
|
||
}
|
||
for i, w := range wantAux {
|
||
e := auxs[i*9:]
|
||
if e[0] != w.typ || le.Uint32(e[1:]) != w.pkg || le.Uint32(e[5:]) != w.idx {
|
||
t.Errorf("aux[%d] = {%d,%d,%d}, want {%d,%d,%d}", i, e[0], le.Uint32(e[1:]), le.Uint32(e[5:]), w.typ, w.pkg, w.idx)
|
||
}
|
||
}
|
||
}
|
||
|
||
// decodePCValues decodes a pc-value table into (pc, value) steps. The
|
||
// table ends with a final unsigned pc delta covering the rest of the
|
||
// function, followed by a zero byte that carries no value delta.
|
||
func decodePCValues(b []byte) (pcs, vals []int64) {
|
||
val, n := binary.Varint(b)
|
||
b = b[n:]
|
||
val-- // the first delta is against the implicit -1
|
||
var pc int64
|
||
pcs = append(pcs, pc)
|
||
vals = append(vals, val)
|
||
for {
|
||
pcd, n := binary.Uvarint(b)
|
||
b = b[n:]
|
||
if pcd == 0 { // zero pc delta terminates the table
|
||
break
|
||
}
|
||
pc += int64(pcd)
|
||
if len(b) == 1 && b[0] == 0 { // final coverage, no value change
|
||
break
|
||
}
|
||
vd, n := binary.Varint(b)
|
||
b = b[n:]
|
||
val += vd
|
||
pcs = append(pcs, pc)
|
||
vals = append(vals, val)
|
||
}
|
||
return pcs, vals
|
||
}
|
||
|
||
// TestGOObjectPcspFrame checks the pcsp table of a frame-pointer function:
|
||
// the prologue raises the stack delta to 8+frame, the RET's epilogue
|
||
// restores it to zero.
|
||
func TestGOObjectPcspFrame(t *testing.T) {
|
||
f, errs := parser.Parse("frame_amd64.s", `
|
||
#include "textflag.h"
|
||
TEXT ·framed(SB), NOSPLIT, $8-0
|
||
MOVQ BP, AX
|
||
RET
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFile(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %v", err)
|
||
}
|
||
fn := img.Funcs[0]
|
||
pcs, vals := decodePCValues(pcspTable(fn, 1))
|
||
// Prologue: PUSHQ BP (1 byte, +8), MOVQ SP, BP (3 bytes, no change),
|
||
// SUBQ $8, SP (4 bytes, +16 in total); the RET's epilogue unwinds
|
||
// ADDQ $8, SP (+8) then POPQ BP (0).
|
||
wantPCs := []int64{0, 1, 8}
|
||
wantVals := []int64{0, 8, 16}
|
||
if len(pcs) < len(wantPCs) {
|
||
t.Fatalf("pcsp pcs = %v vals = %v", pcs, vals)
|
||
}
|
||
for i := range wantPCs {
|
||
if pcs[i] != wantPCs[i] || vals[i] != wantVals[i] {
|
||
t.Errorf("pcsp[%d] = (%d,%d), want (%d,%d) — all: %v %v", i, pcs[i], vals[i], wantPCs[i], wantVals[i], pcs, vals)
|
||
}
|
||
}
|
||
// The last two steps unwind the epilogue to zero.
|
||
n := len(pcs)
|
||
if vals[n-1] != 0 || vals[n-2] != 8 {
|
||
t.Errorf("epilogue steps = %v %v, want …8, 0", pcs, vals)
|
||
}
|
||
// The table covers the whole function.
|
||
if last := pcs[n-1]; last >= int64(fn.Size) {
|
||
t.Errorf("last pc %d beyond function size %d", last, fn.Size)
|
||
}
|
||
}
|
||
|
||
// TestGOObjectExternalRejected checks that a reference to a symbol no GLOBL
|
||
// defines is reported: GOOBJ emission resolves only file-local symbols so
|
||
// far.
|
||
func TestGOObjectExternalRejected(t *testing.T) {
|
||
f, errs := parser.Parse("ext_amd64.s", `
|
||
#include "textflag.h"
|
||
TEXT ·useext(SB), NOSPLIT, $0-8
|
||
MOVQ elsewhere(SB), AX
|
||
MOVQ AX, ret+0(FP)
|
||
RET
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFile(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %v", err)
|
||
}
|
||
if _, err := img.GOObject("p", "ext_amd64.s"); err == nil || !strings.Contains(err.Error(), "external") {
|
||
t.Errorf("error = %v, want an external-symbol error", err)
|
||
}
|
||
}
|
||
|
||
// TestGOObjectLinkAndRun is the end-to-end check: assemble the test
|
||
// functions to a GOOBJ, swap it into a go build in place of the toolchain's
|
||
// assembly object, link, and run — the output must match the baseline
|
||
// binary the Go assembler produced. Skipped when no Go toolchain is
|
||
// available.
|
||
func TestGOObjectLinkAndRun(t *testing.T) {
|
||
goBin, err := exec.LookPath("go")
|
||
if err != nil {
|
||
t.Skip("no Go toolchain available")
|
||
}
|
||
dir := t.TempDir()
|
||
|
||
const asmSrc = `
|
||
#include "textflag.h"
|
||
|
||
TEXT ·addq(SB), NOSPLIT, $0-24
|
||
MOVQ a+0(FP), AX
|
||
MOVQ b+8(FP), CX
|
||
ADDQ CX, AX
|
||
MOVQ AX, ret+16(FP)
|
||
RET
|
||
|
||
TEXT ·loadmask(SB), NOSPLIT, $0-8
|
||
VMOVDQU mask<>(SB), X0
|
||
VPMOVMSKB X0, AX
|
||
MOVQ AX, ret+0(FP)
|
||
RET
|
||
|
||
GLOBL mask<>(SB), RODATA, $16
|
||
DATA mask<>+0(SB)/8, $0x0807060504030201
|
||
DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
||
`
|
||
const mainSrc = `package main
|
||
|
||
func addq(a, b int64) int64
|
||
func loadmask() int64
|
||
|
||
func main() {
|
||
println(addq(41, 1))
|
||
println(loadmask())
|
||
}
|
||
`
|
||
if err := os.WriteFile(filepath.Join(dir, "main_amd64.s"), []byte(asmSrc), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module goobjtest\n\ngo 1.26\n"), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// Baseline build with the toolchain's assembler; keep the work
|
||
// directory and the commands the build used.
|
||
cmd := exec.Command(goBin, "build", "-x", "-work", "-o", "app", ".")
|
||
cmd.Dir = dir
|
||
buildLog, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||
}
|
||
var work string
|
||
var asmObj, pkgArch, linkLine string
|
||
for _, line := range strings.Split(string(buildLog), "\n") {
|
||
switch {
|
||
case strings.HasPrefix(line, "WORK="):
|
||
work = strings.TrimPrefix(line, "WORK=")
|
||
case strings.Contains(line, "/asm ") && strings.Contains(line, "-o ") && strings.Contains(line, "main_amd64.s") && !strings.Contains(line, "-gensymabis"):
|
||
asmObj = fieldAfter(line, "-o")
|
||
case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"):
|
||
pkgArch = strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1])
|
||
pkgArch = strings.Fields(strings.SplitN(pkgArch, "#", 2)[0])[0]
|
||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||
linkLine = line
|
||
}
|
||
}
|
||
if work == "" || asmObj == "" || pkgArch == "" || linkLine == "" {
|
||
t.Fatalf("could not locate the build steps:\n%s", buildLog)
|
||
}
|
||
asmObj = strings.ReplaceAll(asmObj, "$WORK", work)
|
||
pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work)
|
||
|
||
// The baseline's answer.
|
||
baseOut, err := exec.Command(filepath.Join(dir, "app")).CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("run baseline: %v\n%s", err, baseOut)
|
||
}
|
||
|
||
// Assemble the same source with gasm and swap the object in.
|
||
pf, perrs := parser.Parse(filepath.Join(dir, "main_amd64.s"), asmSrc)
|
||
if len(perrs) > 0 {
|
||
t.Fatalf("parse: %v", perrs)
|
||
}
|
||
img, err := AssembleFile(pf)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %v", err)
|
||
}
|
||
obj, err := img.GOObject("main", filepath.Join(dir, "main_amd64.s"))
|
||
if err != nil {
|
||
t.Fatalf("GOObject: %v", err)
|
||
}
|
||
|
||
// Rebuild the package archive with our object in place of the
|
||
// toolchain's (go tool pack has no replace-in-place that dedupes, so
|
||
// extract, substitute and repack). The archive member holding the
|
||
// assembler's output is named after the asm object file, e.g.
|
||
// main_amd64.o.
|
||
extract := exec.Command(goBin, "tool", "pack", "x", pkgArch)
|
||
membersDir := filepath.Join(dir, "members")
|
||
if err := os.MkdirAll(membersDir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
extract.Dir = membersDir
|
||
if out, err := extract.CombinedOutput(); err != nil {
|
||
t.Fatalf("pack x: %v\n%s", err, out)
|
||
}
|
||
member := filepath.Join(membersDir, filepath.Base(asmObj))
|
||
if err := os.Chmod(member, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(member, obj, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch)
|
||
listOut, err := listCmd.CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("pack t: %v\n%s", err, listOut)
|
||
}
|
||
newArch := filepath.Join(dir, "pkg.a")
|
||
args := []string{"tool", "pack", "c", newArch}
|
||
seen := map[string]bool{}
|
||
for _, m := range strings.Fields(string(listOut)) {
|
||
if seen[m] {
|
||
continue
|
||
}
|
||
seen[m] = true
|
||
if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
args = append(args, filepath.Join(membersDir, m))
|
||
}
|
||
pack := exec.Command(goBin, args...)
|
||
pack.Dir = membersDir
|
||
if out, err := pack.CombinedOutput(); err != nil {
|
||
t.Fatalf("pack c: %v\n%s", err, out)
|
||
}
|
||
|
||
// Link with our archive. The link line carries a GOROOT assignment
|
||
// and $WORK placeholders; run it through the shell with the
|
||
// GOEXPERIMENT the toolchain expects (the linker compares the object
|
||
// header against its own, experiments included).
|
||
goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output()
|
||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "_pkg_.a"), newArch)
|
||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "app2"))
|
||
link := exec.Command("sh", "-c", linkLine)
|
||
link.Dir = dir
|
||
link.Env = append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp)))
|
||
if out, err := link.CombinedOutput(); err != nil {
|
||
t.Fatalf("link with gasm object: %v\n%s", err, out)
|
||
}
|
||
got, err := exec.Command(filepath.Join(dir, "app2")).CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("run gasm-linked binary: %v\n%s", err, got)
|
||
}
|
||
if !bytes.Equal(got, baseOut) {
|
||
t.Errorf("gasm-linked output %q, want baseline %q", got, baseOut)
|
||
}
|
||
}
|
||
|
||
// fieldAfter returns the whitespace-delimited field following the first
|
||
// occurrence of flag in line.
|
||
func fieldAfter(line, flag string) string {
|
||
fields := strings.Fields(line)
|
||
for i, f := range fields {
|
||
if f == flag && i+1 < len(fields) {
|
||
return fields[i+1]
|
||
}
|
||
}
|
||
return ""
|
||
}
|