• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2022 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package main
6
7import (
8	"context"
9	"os"
10	"path/filepath"
11	"runtime"
12	"testing"
13	"time"
14
15	"github.com/stretchr/testify/assert"
16	"github.com/stretchr/testify/mock"
17	"github.com/stretchr/testify/require"
18	"go.skia.org/infra/bazel/go/bazel"
19
20	"go.skia.org/infra/go/exec"
21	"go.skia.org/infra/go/gcs"
22	"go.skia.org/infra/go/gcs/mocks"
23	"go.skia.org/infra/go/gerrit"
24	gerrit_testutils "go.skia.org/infra/go/gerrit/testutils"
25	"go.skia.org/infra/go/git"
26	"go.skia.org/infra/go/git/git_common"
27	git_testutils "go.skia.org/infra/go/git/testutils"
28	"go.skia.org/infra/go/gitiles"
29	gitiles_testutils "go.skia.org/infra/go/gitiles/testutils"
30	"go.skia.org/infra/go/mockhttpclient"
31	"go.skia.org/infra/go/now"
32	"go.skia.org/infra/go/testutils"
33	"go.skia.org/infra/go/util"
34	"go.skia.org/infra/task_driver/go/td"
35	"go.skia.org/infra/task_scheduler/go/types"
36)
37
38func TestRunSteps_PostSubmit_Success(t *testing.T) {
39	// The revision is assigned deterministically by the GitBuilder in test().
40	const (
41		expectedBloatyFileGCSPath       = "2022/01/31/01/693abc06538769c662ca1871d347323b133a5d3c/Build-Debian10-Clang-x86_64-Release/dm.tsv"
42		expectedBloatyDiffFileGCSPath   = "2022/01/31/01/693abc06538769c662ca1871d347323b133a5d3c/Build-Debian10-Clang-x86_64-Release/dm.diff.txt"
43		expectedJSONMetadataFileGCSPath = "2022/01/31/01/693abc06538769c662ca1871d347323b133a5d3c/Build-Debian10-Clang-x86_64-Release/dm.json"
44
45		expectedPerfFileGCSPath = "nano-json-v1/2022/01/31/01/693abc06538769c662ca1871d347323b133a5d3c/Build-Debian10-Clang-x86_64-Release/codesize_CkPp9ElAaEXyYWNHpXHU.json"
46	)
47
48	// The revision and author are assigned deterministically by the GitBuilder in test().
49	const expectedJSONMetadataFileContents = `{
50  "version": 1,
51  "timestamp": "2022-01-31T01:02:03Z",
52  "swarming_task_id": "58dccb0d6a3f0411",
53  "swarming_server": "https://chromium-swarm.appspot.com",
54  "task_id": "CkPp9ElAaEXyYWNHpXHU",
55  "task_name": "CodeSize-dm-Debian10-Clang-x86_64-Release",
56  "compile_task_name": "Build-Debian10-Clang-x86_64-Release",
57  "compile_task_name_no_patch": "Build-Debian10-Clang-x86_64-Release-NoPatch",
58  "binary_name": "dm",
59  "bloaty_cipd_version": "1",
60  "bloaty_args": [
61    "build/dm_stripped",
62    "-d",
63    "compileunits,symbols",
64    "-n",
65    "0",
66    "--tsv",
67    "--debug-file=build/dm"
68  ],
69  "bloaty_diff_args": [
70    "build/dm_stripped",
71    "--debug-file=build/dm",
72    "-d",
73    "symbols",
74    "-n",
75    "0",
76    "-s",
77    "file",
78    "--",
79    "build_nopatch/dm_stripped",
80    "--debug-file=build_nopatch/dm"
81  ],
82  "patch_issue": "",
83  "patch_server": "",
84  "patch_set": "",
85  "repo": "https://skia.googlesource.com/skia.git",
86  "revision": "693abc06538769c662ca1871d347323b133a5d3c",
87  "commit_timestamp": "2022-01-30T23:59:00Z",
88  "author": "test (test@google.com)",
89  "subject": "Fake commit subject"
90}`
91	const expectedPerfContents = `{
92  "version": 1,
93  "git_hash": "693abc06538769c662ca1871d347323b133a5d3c",
94  "key": {
95    "binary": "dm",
96    "compile_task_name": "Build-Debian10-Clang-x86_64-Release"
97  },
98  "results": [
99    {
100      "key": {
101        "measurement": "stripped_binary_bytes"
102      },
103      "measurement": 17
104    },
105    {
106      "key": {
107        "measurement": "stripped_diff_bytes"
108      },
109      "measurement": -6
110    }
111  ],
112  "links": {
113    "full_data": "https://task-driver.skia.org/td/CkPp9ElAaEXyYWNHpXHU"
114  }
115}`
116
117	const expectedBloatyFileContents = "I'm a fake Bloaty output!"
118	const expectedBloatyDiffFileContents = "Fake Bloaty diff output over here!"
119
120	// Make sure we use UTC instead of the system timezone.
121	fakeNow := time.Date(2022, time.January, 31, 2, 2, 3, 0, time.FixedZone("UTC+1", 60*60))
122
123	repoState := types.RepoState{
124		Repo: "https://skia.googlesource.com/skia.git",
125	}
126	mockGerrit, mockGitiles, repoState := setupMockGit(t, repoState)
127
128	commandCollector := exec.CommandCollector{}
129	// Mock "bloaty" invocations to output the appropriate contents to the fake stdout.
130	commandCollector.SetDelegateRun(func(ctx context.Context, cmd *exec.Command) error {
131		if filepath.Base(cmd.Name) == "bloaty" {
132			// This argument indicates it's a binary diff invocation, see
133			// https://github.com/google/bloaty/blob/f01ea59bdda11708d74a3826c23d6e2db6c996f0/doc/using.md#size-diffs.
134			if util.In("--", cmd.Args) {
135				_, _ = cmd.CombinedOutput.Write([]byte(expectedBloatyDiffFileContents))
136			} else {
137				_, _ = cmd.CombinedOutput.Write([]byte(expectedBloatyFileContents))
138			}
139			return nil
140		}
141		// "ls" and any other commands directly executed by the task driver produce no mock outputs.
142		return nil
143	})
144
145	mockCodeSizeGCS := mockGCSClient(codesizeGCSBucketName)
146	expectUpload(t, mockCodeSizeGCS, expectedBloatyFileGCSPath, expectedBloatyFileContents)
147	expectUpload(t, mockCodeSizeGCS, expectedBloatyDiffFileGCSPath, expectedBloatyDiffFileContents)
148	expectUpload(t, mockCodeSizeGCS, expectedJSONMetadataFileGCSPath, expectedJSONMetadataFileContents)
149
150	mockPerfGCS := mockGCSClient(perfGCSBucketName)
151	expectUpload(t, mockPerfGCS, expectedPerfFileGCSPath, expectedPerfContents)
152
153	// Realistic but arbitrary arguments.
154	args := runStepsArgs{
155		repoState:              repoState,
156		gerrit:                 mockGerrit.Gerrit,
157		gitilesRepo:            mockGitiles,
158		codesizeGCS:            mockCodeSizeGCS,
159		perfGCS:                mockPerfGCS,
160		swarmingTaskID:         "58dccb0d6a3f0411",
161		swarmingServer:         "https://chromium-swarm.appspot.com",
162		taskID:                 "CkPp9ElAaEXyYWNHpXHU",
163		taskName:               "CodeSize-dm-Debian10-Clang-x86_64-Release",
164		compileTaskName:        "Build-Debian10-Clang-x86_64-Release",
165		compileTaskNameNoPatch: "Build-Debian10-Clang-x86_64-Release-NoPatch",
166		binaryName:             "dm",
167		bloatyCIPDVersion:      "1",
168		bloatyPath:             "/path/to/bloaty",
169		stripPath:              "/path/to/strip",
170	}
171
172	res := td.RunTestSteps(t, false, func(ctx context.Context) error {
173		ctx = now.TimeTravelingContext(fakeNow).WithContext(ctx)
174		ctx = td.WithExecRunFn(ctx, commandCollector.Run)
175		// Be in a temporary directory. We must restore the working directory after this test case
176		// finishes, or subsequent test cases that call os.Getwd() might fail with "getwd: no such file
177		// or directory".
178		origWd, err := os.Getwd()
179		require.NoError(t, err)
180		defer func() { _ = os.Chdir(origWd) }()
181		require.NoError(t, os.Chdir(t.TempDir()))
182
183		// Create a file to simulate the result of copying and stripping the binary
184		createTestFile(t, filepath.Join("build", "dm_stripped"), "This has 17 bytes")
185		createTestFile(t, filepath.Join("build_nopatch", "dm_stripped"), "This has 23 bytes total")
186
187		err = runSteps(ctx, args)
188		assert.NoError(t, err)
189		return err
190	})
191	require.Empty(t, res.Errors)
192	require.Empty(t, res.Exceptions)
193
194	// Filter out all Git commands.
195	var commands []*exec.Command
196	for _, c := range commandCollector.Commands() {
197		if filepath.Base(c.Name) != "git" {
198			commands = append(commands, c)
199		}
200	}
201
202	// We expect the following sequence of commands: "cp", "strip", "ls", "bloaty",
203	//                                               "cp", "strip", "ls", "bloaty".
204	require.Len(t, commands, 8)
205
206	// We copy the binary and strip the debug symbols from the copy.
207	assertCommandEqual(t, commands[0], "cp", "build/dm", "build/dm_stripped")
208	assertCommandEqual(t, commands[1], "/path/to/strip", "build/dm_stripped")
209	// listing the contents of the directory with the binaries is useful for debugging.
210	assertCommandEqual(t, commands[2], "ls", "-al", "build")
211
212	// Assert that Bloaty was invoked on the binary with the right arguments
213	assertCommandEqual(t, commands[3], "/path/to/bloaty",
214		"build/dm_stripped", "-d", "compileunits,symbols", "-n", "0", "--tsv", "--debug-file=build/dm")
215
216	assertCommandEqual(t, commands[4], "cp", "build_nopatch/dm", "build_nopatch/dm_stripped")
217	assertCommandEqual(t, commands[5], "/path/to/strip", "build_nopatch/dm_stripped")
218	// Assert that "ls build_nopatch" was executed to list the contents of the directory with the
219	// binaries built by the compile task at tip-of-tree, for debugging purposes.
220	assertCommandEqual(t, commands[6], "ls", "-al", "build_nopatch")
221	// We perform a diff between the two binaries (the -- is how bloaty does that).
222	assertCommandEqual(t, commands[7], "/path/to/bloaty",
223		"build/dm_stripped", "--debug-file=build/dm",
224		"-d", "symbols", "-n", "0", "-s", "file",
225		"--", "build_nopatch/dm_stripped", "--debug-file=build_nopatch/dm")
226
227	// Assert that the .json and .tsv files were uploaded to GCS.
228	mockCodeSizeGCS.AssertExpectations(t)
229}
230
231func createTestFile(t *testing.T, path, contents string) {
232	require.NoError(t, os.MkdirAll(filepath.Dir(path), 0755))
233	require.NoError(t, os.WriteFile(path, []byte(contents), 0644))
234}
235
236func TestRunSteps_Tryjob_Success(t *testing.T) {
237	const (
238		expectedBloatyFileGCSPath       = "2022/01/31/01/tryjob/12345/3/CkPp9ElAaEXyYWNHpXHU/Build-Debian10-Clang-x86_64-Release/dm.tsv"
239		expectedBloatyDiffFileGCSPath   = "2022/01/31/01/tryjob/12345/3/CkPp9ElAaEXyYWNHpXHU/Build-Debian10-Clang-x86_64-Release/dm.diff.txt"
240		expectedJSONMetadataFileGCSPath = "2022/01/31/01/tryjob/12345/3/CkPp9ElAaEXyYWNHpXHU/Build-Debian10-Clang-x86_64-Release/dm.json"
241	)
242
243	// The revision and author are assigned deterministically by the GitBuilder in test().
244	const expectedJSONMetadataFileContents = `{
245  "version": 1,
246  "timestamp": "2022-01-31T01:02:03Z",
247  "swarming_task_id": "58dccb0d6a3f0411",
248  "swarming_server": "https://chromium-swarm.appspot.com",
249  "task_id": "CkPp9ElAaEXyYWNHpXHU",
250  "task_name": "CodeSize-dm-Debian10-Clang-x86_64-Release",
251  "compile_task_name": "Build-Debian10-Clang-x86_64-Release",
252  "compile_task_name_no_patch": "Build-Debian10-Clang-x86_64-Release-NoPatch",
253  "binary_name": "dm",
254  "bloaty_cipd_version": "1",
255  "bloaty_args": [
256    "build/dm_stripped",
257    "-d",
258    "compileunits,symbols",
259    "-n",
260    "0",
261    "--tsv",
262    "--debug-file=build/dm"
263  ],
264  "bloaty_diff_args": [
265    "build/dm_stripped",
266    "--debug-file=build/dm",
267    "-d",
268    "symbols",
269    "-n",
270    "0",
271    "-s",
272    "file",
273    "--",
274    "build_nopatch/dm_stripped",
275    "--debug-file=build_nopatch/dm"
276  ],
277  "patch_issue": "12345",
278  "patch_server": "https://skia-review.googlesource.com",
279  "patch_set": "3",
280  "repo": "https://skia.googlesource.com/skia.git",
281  "revision": "693abc06538769c662ca1871d347323b133a5d3c",
282  "commit_timestamp": "2022-01-30T23:59:00Z",
283  "author": "test (test@google.com)",
284  "subject": "Fake commit subject"
285}`
286	const expectedBloatyFileContents = "I'm a fake Bloaty output!"
287	const expectedBloatyDiffFileContents = "Fake Bloaty diff output over here!"
288
289	// Make sure we use UTC instead of the system timezone.
290	fakeNow := time.Date(2022, time.January, 31, 2, 2, 3, 0, time.FixedZone("UTC+1", 60*60))
291
292	repoState := types.RepoState{
293		Patch: types.Patch{
294			Issue:     "12345",
295			PatchRepo: "https://skia.googlesource.com/skia.git",
296			Patchset:  "3",
297			Server:    "https://skia-review.googlesource.com",
298		},
299		Repo: "https://skia.googlesource.com/skia.git",
300	}
301	mockGerrit, mockGitiles, repoState := setupMockGit(t, repoState)
302
303	// Mock "bloaty" invocations.
304	commandCollector := exec.CommandCollector{}
305	commandCollector.SetDelegateRun(func(ctx context.Context, cmd *exec.Command) error {
306		if filepath.Base(cmd.Name) == "bloaty" {
307			// This argument indicates it's a binary diff invocation, see
308			// https://github.com/google/bloaty/blob/f01ea59bdda11708d74a3826c23d6e2db6c996f0/doc/using.md#size-diffs.
309			if util.In("--", cmd.Args) {
310				_, _ = cmd.CombinedOutput.Write([]byte(expectedBloatyDiffFileContents))
311			} else {
312				_, _ = cmd.CombinedOutput.Write([]byte(expectedBloatyFileContents))
313			}
314			return nil
315		}
316		// "ls" and any other commands directly executed by the task driver produce no mock outputs.
317		return nil
318	})
319
320	mockCodeSizeGCS := mockGCSClient(codesizeGCSBucketName)
321	expectUpload(t, mockCodeSizeGCS, expectedBloatyFileGCSPath, expectedBloatyFileContents)
322	expectUpload(t, mockCodeSizeGCS, expectedBloatyDiffFileGCSPath, expectedBloatyDiffFileContents)
323	expectUpload(t, mockCodeSizeGCS, expectedJSONMetadataFileGCSPath, expectedJSONMetadataFileContents)
324
325	// Realistic but arbitrary arguments.
326	args := runStepsArgs{
327		repoState:              repoState,
328		gerrit:                 mockGerrit.Gerrit,
329		gitilesRepo:            mockGitiles,
330		codesizeGCS:            mockCodeSizeGCS,
331		swarmingTaskID:         "58dccb0d6a3f0411",
332		swarmingServer:         "https://chromium-swarm.appspot.com",
333		taskID:                 "CkPp9ElAaEXyYWNHpXHU",
334		taskName:               "CodeSize-dm-Debian10-Clang-x86_64-Release",
335		compileTaskName:        "Build-Debian10-Clang-x86_64-Release",
336		compileTaskNameNoPatch: "Build-Debian10-Clang-x86_64-Release-NoPatch",
337		binaryName:             "dm",
338		bloatyCIPDVersion:      "1",
339		bloatyPath:             "/path/to/bloaty",
340		stripPath:              "/path/to/strip",
341	}
342
343	res := td.RunTestSteps(t, false, func(ctx context.Context) error {
344		ctx = now.TimeTravelingContext(fakeNow).WithContext(ctx)
345		ctx = td.WithExecRunFn(ctx, commandCollector.Run)
346		// Be in a temporary directory. We must restore the working directory after this test case
347		// finishes, or subsequent test cases that call os.Getwd() might fail with "getwd: no such file
348		// or directory".
349		origWd, err := os.Getwd()
350		require.NoError(t, err)
351		defer func() { _ = os.Chdir(origWd) }()
352		require.NoError(t, os.Chdir(t.TempDir()))
353
354		// Create a file to simulate the result of copying and stripping the binary
355		createTestFile(t, filepath.Join("build", "dm_stripped"), "This has 17 bytes")
356		createTestFile(t, filepath.Join("build_nopatch", "dm_stripped"), "This has 23 bytes total")
357
358		err = runSteps(ctx, args)
359		assert.NoError(t, err)
360		return err
361	})
362	require.Empty(t, res.Errors)
363	require.Empty(t, res.Exceptions)
364
365	// Filter out all Git commands.
366	var commands []*exec.Command
367	for _, c := range commandCollector.Commands() {
368		if filepath.Base(c.Name) != "git" {
369			commands = append(commands, c)
370		}
371	}
372
373	// We expect the following sequence of commands: "cp", "strip", "ls", "bloaty",
374	//                                               "cp", "strip", "ls", "bloaty".
375	require.Len(t, commands, 8)
376
377	// We copy the binary and strip the debug symbols from the copy.
378	assertCommandEqual(t, commands[0], "cp", "build/dm", "build/dm_stripped")
379	assertCommandEqual(t, commands[1], "/path/to/strip", "build/dm_stripped")
380	// listing the contents of the directory with the binaries is useful for debugging.
381	assertCommandEqual(t, commands[2], "ls", "-al", "build")
382
383	// Assert that Bloaty was invoked on the binary with the right arguments
384	assertCommandEqual(t, commands[3], "/path/to/bloaty",
385		"build/dm_stripped", "-d", "compileunits,symbols", "-n", "0", "--tsv", "--debug-file=build/dm")
386
387	assertCommandEqual(t, commands[4], "cp", "build_nopatch/dm", "build_nopatch/dm_stripped")
388	assertCommandEqual(t, commands[5], "/path/to/strip", "build_nopatch/dm_stripped")
389	// Assert that "ls build_nopatch" was executed to list the contents of the directory with the
390	// binaries built by the compile task at tip-of-tree, for debugging purposes.
391	assertCommandEqual(t, commands[6], "ls", "-al", "build_nopatch")
392	// We perform a diff between the two binaries (the -- is how bloaty does that).
393	assertCommandEqual(t, commands[7], "/path/to/bloaty",
394		"build/dm_stripped", "--debug-file=build/dm",
395		"-d", "symbols", "-n", "0", "-s", "file",
396		"--", "build_nopatch/dm_stripped", "--debug-file=build_nopatch/dm")
397
398	// Assert that the .json, .tsv and .diff.txt files were uploaded to GCS.
399	mockCodeSizeGCS.AssertExpectations(t)
400}
401
402func mockGCSClient(name string) *mocks.GCSClient {
403	m := &mocks.GCSClient{}
404	m.On("Bucket").Return(name).Maybe()
405	return m
406}
407
408func expectUpload(t *testing.T, client *mocks.GCSClient, path, contents string) {
409	client.On("SetFileContents", testutils.AnyContext, path, gcs.FILE_WRITE_OPTS_TEXT, mock.Anything).Run(func(args mock.Arguments) {
410		fileContents := string(args.Get(3).([]byte))
411		assert.Equal(t, contents, fileContents)
412	}).Return(nil)
413}
414
415func assertCommandEqual(t *testing.T, actualCmd *exec.Command, expectedCmd string, expectedArgs ...string) {
416	assert.Equal(t, expectedCmd, actualCmd.Name)
417	assert.Equal(t, expectedArgs, actualCmd.Args)
418}
419
420func setupMockGit(t *testing.T, repoState types.RepoState) (*gerrit_testutils.MockGerrit, *gitiles.Repo, types.RepoState) {
421	commitTimestamp := time.Date(2022, time.January, 30, 23, 59, 0, 0, time.UTC)
422
423	// Seed a fake Git repository.
424	ctx := context.Background()
425	if bazel.InBazelTest() && runtime.GOOS == "linux" {
426		ctx = git_common.WithGitFinder(context.Background(), func() (string, error) {
427			return filepath.Join(bazel.RunfilesDir(), "external", "git_linux_amd64", "bin", "git"), nil
428		})
429	}
430	gitBuilder := git_testutils.GitInit(t, ctx)
431	t.Cleanup(func() {
432		gitBuilder.Cleanup()
433	})
434	gitBuilder.Add(ctx, "README.md", "I'm a fake repository.")
435	repoState.Revision = gitBuilder.CommitMsgAt(ctx, "Fake commit subject", commitTimestamp)
436
437	// Mock a Gerrit client.
438	mockGerrit := gerrit_testutils.NewGerrit(t)
439	mockGerrit.MockGetIssueProperties(&gerrit.ChangeInfo{
440		Issue: 12345,
441		Owner: &gerrit.Person{
442			Name:  "test",
443			Email: "test@google.com",
444		},
445		Subject: "Fake commit subject",
446		// We ignore the patchset commit hashes, their values do not matter.
447		Revisions: map[string]*gerrit.Revision{
448			"commit hash for patchset 1": {
449				Number:        1,
450				CreatedString: commitTimestamp.Add(-2 * time.Hour).Format(time.RFC3339),
451			},
452			"commit hash for patchset 2": {
453				Number:        2,
454				CreatedString: commitTimestamp.Add(-time.Hour).Format(time.RFC3339),
455			},
456			"commit hash for patchset 3": {
457				Number:        3,
458				CreatedString: commitTimestamp.Format(time.RFC3339),
459			},
460			"commit hash for patchset 4": {
461				Number:        4,
462				CreatedString: commitTimestamp.Add(time.Hour).Format(time.RFC3339),
463			},
464		},
465	})
466
467	// Mock a Gitiles client.
468	urlMock := mockhttpclient.NewURLMock()
469	mockRepo := gitiles_testutils.NewMockRepo(t, gitBuilder.RepoUrl(), git.CheckoutDir(gitBuilder.Dir()), urlMock)
470	mockRepo.MockGetCommit(ctx, repoState.Revision)
471	mockGitiles := gitiles.NewRepo(gitBuilder.RepoUrl(), urlMock.Client())
472	return mockGerrit, mockGitiles, repoState
473}
474
475func TestParseBloatyDiffOutput(t *testing.T) {
476	tests := []struct {
477		desc             string
478		bloatyDiff       string
479		expectedVMDiff   string
480		expectedFileDiff string
481	}{
482		{
483			desc:             "empty diff",
484			bloatyDiff:       "",
485			expectedVMDiff:   "",
486			expectedFileDiff: "",
487		},
488		{
489			desc:             "well-formed diff",
490			bloatyDiff:       "test\n\test\n+0.0% +832 TOTAL +848 +0.0%\n\n",
491			expectedVMDiff:   "+832",
492			expectedFileDiff: "+848",
493		},
494		{
495			desc:             "malformed diff",
496			bloatyDiff:       "test\n\test\ntest\n",
497			expectedVMDiff:   "",
498			expectedFileDiff: "",
499		},
500	}
501
502	for _, test := range tests {
503		actualVMDiff, actualFileDiff := parseBloatyDiffOutput(test.bloatyDiff)
504		assert.Equal(t, test.expectedVMDiff, actualVMDiff, test.desc)
505		assert.Equal(t, test.expectedFileDiff, actualFileDiff, test.desc)
506	}
507}
508