builder: convert TestBinarySize to a golden file test

This commit is contained in:
Jake Bailey
2026-08-25 19:51:48 -07:00
committed by Ron Evans
parent 840af7a842
commit 8f8ce06594
2 changed files with 125 additions and 33 deletions
+121 -33
View File
@@ -1,8 +1,13 @@
package builder package builder
import ( import (
"flag"
"fmt"
"os"
"regexp" "regexp"
"runtime" "runtime"
"strconv"
"strings"
"testing" "testing"
"time" "time"
@@ -11,21 +16,19 @@ import (
var sema = make(chan struct{}, runtime.NumCPU()) var sema = make(chan struct{}, runtime.NumCPU())
var flagUpdate = flag.Bool("update", false, "update builder package tests")
type sizeTest struct { type sizeTest struct {
target string target string
path string path string
codeSize uint64
rodataSize uint64
dataSize uint64
bssSize uint64
} }
// Test whether code and data size is as expected for the given targets. // Test whether code and data size is as expected for the given targets.
// This tests both the logic of loadProgramSize and checks that code size // This tests both the logic of loadProgramSize and checks that code size
// doesn't change unintentionally. // doesn't change unintentionally.
// //
// If you find that code or data size is reduced, then great! You can reduce the // If you find that code or data size is reduced, then great! You can update the
// number in this test. // golden file by passing -update to the test.
// If you find that the code or data size is increased, take a look as to why // If you find that the code or data size is increased, take a look as to why
// this is. It could be due to an update (LLVM version, Go version, etc) which // this is. It could be due to an update (LLVM version, Go version, etc) which
// is fine, but it could also mean that a recent change introduced this size // is fine, but it could also mean that a recent change introduced this size
@@ -42,34 +45,110 @@ func TestBinarySize(t *testing.T) {
// This is a small number of very diverse targets that we want to test. // This is a small number of very diverse targets that we want to test.
tests := []sizeTest{ tests := []sizeTest{
// microcontrollers // microcontrollers
{"hifive1b", "examples/echo", 4321, 323, 0, 2268}, {"hifive1b", "examples/echo"},
{"microbit", "examples/serial", 2842, 382, 8, 2264}, {"microbit", "examples/serial"},
{"wioterminal", "examples/pininterrupt", 8039, 1665, 132, 7496}, {"wioterminal", "examples/pininterrupt"},
// TODO: also check wasm. Right now this is difficult, because // TODO: also check wasm. Right now this is difficult, because
// wasm binaries are run through wasm-opt and therefore the // wasm binaries are run through wasm-opt and therefore the
// output varies by binaryen version. // output varies by binaryen version.
} }
for _, tc := range tests { sizes := measureBinarySizes(t, tests)
t.Run(tc.target+"/"+tc.path, func(t *testing.T) { output := formatSizeTable(tests, sizes)
t.Parallel() checkGolden(t, "testdata/binary-size.txt", output)
}
// Build the binary. func checkGolden(t *testing.T, path, actual string) {
result := buildBinary(t, tc.target, tc.path) t.Helper()
if *flagUpdate {
// Check whether the size of the binary matches the expected size. if err := os.WriteFile(path, []byte(actual), 0o666); err != nil {
sizes, err := loadProgramSize(result.Executable, nil) t.Fatal("failed to write updated golden file:", err)
if err != nil { }
t.Fatal("could not read program size:", err) return
}
if sizes.Code != tc.codeSize || sizes.ROData != tc.rodataSize || sizes.Data != tc.dataSize || sizes.BSS != tc.bssSize {
t.Errorf("Unexpected code size when compiling: -target=%s %s", tc.target, tc.path)
t.Errorf(" code rodata data bss")
t.Errorf("expected: %6d %6d %6d %6d", tc.codeSize, tc.rodataSize, tc.dataSize, tc.bssSize)
t.Errorf("actual: %6d %6d %6d %6d", sizes.Code, sizes.ROData, sizes.Data, sizes.BSS)
}
})
} }
expected, err := os.ReadFile(path)
if err != nil {
t.Fatal("failed to read golden file:", err)
}
if actual != string(expected) {
t.Errorf("%s does not match expected output (re-run with -update to regenerate):\nexpected:\n%sactual:\n%s", path, expected, actual)
}
}
func measureBinarySizes(t *testing.T, tests []sizeTest) []*programSize {
t.Helper()
type result struct {
index int
size *programSize
err error
}
results := make(chan result, len(tests))
for i, tc := range tests {
tmpdir := t.TempDir()
go func() {
size, err := measureBinarySize(tc, tmpdir)
results <- result{i, size, err}
}()
}
sizes := make([]*programSize, len(tests))
failed := false
for range tests {
result := <-results
if result.err != nil {
tc := tests[result.index]
t.Errorf("%s/%s: %v", tc.target, tc.path, result.err)
failed = true
}
sizes[result.index] = result.size
}
if failed {
t.FailNow()
}
return sizes
}
func measureBinarySize(tc sizeTest, tmpdir string) (*programSize, error) {
result, err := buildBinaryInDir(tc.target, tc.path, tmpdir)
if err != nil {
return nil, err
}
size, err := loadProgramSize(result.Executable, nil)
if err != nil {
return nil, fmt.Errorf("could not read program size: %w", err)
}
return size, nil
}
func formatSizeTable(tests []sizeTest, sizes []*programSize) string {
targetWidth := len("target")
packageWidth := len("package")
codeWidth := len("code")
rodataWidth := len("rodata")
dataWidth := len("data")
bssWidth := len("bss")
for i, tc := range tests {
targetWidth = max(targetWidth, len(tc.target))
packageWidth = max(packageWidth, len(tc.path))
codeWidth = max(codeWidth, len(strconv.FormatUint(sizes[i].Code, 10)))
rodataWidth = max(rodataWidth, len(strconv.FormatUint(sizes[i].ROData, 10)))
dataWidth = max(dataWidth, len(strconv.FormatUint(sizes[i].Data, 10)))
bssWidth = max(bssWidth, len(strconv.FormatUint(sizes[i].BSS, 10)))
}
var output strings.Builder
fmt.Fprintf(&output, "%-*s %-*s %*s %*s %*s %*s\n",
targetWidth, "target", packageWidth, "package",
codeWidth, "code", rodataWidth, "rodata", dataWidth, "data", bssWidth, "bss")
for i, tc := range tests {
size := sizes[i]
fmt.Fprintf(&output, "%-*s %-*s %*d %*d %*d %*d\n",
targetWidth, tc.target, packageWidth, tc.path,
codeWidth, size.Code, rodataWidth, size.ROData,
dataWidth, size.Data, bssWidth, size.BSS)
}
return output.String()
} }
// Check that the -size=full flag attributes binary size to the correct package // Check that the -size=full flag attributes binary size to the correct package
@@ -114,6 +193,15 @@ func TestSizeFull(t *testing.T) {
} }
func buildBinary(t *testing.T, targetString, pkgName string) BuildResult { func buildBinary(t *testing.T, targetString, pkgName string) BuildResult {
t.Helper()
result, err := buildBinaryInDir(targetString, pkgName, t.TempDir())
if err != nil {
t.Fatal(err)
}
return result
}
func buildBinaryInDir(targetString, pkgName, tmpdir string) (BuildResult, error) {
options := compileopts.Options{ options := compileopts.Options{
Target: targetString, Target: targetString,
Opt: "z", Opt: "z",
@@ -124,15 +212,15 @@ func buildBinary(t *testing.T, targetString, pkgName string) BuildResult {
} }
target, err := compileopts.LoadTarget(&options) target, err := compileopts.LoadTarget(&options)
if err != nil { if err != nil {
t.Fatal("could not load target:", err) return BuildResult{}, fmt.Errorf("could not load target: %w", err)
} }
config := &compileopts.Config{ config := &compileopts.Config{
Options: &options, Options: &options,
Target: target, Target: target,
} }
result, err := Build(pkgName, "", t.TempDir(), config) result, err := Build(pkgName, "", tmpdir, config)
if err != nil { if err != nil {
t.Fatal("could not build:", err) return BuildResult{}, fmt.Errorf("could not build: %w", err)
} }
return result return result, nil
} }
+4
View File
@@ -0,0 +1,4 @@
target package code rodata data bss
hifive1b examples/echo 4321 323 0 2268
microbit examples/serial 2842 382 8 2264
wioterminal examples/pininterrupt 8039 1665 132 7496