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