forked from burrowers/garble
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main_test.go
498 lines (451 loc) · 12.8 KB
/
main_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
// 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)
}
})
}
}