You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
garble/main_test.go

499 lines
13 KiB
Go

// Copyright (c) 2019, The Garble Authors.
// See LICENSE for licensing information.
package main
import (
"flag"
"fmt"
"go/ast"
"go/printer"
"go/token"
"io/fs"
mathrand "math/rand"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/rogpeppe/go-internal/goproxytest"
"github.com/rogpeppe/go-internal/gotooltest"
"github.com/rogpeppe/go-internal/testscript"
ah "mvdan.cc/garble/internal/asthelper"
)
var proxyURL string
func TestMain(m *testing.M) {
// If GORACE is unset, lower the default of atexit_sleep_ms=1000,
// since otherwise every execution of garble through the test binary
// would sleep for one second before exiting.
// Given how many times garble runs via toolexec, that is very slow!
// If GORACE is set, we assume that the caller knows what they are doing,
// and we don't try to replace or modify their flags.
if os.Getenv("GORACE") == "" {
os.Setenv("GORACE", "atexit_sleep_ms=10")
}
if os.Getenv("RUN_GARBLE_MAIN") == "true" {
os.Exit(main1())
}
os.Exit(testscript.RunMain(garbleMain{m}, map[string]func() int{
"garble": main1,
}))
}
type garbleMain struct {
m *testing.M
}
func (m garbleMain) Run() int {
// Start the Go proxy server running for all tests.
srv, err := goproxytest.NewServer("testdata/mod", "")
if err != nil {
panic(fmt.Sprintf("cannot start proxy: %v", err))
}
proxyURL = srv.URL
return m.m.Run()
}
var update = flag.Bool("u", false, "update testscript output files")
func TestScript(t *testing.T) {
t.Parallel()
execPath, err := os.Executable()
if err != nil {
t.Fatal(err)
}
tempCacheDir := t.TempDir()
hostCacheDir, err := os.UserCacheDir()
if err != nil {
t.Fatal(err)
}
p := testscript.Params{
Dir: filepath.Join("testdata", "script"),
Setup: func(env *testscript.Env) error {
// Use testdata/mod as our module proxy.
env.Setenv("GOPROXY", proxyURL)
// We use our own proxy, so avoid sum.golang.org.
env.Setenv("GONOSUMDB", "*")
// "go build" starts many short-lived Go processes,
// such as asm, buildid, compile, and link.
// They don't allocate huge amounts of memory,
// and they'll exit within seconds,
// so using the GC is basically a waste of CPU.
// Turn it off entirely, releasing memory on exit.
//
// We don't want this setting always on,
// as it could result in memory problems for users.
// But it helps for our test suite,
// as the packages are relatively small.
env.Setenv("GOGC", "off")
env.Setenv("gofullversion", runtime.Version())
env.Setenv("EXEC_PATH", execPath)
if os.Getenv("GOCOVERDIR") != "" {
// Don't share cache dirs with the host if we want to collect code
// coverage. Otherwise, the coverage info might be incomplete.
env.Setenv("GOCACHE", filepath.Join(tempCacheDir, "go-cache"))
env.Setenv("GARBLE_CACHE", filepath.Join(tempCacheDir, "garble-cache"))
} else {
// GOCACHE is initialized by gotooltest to use the host's cache.
env.Setenv("GARBLE_CACHE", filepath.Join(hostCacheDir, "garble"))
}
return nil
},
// TODO: this condition should probably be supported by gotooltest
Condition: func(cond string) (bool, error) {
switch cond {
case "cgo":
out, err := exec.Command("go", "env", "CGO_ENABLED").CombinedOutput()
if err != nil {
return false, err
}
result := strings.TrimSpace(string(out))
switch result {
case "0", "1":
return result == "1", nil
default:
return false, fmt.Errorf("unknown CGO_ENABLED: %q", result)
}
}
return false, fmt.Errorf("unknown condition")
},
Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
"sleep": sleep,
"binsubstr": binsubstr,
"bincmp": bincmp,
"generate-literals": generateLiterals,
"setenvfile": setenvfile,
"grepfiles": grepfiles,
},
UpdateScripts: *update,
RequireExplicitExec: true,
RequireUniqueNames: true,
}
if err := gotooltest.Setup(&p); err != nil {
t.Fatal(err)
}
testscript.Run(t, p)
}
func createFile(ts *testscript.TestScript, path string) *os.File {
file, err := os.Create(ts.MkAbs(path))
if err != nil {
ts.Fatalf("%v", err)
}
return file
}
// sleep is akin to a shell's sleep builtin.
// Note that tests should almost never use this; it's currently only used to
// work around a low-level Go syscall race on Linux.
func sleep(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 1 {
ts.Fatalf("usage: sleep duration")
}
d, err := time.ParseDuration(args[0])
if err != nil {
ts.Fatalf("%v", err)
}
time.Sleep(d)
}
func binsubstr(ts *testscript.TestScript, neg bool, args []string) {
if len(args) < 2 {
ts.Fatalf("usage: binsubstr file substr...")
}
data := ts.ReadFile(args[0])
var failed []string
for _, substr := range args[1:] {
match := strings.Contains(data, substr)
if match && neg {
failed = append(failed, substr)
} else if !match && !neg {
failed = append(failed, substr)
}
}
if len(failed) > 0 && neg {
ts.Fatalf("unexpected match for %q in %s", failed, args[0])
} else if len(failed) > 0 {
ts.Fatalf("expected match for %q in %s", failed, args[0])
}
}
func bincmp(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
ts.Fatalf("usage: bincmp file1 file2")
}
for _, arg := range args {
switch arg {
case "stdout", "stderr":
// Note that the diffoscope call below would not deal with
// stdout/stderr either.
ts.Fatalf("bincmp is for binary files. did you mean cmp?")
}
}
data1 := ts.ReadFile(args[0])
data2 := ts.ReadFile(args[1])
if neg {
if data1 == data2 {
ts.Fatalf("%s and %s don't differ", args[0], args[1])
}
return
}
if data1 != data2 {
if _, err := exec.LookPath("diffoscope"); err == nil {
// We'll error below; ignore the exec error here.
ts.Exec("diffoscope",
"--diff-context", "2", // down from 7 by default
"--max-text-report-size", "4096", // no limit (in bytes) by default; avoid huge output
ts.MkAbs(args[0]), ts.MkAbs(args[1]))
} else {
ts.Logf("diffoscope not found; skipping")
}
outDir := "bincmp_output"
err := os.MkdirAll(outDir, 0o777)
ts.Check(err)
file1, err := os.CreateTemp(outDir, "file1-*")
ts.Check(err)
_, err = file1.Write([]byte(data1))
ts.Check(err)
err = file1.Close()
ts.Check(err)
file2, err := os.CreateTemp(outDir, "file2-*")
ts.Check(err)
_, err = file2.Write([]byte(data2))
ts.Check(err)
err = file2.Close()
ts.Check(err)
ts.Logf("wrote files to %s and %s", file1.Name(), file2.Name())
sizeDiff := len(data2) - len(data1)
ts.Fatalf("%s and %s differ; diffoscope above, size diff: %+d",
args[0], args[1], sizeDiff)
}
}
var testRand = mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
func generateStringLit(minSize int) *ast.BasicLit {
buffer := make([]byte, minSize)
_, err := testRand.Read(buffer)
if err != nil {
panic(err)
}
return ah.StringLit(string(buffer) + "a_unique_string_that_is_part_of_all_extra_literals")
}
// generateLiterals creates a new source code file with a few random literals inside.
// All literals contain the string "a_unique_string_that_is_part_of_all_extra_literals"
// so we can later check if they are all obfuscated by looking for this substring.
// The code is designed such that the Go compiler does not optimize away the literals,
// which would destroy the test.
// This is achieved by defining a global variable `var x = ""` and an `init` function
// which appends all literals to `x`.
func generateLiterals(ts *testscript.TestScript, neg bool, args []string) {
if neg {
ts.Fatalf("unsupported: ! generate-literals")
}
if len(args) != 1 {
ts.Fatalf("usage: generate-literals file")
}
codePath := args[0]
// Global string variable to which which we append string literals: `var x = ""`
globalVar := &ast.GenDecl{
Tok: token.VAR,
Specs: []ast.Spec{
&ast.ValueSpec{
Names: []*ast.Ident{ast.NewIdent("x")},
Values: []ast.Expr{
&ast.BasicLit{Kind: token.STRING, Value: `""`},
},
},
},
}
var statements []ast.Stmt
// Assignments which append 100 random small literals to x: `x += "the_small_random_literal"`
for range 100 {
statements = append(
statements,
&ast.AssignStmt{
Lhs: []ast.Expr{ast.NewIdent("x")},
Tok: token.ADD_ASSIGN,
Rhs: []ast.Expr{generateStringLit(1 + testRand.Intn(255))},
},
)
}
// Assignments which append 5 random huge literals to x: `x += "the_huge_random_literal"`
// We add huge literals to make sure we obfuscate them fast.
// 5 * 128KiB is large enough that it would take a very, very long time
// to obfuscate those literals if too complex obfuscators are used.
for range 5 {
statements = append(
statements,
&ast.AssignStmt{
Lhs: []ast.Expr{ast.NewIdent("x")},
Tok: token.ADD_ASSIGN,
Rhs: []ast.Expr{generateStringLit(128 << 10)},
},
)
}
// An `init` function which includes all assignments from above
initFunc := &ast.FuncDecl{
Name: &ast.Ident{
Name: "init",
},
Type: &ast.FuncType{},
Body: ah.BlockStmt(statements...),
}
// A file with the global string variable and init function
file := &ast.File{
Name: ast.NewIdent("main"),
Decls: []ast.Decl{
globalVar,
initFunc,
},
}
codeFile := createFile(ts, codePath)
defer codeFile.Close()
if err := printer.Fprint(codeFile, token.NewFileSet(), file); err != nil {
ts.Fatalf("%v", err)
}
}
func setenvfile(ts *testscript.TestScript, neg bool, args []string) {
if neg {
ts.Fatalf("unsupported: ! setenvfile")
}
if len(args) != 2 {
ts.Fatalf("usage: setenvfile name file")
}
ts.Setenv(args[0], ts.ReadFile(args[1]))
}
func grepfiles(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
ts.Fatalf("usage: grepfiles path pattern")
}
anyFound := false
path, pattern := ts.MkAbs(args[0]), args[1]
rx := regexp.MustCompile(pattern)
if err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if rx.MatchString(path) {
if neg {
return fmt.Errorf("%q matches %q", path, pattern)
} else {
anyFound = true
return fs.SkipAll
}
}
return nil
}); err != nil {
ts.Fatalf("%s", err)
}
if !neg && !anyFound {
ts.Fatalf("no matches for %q", pattern)
}
}
func TestSplitFlagsFromArgs(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args []string
want [2][]string
}{
{"Empty", []string{}, [2][]string{{}, nil}},
{
"JustFlags",
[]string{"-foo", "bar", "-baz"},
[2][]string{{"-foo", "bar", "-baz"}, nil},
},
{
"JustArgs",
[]string{"some", "pkgs"},
[2][]string{{}, {"some", "pkgs"}},
},
{
"FlagsAndArgs",
[]string{"-foo=bar", "baz"},
[2][]string{{"-foo=bar"}, {"baz"}},
},
{
"BoolFlagsAndArgs",
[]string{"-race", "pkg"},
[2][]string{{"-race"}, {"pkg"}},
},
{
"ExplicitBoolFlag",
[]string{"-race=true", "pkg"},
[2][]string{{"-race=true"}, {"pkg"}},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
flags, args := splitFlagsFromArgs(test.args)
got := [2][]string{flags, args}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Fatalf("splitFlagsFromArgs(%q) mismatch (-want +got):\n%s", test.args, diff)
}
})
}
}
func TestFilterForwardBuildFlags(t *testing.T) {
t.Parallel()
tests := []struct {
name string
flags []string
want []string
}{
{"Empty", []string{}, nil},
{
"NoBuild",
[]string{"-short", "-json"},
nil,
},
{
"Mixed",
[]string{"-short", "-tags", "foo", "-mod=readonly", "-json"},
[]string{"-tags", "foo", "-mod=readonly"},
},
{
"NonBinarySkipped",
[]string{"-o", "binary", "-tags", "foo"},
[]string{"-tags", "foo"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
got, _ := filterForwardBuildFlags(test.flags)
if diff := cmp.Diff(test.want, got); diff != "" {
t.Fatalf("filterForwardBuildFlags(%q) mismatch (-want +got):\n%s", test.flags, diff)
}
})
}
}
func TestFlagValue(t *testing.T) {
t.Parallel()
tests := []struct {
name string
flags []string
flagName string
want string
}{
{"StrSpace", []string{"-buildid", "bar"}, "-buildid", "bar"},
{"StrSpaceDash", []string{"-buildid", "-bar"}, "-buildid", "-bar"},
{"StrEqual", []string{"-buildid=bar"}, "-buildid", "bar"},
{"StrEqualDash", []string{"-buildid=-bar"}, "-buildid", "-bar"},
{"StrMissing", []string{"-foo"}, "-buildid", ""},
{"StrNotFollowed", []string{"-buildid"}, "-buildid", ""},
{"StrEmpty", []string{"-buildid="}, "-buildid", ""},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
got := flagValue(test.flags, test.flagName)
if got != test.want {
t.Fatalf("flagValue(%q, %q) got %q, want %q",
test.flags, test.flagName, got, test.want)
}
})
}
}