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