1// Copyright 2019 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package android 16 17import ( 18 "fmt" 19 "io" 20 "reflect" 21 "runtime" 22 "strings" 23 "testing" 24 25 "github.com/google/blueprint/proptools" 26) 27 28type customModule struct { 29 ModuleBase 30 31 properties struct { 32 Default_dist_files *string 33 Dist_output_file *bool 34 } 35 36 data AndroidMkData 37 distFiles TaggedDistFiles 38 outputFile OptionalPath 39 40 // The paths that will be used as the default dist paths if no tag is 41 // specified. 42 defaultDistPaths Paths 43} 44 45const ( 46 defaultDistFiles_None = "none" 47 defaultDistFiles_Default = "default" 48 defaultDistFiles_Tagged = "tagged" 49) 50 51func (m *customModule) GenerateAndroidBuildActions(ctx ModuleContext) { 52 53 m.base().licenseMetadataFile = PathForOutput(ctx, "meta_lic") 54 55 // If the dist_output_file: true then create an output file that is stored in 56 // the OutputFile property of the AndroidMkEntry. 57 if proptools.BoolDefault(m.properties.Dist_output_file, true) { 58 path := PathForTesting("dist-output-file.out") 59 m.outputFile = OptionalPathForPath(path) 60 61 // Previous code would prioritize the DistFiles property over the OutputFile 62 // property in AndroidMkEntry when determining the default dist paths. 63 // Setting this first allows it to be overridden based on the 64 // default_dist_files setting replicating that previous behavior. 65 m.defaultDistPaths = Paths{path} 66 } 67 68 // Based on the setting of the default_dist_files property possibly create a 69 // TaggedDistFiles structure that will be stored in the DistFiles property of 70 // the AndroidMkEntry. 71 defaultDistFiles := proptools.StringDefault(m.properties.Default_dist_files, defaultDistFiles_Tagged) 72 switch defaultDistFiles { 73 case defaultDistFiles_None: 74 // Do nothing 75 76 case defaultDistFiles_Default: 77 path := PathForTesting("default-dist.out") 78 m.defaultDistPaths = Paths{path} 79 m.distFiles = MakeDefaultDistFiles(path) 80 81 case defaultDistFiles_Tagged: 82 // Module types that set AndroidMkEntry.DistFiles to the result of calling 83 // GenerateTaggedDistFiles(ctx) relied on no tag being treated as "" which 84 // meant that the default dist paths would be whatever was returned by 85 // OutputFiles(""). In order to preserve that behavior when treating no tag 86 // as being equal to DefaultDistTag this ensures that 87 // OutputFiles(DefaultDistTag) will return the same as OutputFiles(""). 88 m.defaultDistPaths = PathsForTesting("one.out") 89 90 // This must be called after setting defaultDistPaths/outputFile as 91 // GenerateTaggedDistFiles calls into OutputFiles(tag) which may use those 92 // fields. 93 m.distFiles = m.GenerateTaggedDistFiles(ctx) 94 } 95} 96 97func (m *customModule) AndroidMk() AndroidMkData { 98 return AndroidMkData{ 99 Custom: func(w io.Writer, name, prefix, moduleDir string, data AndroidMkData) { 100 m.data = data 101 }, 102 } 103} 104 105func (m *customModule) OutputFiles(tag string) (Paths, error) { 106 switch tag { 107 case DefaultDistTag: 108 if m.defaultDistPaths != nil { 109 return m.defaultDistPaths, nil 110 } else { 111 return nil, fmt.Errorf("default dist tag is not available") 112 } 113 case "": 114 return PathsForTesting("one.out"), nil 115 case ".multiple": 116 return PathsForTesting("two.out", "three/four.out"), nil 117 case ".another-tag": 118 return PathsForTesting("another.out"), nil 119 default: 120 return nil, fmt.Errorf("unsupported module reference tag %q", tag) 121 } 122} 123 124func (m *customModule) AndroidMkEntries() []AndroidMkEntries { 125 return []AndroidMkEntries{ 126 { 127 Class: "CUSTOM_MODULE", 128 DistFiles: m.distFiles, 129 OutputFile: m.outputFile, 130 }, 131 } 132} 133 134func customModuleFactory() Module { 135 module := &customModule{} 136 137 module.AddProperties(&module.properties) 138 139 InitAndroidModule(module) 140 return module 141} 142 143// buildContextAndCustomModuleFoo creates a config object, processes the supplied 144// bp module and then returns the config and the custom module called "foo". 145func buildContextAndCustomModuleFoo(t *testing.T, bp string) (*TestContext, *customModule) { 146 t.Helper() 147 result := GroupFixturePreparers( 148 // Enable androidmk Singleton 149 PrepareForTestWithAndroidMk, 150 FixtureRegisterWithContext(func(ctx RegistrationContext) { 151 ctx.RegisterModuleType("custom", customModuleFactory) 152 }), 153 FixtureModifyProductVariables(func(variables FixtureProductVariables) { 154 variables.DeviceProduct = proptools.StringPtr("bar") 155 }), 156 FixtureWithRootAndroidBp(bp), 157 ).RunTest(t) 158 159 module := result.ModuleForTests("foo", "").Module().(*customModule) 160 return result.TestContext, module 161} 162 163func TestAndroidMkSingleton_PassesUpdatedAndroidMkDataToCustomCallback(t *testing.T) { 164 if runtime.GOOS == "darwin" { 165 // Device modules are not exported on Mac, so this test doesn't work. 166 t.SkipNow() 167 } 168 169 bp := ` 170 custom { 171 name: "foo", 172 required: ["bar"], 173 host_required: ["baz"], 174 target_required: ["qux"], 175 } 176 ` 177 178 _, m := buildContextAndCustomModuleFoo(t, bp) 179 180 assertEqual := func(expected interface{}, actual interface{}) { 181 if !reflect.DeepEqual(expected, actual) { 182 t.Errorf("%q expected, but got %q", expected, actual) 183 } 184 } 185 assertEqual([]string{"bar"}, m.data.Required) 186 assertEqual([]string{"baz"}, m.data.Host_required) 187 assertEqual([]string{"qux"}, m.data.Target_required) 188} 189 190func TestGenerateDistContributionsForMake(t *testing.T) { 191 dc := &distContributions{ 192 copiesForGoals: []*copiesForGoals{ 193 { 194 goals: "my_goal", 195 copies: []distCopy{ 196 distCopyForTest("one.out", "one.out"), 197 distCopyForTest("two.out", "other.out"), 198 }, 199 }, 200 }, 201 } 202 203 dc.licenseMetadataFile = PathForTesting("meta_lic") 204 makeOutput := generateDistContributionsForMake(dc) 205 206 assertStringEquals(t, `.PHONY: my_goal 207$(if $(strip $(ALL_TARGETS.one.out.META_LIC)),,$(eval ALL_TARGETS.one.out.META_LIC := meta_lic)) 208$(call dist-for-goals,my_goal,one.out:one.out) 209$(if $(strip $(ALL_TARGETS.two.out.META_LIC)),,$(eval ALL_TARGETS.two.out.META_LIC := meta_lic)) 210$(call dist-for-goals,my_goal,two.out:other.out) 211`, strings.Join(makeOutput, "")) 212} 213 214func TestGetDistForGoals(t *testing.T) { 215 bp := ` 216 custom { 217 name: "foo", 218 dist: { 219 targets: ["my_goal", "my_other_goal"], 220 tag: ".multiple", 221 }, 222 dists: [ 223 { 224 targets: ["my_second_goal"], 225 tag: ".multiple", 226 }, 227 { 228 targets: ["my_third_goal"], 229 dir: "test/dir", 230 }, 231 { 232 targets: ["my_fourth_goal"], 233 suffix: ".suffix", 234 }, 235 { 236 targets: ["my_fifth_goal"], 237 dest: "new-name", 238 }, 239 { 240 targets: ["my_sixth_goal"], 241 dest: "new-name", 242 dir: "some/dir", 243 suffix: ".suffix", 244 }, 245 ], 246 } 247 ` 248 249 expectedAndroidMkLines := []string{ 250 ".PHONY: my_second_goal\n", 251 "$(if $(strip $(ALL_TARGETS.two.out.META_LIC)),,$(eval ALL_TARGETS.two.out.META_LIC := meta_lic))\n", 252 "$(call dist-for-goals,my_second_goal,two.out:two.out)\n", 253 "$(if $(strip $(ALL_TARGETS.three/four.out.META_LIC)),,$(eval ALL_TARGETS.three/four.out.META_LIC := meta_lic))\n", 254 "$(call dist-for-goals,my_second_goal,three/four.out:four.out)\n", 255 ".PHONY: my_third_goal\n", 256 "$(if $(strip $(ALL_TARGETS.one.out.META_LIC)),,$(eval ALL_TARGETS.one.out.META_LIC := meta_lic))\n", 257 "$(call dist-for-goals,my_third_goal,one.out:test/dir/one.out)\n", 258 ".PHONY: my_fourth_goal\n", 259 "$(if $(strip $(ALL_TARGETS.one.out.META_LIC)),,$(eval ALL_TARGETS.one.out.META_LIC := meta_lic))\n", 260 "$(call dist-for-goals,my_fourth_goal,one.out:one.suffix.out)\n", 261 ".PHONY: my_fifth_goal\n", 262 "$(if $(strip $(ALL_TARGETS.one.out.META_LIC)),,$(eval ALL_TARGETS.one.out.META_LIC := meta_lic))\n", 263 "$(call dist-for-goals,my_fifth_goal,one.out:new-name)\n", 264 ".PHONY: my_sixth_goal\n", 265 "$(if $(strip $(ALL_TARGETS.one.out.META_LIC)),,$(eval ALL_TARGETS.one.out.META_LIC := meta_lic))\n", 266 "$(call dist-for-goals,my_sixth_goal,one.out:some/dir/new-name.suffix)\n", 267 ".PHONY: my_goal my_other_goal\n", 268 "$(if $(strip $(ALL_TARGETS.two.out.META_LIC)),,$(eval ALL_TARGETS.two.out.META_LIC := meta_lic))\n", 269 "$(call dist-for-goals,my_goal my_other_goal,two.out:two.out)\n", 270 "$(if $(strip $(ALL_TARGETS.three/four.out.META_LIC)),,$(eval ALL_TARGETS.three/four.out.META_LIC := meta_lic))\n", 271 "$(call dist-for-goals,my_goal my_other_goal,three/four.out:four.out)\n", 272 } 273 274 ctx, module := buildContextAndCustomModuleFoo(t, bp) 275 entries := AndroidMkEntriesForTest(t, ctx, module) 276 if len(entries) != 1 { 277 t.Errorf("Expected a single AndroidMk entry, got %d", len(entries)) 278 } 279 androidMkLines := entries[0].GetDistForGoals(module) 280 281 if len(androidMkLines) != len(expectedAndroidMkLines) { 282 t.Errorf( 283 "Expected %d AndroidMk lines, got %d:\n%v", 284 len(expectedAndroidMkLines), 285 len(androidMkLines), 286 androidMkLines, 287 ) 288 } 289 for idx, line := range androidMkLines { 290 expectedLine := strings.ReplaceAll(expectedAndroidMkLines[idx], "meta_lic", module.base().licenseMetadataFile.String()) 291 if line != expectedLine { 292 t.Errorf( 293 "Expected AndroidMk line to be '%s', got '%s'", 294 expectedLine, 295 line, 296 ) 297 } 298 } 299} 300 301func distCopyForTest(from, to string) distCopy { 302 return distCopy{PathForTesting(from), to} 303} 304 305func TestGetDistContributions(t *testing.T) { 306 compareContributions := func(d1 *distContributions, d2 *distContributions) error { 307 if d1 == nil || d2 == nil { 308 if d1 != d2 { 309 return fmt.Errorf("pointer mismatch, expected both to be nil but they were %p and %p", d1, d2) 310 } else { 311 return nil 312 } 313 } 314 if expected, actual := len(d1.copiesForGoals), len(d2.copiesForGoals); expected != actual { 315 return fmt.Errorf("length mismatch, expected %d found %d", expected, actual) 316 } 317 318 for i, copies1 := range d1.copiesForGoals { 319 copies2 := d2.copiesForGoals[i] 320 if expected, actual := copies1.goals, copies2.goals; expected != actual { 321 return fmt.Errorf("goals mismatch at position %d: expected %q found %q", i, expected, actual) 322 } 323 324 if expected, actual := len(copies1.copies), len(copies2.copies); expected != actual { 325 return fmt.Errorf("length mismatch in copy instructions at position %d, expected %d found %d", i, expected, actual) 326 } 327 328 for j, c1 := range copies1.copies { 329 c2 := copies2.copies[j] 330 if expected, actual := NormalizePathForTesting(c1.from), NormalizePathForTesting(c2.from); expected != actual { 331 return fmt.Errorf("paths mismatch at position %d.%d: expected %q found %q", i, j, expected, actual) 332 } 333 334 if expected, actual := c1.dest, c2.dest; expected != actual { 335 return fmt.Errorf("dest mismatch at position %d.%d: expected %q found %q", i, j, expected, actual) 336 } 337 } 338 } 339 340 return nil 341 } 342 343 formatContributions := func(d *distContributions) string { 344 buf := &strings.Builder{} 345 if d == nil { 346 fmt.Fprint(buf, "nil") 347 } else { 348 for _, copiesForGoals := range d.copiesForGoals { 349 fmt.Fprintf(buf, " Goals: %q {\n", copiesForGoals.goals) 350 for _, c := range copiesForGoals.copies { 351 fmt.Fprintf(buf, " %s -> %s\n", NormalizePathForTesting(c.from), c.dest) 352 } 353 fmt.Fprint(buf, " }\n") 354 } 355 } 356 return buf.String() 357 } 358 359 testHelper := func(t *testing.T, name, bp string, expectedContributions *distContributions) { 360 t.Helper() 361 t.Run(name, func(t *testing.T) { 362 t.Helper() 363 364 ctx, module := buildContextAndCustomModuleFoo(t, bp) 365 entries := AndroidMkEntriesForTest(t, ctx, module) 366 if len(entries) != 1 { 367 t.Errorf("Expected a single AndroidMk entry, got %d", len(entries)) 368 } 369 distContributions := entries[0].getDistContributions(module) 370 371 if err := compareContributions(expectedContributions, distContributions); err != nil { 372 t.Errorf("%s\nExpected Contributions\n%sActualContributions\n%s", 373 err, 374 formatContributions(expectedContributions), 375 formatContributions(distContributions)) 376 } 377 }) 378 } 379 380 testHelper(t, "dist-without-tag", ` 381 custom { 382 name: "foo", 383 dist: { 384 targets: ["my_goal"] 385 } 386 } 387`, 388 &distContributions{ 389 copiesForGoals: []*copiesForGoals{ 390 { 391 goals: "my_goal", 392 copies: []distCopy{ 393 distCopyForTest("one.out", "one.out"), 394 }, 395 }, 396 }, 397 }) 398 399 testHelper(t, "dist-with-tag", ` 400 custom { 401 name: "foo", 402 dist: { 403 targets: ["my_goal"], 404 tag: ".another-tag", 405 } 406 } 407`, 408 &distContributions{ 409 copiesForGoals: []*copiesForGoals{ 410 { 411 goals: "my_goal", 412 copies: []distCopy{ 413 distCopyForTest("another.out", "another.out"), 414 }, 415 }, 416 }, 417 }) 418 419 testHelper(t, "append-artifact-with-product", ` 420 custom { 421 name: "foo", 422 dist: { 423 targets: ["my_goal"], 424 append_artifact_with_product: true, 425 } 426 } 427`, &distContributions{ 428 copiesForGoals: []*copiesForGoals{ 429 { 430 goals: "my_goal", 431 copies: []distCopy{ 432 distCopyForTest("one.out", "one_bar.out"), 433 }, 434 }, 435 }, 436 }) 437 438 testHelper(t, "dists-with-tag", ` 439 custom { 440 name: "foo", 441 dists: [ 442 { 443 targets: ["my_goal"], 444 tag: ".another-tag", 445 }, 446 ], 447 } 448`, 449 &distContributions{ 450 copiesForGoals: []*copiesForGoals{ 451 { 452 goals: "my_goal", 453 copies: []distCopy{ 454 distCopyForTest("another.out", "another.out"), 455 }, 456 }, 457 }, 458 }) 459 460 testHelper(t, "multiple-dists-with-and-without-tag", ` 461 custom { 462 name: "foo", 463 dists: [ 464 { 465 targets: ["my_goal"], 466 }, 467 { 468 targets: ["my_second_goal", "my_third_goal"], 469 }, 470 ], 471 } 472`, 473 &distContributions{ 474 copiesForGoals: []*copiesForGoals{ 475 { 476 goals: "my_goal", 477 copies: []distCopy{ 478 distCopyForTest("one.out", "one.out"), 479 }, 480 }, 481 { 482 goals: "my_second_goal my_third_goal", 483 copies: []distCopy{ 484 distCopyForTest("one.out", "one.out"), 485 }, 486 }, 487 }, 488 }) 489 490 testHelper(t, "dist-plus-dists-without-tags", ` 491 custom { 492 name: "foo", 493 dist: { 494 targets: ["my_goal"], 495 }, 496 dists: [ 497 { 498 targets: ["my_second_goal", "my_third_goal"], 499 }, 500 ], 501 } 502`, 503 &distContributions{ 504 copiesForGoals: []*copiesForGoals{ 505 { 506 goals: "my_second_goal my_third_goal", 507 copies: []distCopy{ 508 distCopyForTest("one.out", "one.out"), 509 }, 510 }, 511 { 512 goals: "my_goal", 513 copies: []distCopy{ 514 distCopyForTest("one.out", "one.out"), 515 }, 516 }, 517 }, 518 }) 519 520 testHelper(t, "dist-plus-dists-with-tags", ` 521 custom { 522 name: "foo", 523 dist: { 524 targets: ["my_goal", "my_other_goal"], 525 tag: ".multiple", 526 }, 527 dists: [ 528 { 529 targets: ["my_second_goal"], 530 tag: ".multiple", 531 }, 532 { 533 targets: ["my_third_goal"], 534 dir: "test/dir", 535 }, 536 { 537 targets: ["my_fourth_goal"], 538 suffix: ".suffix", 539 }, 540 { 541 targets: ["my_fifth_goal"], 542 dest: "new-name", 543 }, 544 { 545 targets: ["my_sixth_goal"], 546 dest: "new-name", 547 dir: "some/dir", 548 suffix: ".suffix", 549 }, 550 ], 551 } 552`, 553 &distContributions{ 554 copiesForGoals: []*copiesForGoals{ 555 { 556 goals: "my_second_goal", 557 copies: []distCopy{ 558 distCopyForTest("two.out", "two.out"), 559 distCopyForTest("three/four.out", "four.out"), 560 }, 561 }, 562 { 563 goals: "my_third_goal", 564 copies: []distCopy{ 565 distCopyForTest("one.out", "test/dir/one.out"), 566 }, 567 }, 568 { 569 goals: "my_fourth_goal", 570 copies: []distCopy{ 571 distCopyForTest("one.out", "one.suffix.out"), 572 }, 573 }, 574 { 575 goals: "my_fifth_goal", 576 copies: []distCopy{ 577 distCopyForTest("one.out", "new-name"), 578 }, 579 }, 580 { 581 goals: "my_sixth_goal", 582 copies: []distCopy{ 583 distCopyForTest("one.out", "some/dir/new-name.suffix"), 584 }, 585 }, 586 { 587 goals: "my_goal my_other_goal", 588 copies: []distCopy{ 589 distCopyForTest("two.out", "two.out"), 590 distCopyForTest("three/four.out", "four.out"), 591 }, 592 }, 593 }, 594 }) 595 596 // The above test the default values of default_dist_files and use_output_file. 597 598 // The following tests explicitly test the different combinations of those settings. 599 testHelper(t, "tagged-dist-files-no-output", ` 600 custom { 601 name: "foo", 602 default_dist_files: "tagged", 603 dist_output_file: false, 604 dists: [ 605 { 606 targets: ["my_goal"], 607 }, 608 { 609 targets: ["my_goal"], 610 tag: ".multiple", 611 }, 612 ], 613 } 614`, &distContributions{ 615 copiesForGoals: []*copiesForGoals{ 616 { 617 goals: "my_goal", 618 copies: []distCopy{ 619 distCopyForTest("one.out", "one.out"), 620 }, 621 }, 622 { 623 goals: "my_goal", 624 copies: []distCopy{ 625 distCopyForTest("two.out", "two.out"), 626 distCopyForTest("three/four.out", "four.out"), 627 }, 628 }, 629 }, 630 }) 631 632 testHelper(t, "default-dist-files-no-output", ` 633 custom { 634 name: "foo", 635 default_dist_files: "default", 636 dist_output_file: false, 637 dists: [ 638 { 639 targets: ["my_goal"], 640 }, 641 { 642 targets: ["my_goal"], 643 tag: ".multiple", 644 }, 645 ], 646 } 647`, &distContributions{ 648 copiesForGoals: []*copiesForGoals{ 649 { 650 goals: "my_goal", 651 copies: []distCopy{ 652 distCopyForTest("default-dist.out", "default-dist.out"), 653 }, 654 }, 655 { 656 goals: "my_goal", 657 copies: []distCopy{ 658 distCopyForTest("two.out", "two.out"), 659 distCopyForTest("three/four.out", "four.out"), 660 }, 661 }, 662 }, 663 }) 664 665 testHelper(t, "no-dist-files-no-output", ` 666 custom { 667 name: "foo", 668 default_dist_files: "none", 669 dist_output_file: false, 670 dists: [ 671 // The following is silently ignored because there is not default file 672 // in either the dist files or the output file. 673 { 674 targets: ["my_goal"], 675 }, 676 { 677 targets: ["my_goal"], 678 tag: ".multiple", 679 }, 680 ], 681 } 682`, &distContributions{ 683 copiesForGoals: []*copiesForGoals{ 684 { 685 goals: "my_goal", 686 copies: []distCopy{ 687 distCopyForTest("two.out", "two.out"), 688 distCopyForTest("three/four.out", "four.out"), 689 }, 690 }, 691 }, 692 }) 693 694 testHelper(t, "tagged-dist-files-default-output", ` 695 custom { 696 name: "foo", 697 default_dist_files: "tagged", 698 dist_output_file: true, 699 dists: [ 700 { 701 targets: ["my_goal"], 702 }, 703 { 704 targets: ["my_goal"], 705 tag: ".multiple", 706 }, 707 ], 708 } 709`, &distContributions{ 710 copiesForGoals: []*copiesForGoals{ 711 { 712 goals: "my_goal", 713 copies: []distCopy{ 714 distCopyForTest("one.out", "one.out"), 715 }, 716 }, 717 { 718 goals: "my_goal", 719 copies: []distCopy{ 720 distCopyForTest("two.out", "two.out"), 721 distCopyForTest("three/four.out", "four.out"), 722 }, 723 }, 724 }, 725 }) 726 727 testHelper(t, "default-dist-files-default-output", ` 728 custom { 729 name: "foo", 730 default_dist_files: "default", 731 dist_output_file: true, 732 dists: [ 733 { 734 targets: ["my_goal"], 735 }, 736 { 737 targets: ["my_goal"], 738 tag: ".multiple", 739 }, 740 ], 741 } 742`, &distContributions{ 743 copiesForGoals: []*copiesForGoals{ 744 { 745 goals: "my_goal", 746 copies: []distCopy{ 747 distCopyForTest("default-dist.out", "default-dist.out"), 748 }, 749 }, 750 { 751 goals: "my_goal", 752 copies: []distCopy{ 753 distCopyForTest("two.out", "two.out"), 754 distCopyForTest("three/four.out", "four.out"), 755 }, 756 }, 757 }, 758 }) 759 760 testHelper(t, "no-dist-files-default-output", ` 761 custom { 762 name: "foo", 763 default_dist_files: "none", 764 dist_output_file: true, 765 dists: [ 766 { 767 targets: ["my_goal"], 768 }, 769 { 770 targets: ["my_goal"], 771 tag: ".multiple", 772 }, 773 ], 774 } 775`, &distContributions{ 776 copiesForGoals: []*copiesForGoals{ 777 { 778 goals: "my_goal", 779 copies: []distCopy{ 780 distCopyForTest("dist-output-file.out", "dist-output-file.out"), 781 }, 782 }, 783 { 784 goals: "my_goal", 785 copies: []distCopy{ 786 distCopyForTest("two.out", "two.out"), 787 distCopyForTest("three/four.out", "four.out"), 788 }, 789 }, 790 }, 791 }) 792} 793