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