mirror of
https://github.com/burrowers/garble.git
synced 2026-04-22 15:47:04 +08:00
ef2385ee97
The added benchmark script shows these numbers for building and running
with vanilla go on literal sizes between 16B and 2048B,
showing that vanilla Go isn't affected at all by these sizes:
│ go │
│ sec/op │
Build/16B 118.1m ± ∞ ¹
Build/64B 123.1m ± ∞ ¹
Build/256B 119.7m ± ∞ ¹
Build/1024B 119.1m ± ∞ ¹
Build/2048B 124.3m ± ∞ ¹
Run/16B 1.671m ± ∞ ¹
Run/64B 1.143m ± ∞ ¹
Run/256B 1.190m ± ∞ ¹
Run/1024B 1.222m ± ∞ ¹
Run/2048B 1.080m ± ∞ ¹
Our simple and swap obfuscators scale pretty well to these same sizes,
only causing moderate slow-downs to build and runtime speeds:
│ simple │ swap │
│ sec/op vs base │ sec/op vs base │
Build/16B 268.0m ± ∞ ¹ +126.88% (p=1.000 n=1) 262.0m ± ∞ ¹ +121.80% (p=1.000 n=1)
Build/64B 253.8m ± ∞ ¹ +106.16% (p=1.000 n=1) 252.4m ± ∞ ¹ +105.00% (p=1.000 n=1)
Build/256B 265.4m ± ∞ ¹ +121.78% (p=1.000 n=1) 276.7m ± ∞ ¹ +131.16% (p=1.000 n=1)
Build/1024B 267.4m ± ∞ ¹ +124.44% (p=1.000 n=1) 315.0m ± ∞ ¹ +164.48% (p=1.000 n=1)
Build/2048B 277.4m ± ∞ ¹ +123.11% (p=1.000 n=1) 383.8m ± ∞ ¹ +208.70% (p=1.000 n=1)
Run/16B 1.740m ± ∞ ¹ +4.12% (p=1.000 n=1) 1.463m ± ∞ ¹ -12.47% (p=1.000 n=1)
Run/64B 1.470m ± ∞ ¹ +28.66% (p=1.000 n=1) 1.455m ± ∞ ¹ +27.35% (p=1.000 n=1)
Run/256B 1.729m ± ∞ ¹ +45.25% (p=1.000 n=1) 1.812m ± ∞ ¹ +52.26% (p=1.000 n=1)
Run/1024B 1.315m ± ∞ ¹ +7.62% (p=1.000 n=1) 1.352m ± ∞ ¹ +10.60% (p=1.000 n=1)
Run/2048B 1.425m ± ∞ ¹ +31.93% (p=1.000 n=1) 1.316m ± ∞ ¹ +21.88% (p=1.000 n=1)
However, the other three cause huge slow-downs in both build and runtime speeds:
│ split │ shuffle │ seed │
│ sec/op vs base │ sec/op vs base │ sec/op vs base │
Build/16B 326.6m ± ∞ ¹ +176.53% (p=1.000 n=1) 363.1m ± ∞ ¹ +207.44% (p=1.000 n=1) 217.4m ± ∞ ¹ +84.05% (p=1.000 n=1)
Build/64B 400.6m ± ∞ ¹ +225.34% (p=1.000 n=1) 328.0m ± ∞ ¹ +166.39% (p=1.000 n=1) 262.1m ± ∞ ¹ +112.87% (p=1.000 n=1)
Build/256B 824.7m ± ∞ ¹ +589.08% (p=1.000 n=1) 588.2m ± ∞ ¹ +391.45% (p=1.000 n=1) 873.7m ± ∞ ¹ +630.05% (p=1.000 n=1)
Build/1024B 3257.7m ± ∞ ¹ +2634.84% (p=1.000 n=1) 1671.4m ± ∞ ¹ +1303.15% (p=1.000 n=1) 5000.0m ± ∞ ¹ +4097.53% (p=1.000 n=1)
Build/2048B 5000.0m ± ∞ ¹ +3921.73% (p=1.000 n=1) 3936.4m ± ∞ ¹ +3066.21% (p=1.000 n=1) 5000.0m ± ∞ ¹ +3921.73% (p=1.000 n=1)
Run/16B 1.680m ± ∞ ¹ +0.51% (p=1.000 n=1) 1.426m ± ∞ ¹ -14.67% (p=1.000 n=1) 1.908m ± ∞ ¹ +14.13% (p=1.000 n=1)
Run/64B 1.560m ± ∞ ¹ +36.53% (p=1.000 n=1) 1.345m ± ∞ ¹ +17.74% (p=1.000 n=1) 1.704m ± ∞ ¹ +49.10% (p=1.000 n=1)
Run/256B 2.133m ± ∞ ¹ +79.24% (p=1.000 n=1) 1.833m ± ∞ ¹ +53.98% (p=1.000 n=1) 1.838m ± ∞ ¹ +54.40% (p=1.000 n=1)
Run/1024B 2.863m ± ∞ ¹ +134.21% (p=1.000 n=1) 1.786m ± ∞ ¹ +46.12% (p=1.000 n=1) 5000.000m ± ∞ ¹ +408975.92% (p=1.000 n=1)
Run/2048B 5000.000m ± ∞ ¹ +462837.24% (p=1.000 n=1) 2.900m ± ∞ ¹ +168.54% (p=1.000 n=1) 5000.000m ± ∞ ¹ +462837.24% (p=1.000 n=1)
As such, limit the scope of when we apply our obfuscators in two ways.
First, always apply a limit of 2048 bytes for all obfuscators.
As we can see above, the two cheap obfuscators still add some overhead,
and we aren't testing what happens if they run on truly huge strings.
It's likely that they will still cause unexpected slow-down.
Second, split the obfuscators along this line and call them
"cheap" versus "expensive". The expensive ones are only used
for sizes of up to 256 bytes. We still measure moderate slow-downs
in build times of 400-600%, but this is a reasonable compromise for now.
We will be filing bugs with upstream Go about the compiler overhead.
We adjust generateLiterals accordingly;
it now generates 100 literals between MinSize and MaxSize,
which must be obfuscated in some way, and 5 literals past MaxSize,
which don't need to be obfuscated, to check for issues like crashes.
Fixes #928.
497 lines
13 KiB
Go
497 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/go-quicktest/qt"
|
|
"github.com/rogpeppe/go-internal/goproxytest"
|
|
"github.com/rogpeppe/go-internal/gotooltest"
|
|
"github.com/rogpeppe/go-internal/testscript"
|
|
|
|
ah "mvdan.cc/garble/internal/asthelper"
|
|
"mvdan.cc/garble/internal/literals"
|
|
)
|
|
|
|
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" {
|
|
main()
|
|
return
|
|
}
|
|
testscript.Main(garbleMain{m}, map[string]func(){
|
|
"garble": main,
|
|
})
|
|
}
|
|
|
|
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()
|
|
qt.Assert(t, qt.IsNil(err))
|
|
|
|
tempCacheDir := t.TempDir()
|
|
|
|
hostCacheDir, err := os.UserCacheDir()
|
|
qt.Assert(t, qt.IsNil(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)
|
|
|
|
// gotoolchain.txtar is one test which wants to reuse GOMODCACHE.
|
|
out, err := exec.Command("go", "env", "GOMODCACHE").Output()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
env.Setenv("HOST_GOMODCACHE", strings.TrimSpace(string(out)))
|
|
|
|
// We use our own GOPROXY above, so avoid using sum.golang.org,
|
|
// as we would fail to update any go.sum file in the testscripts.
|
|
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").Output()
|
|
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,
|
|
"setup-go": setupGo,
|
|
},
|
|
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":
|
|
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 {
|
|
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; try inspecting with diffoscope",
|
|
file1.Name(), file2.Name())
|
|
sizeDiff := len(data2) - len(data1)
|
|
ts.Fatalf("%s and %s differ; size diff: %+d",
|
|
args[0], args[1], sizeDiff)
|
|
}
|
|
}
|
|
|
|
var testRand = mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
|
|
|
|
const uniqueLitString = "garble_unique_string"
|
|
|
|
// generateLiterals creates a source file with random string literals appended
|
|
// to a global var in init, preventing the compiler from optimizing them away.
|
|
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
|
|
|
|
// 100 literals up to MaxSize, all containing uniqueLitString.
|
|
for range 100 {
|
|
randSize := testRand.Intn(literals.MaxSize - len(uniqueLitString) + 1)
|
|
buffer := make([]byte, randSize)
|
|
testRand.Read(buffer)
|
|
statements = append(
|
|
statements,
|
|
&ast.AssignStmt{
|
|
Lhs: []ast.Expr{ast.NewIdent("x")},
|
|
Tok: token.ADD_ASSIGN,
|
|
Rhs: []ast.Expr{ah.StringLit(string(buffer) + uniqueLitString)},
|
|
},
|
|
)
|
|
}
|
|
|
|
// 5 huge literals past MaxSize, without uniqueLitString; not obfuscated.
|
|
for range 5 {
|
|
size := literals.MaxSize + 1 + testRand.Intn(128<<10)
|
|
buffer := make([]byte, size)
|
|
testRand.Read(buffer)
|
|
statements = append(
|
|
statements,
|
|
&ast.AssignStmt{
|
|
Lhs: []ast.Expr{ast.NewIdent("x")},
|
|
Tok: token.ADD_ASSIGN,
|
|
Rhs: []ast.Expr{ah.StringLit(string(buffer))},
|
|
},
|
|
)
|
|
}
|
|
|
|
// 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 setupGo(ts *testscript.TestScript, neg bool, args []string) {
|
|
if neg || len(args) != 1 {
|
|
ts.Fatalf("usage: setup-go version")
|
|
}
|
|
// Download the version of Go specified as an argument, cache it in GOMODCACHE,
|
|
// and get its GOROOT directory inside the cache so we can use it.
|
|
cmd := exec.Command("go", "env", "GOROOT")
|
|
cmd.Env = append(cmd.Environ(), "GOTOOLCHAIN="+args[0])
|
|
out, err := cmd.Output()
|
|
ts.Check(err)
|
|
|
|
goroot := strings.TrimSpace(string(out))
|
|
|
|
ts.Setenv("PATH", filepath.Join(goroot, "bin")+string(os.PathListSeparator)+ts.Getenv("PATH"))
|
|
// Remove GOROOT from the environment, as it is unnecessary and gets in the way
|
|
// when we want to test GOTOOLCHAIN upgrades, which will need different GOROOTs.
|
|
ts.Setenv("GOROOT", "")
|
|
}
|
|
|
|
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}
|
|
|
|
qt.Assert(t, qt.DeepEquals(got, test.want))
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
qt.Assert(t, qt.DeepEquals(got, test.want))
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
qt.Assert(t, qt.DeepEquals(got, test.want))
|
|
})
|
|
}
|
|
}
|