| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2013 the V8 project authors. All rights reserved. | 2 # Copyright 2013 the V8 project authors. All rights reserved. |
| 3 # Redistribution and use in source and binary forms, with or without | 3 # Redistribution and use in source and binary forms, with or without |
| 4 # modification, are permitted provided that the following conditions are | 4 # modification, are permitted provided that the following conditions are |
| 5 # met: | 5 # met: |
| 6 # | 6 # |
| 7 # * Redistributions of source code must retain the above copyright | 7 # * Redistributions of source code must retain the above copyright |
| 8 # notice, this list of conditions and the following disclaimer. | 8 # notice, this list of conditions and the following disclaimer. |
| 9 # * Redistributions in binary form must reproduce the above | 9 # * Redistributions in binary form must reproduce the above |
| 10 # copyright notice, this list of conditions and the following | 10 # copyright notice, this list of conditions and the following |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 46 VERSION_FILE: None, | 46 VERSION_FILE: None, |
| 47 CHANGELOG_FILE: None, | 47 CHANGELOG_FILE: None, |
| 48 CHANGELOG_ENTRY_FILE: "/tmp/test-v8-push-to-trunk-tempfile-changelog-entry", | 48 CHANGELOG_ENTRY_FILE: "/tmp/test-v8-push-to-trunk-tempfile-changelog-entry", |
| 49 PATCH_FILE: "/tmp/test-v8-push-to-trunk-tempfile-patch", | 49 PATCH_FILE: "/tmp/test-v8-push-to-trunk-tempfile-patch", |
| 50 COMMITMSG_FILE: "/tmp/test-v8-push-to-trunk-tempfile-commitmsg", | 50 COMMITMSG_FILE: "/tmp/test-v8-push-to-trunk-tempfile-commitmsg", |
| 51 CHROMIUM: "/tmp/test-v8-push-to-trunk-tempfile-chromium", | 51 CHROMIUM: "/tmp/test-v8-push-to-trunk-tempfile-chromium", |
| 52 DEPS_FILE: "/tmp/test-v8-push-to-trunk-tempfile-chromium/DEPS", | 52 DEPS_FILE: "/tmp/test-v8-push-to-trunk-tempfile-chromium/DEPS", |
| 53 } | 53 } |
| 54 | 54 |
| 55 | 55 |
| 56 def MakeOptions(s=0, l=None, f=False, m=True, r=None, c=None): |
| 57 """Convenience wrapper.""" |
| 58 class Options(object): |
| 59 pass |
| 60 options = Options() |
| 61 options.s = s |
| 62 options.l = l |
| 63 options.f = f |
| 64 options.m = m |
| 65 options.r = r |
| 66 options.c = c |
| 67 return options |
| 68 |
| 69 |
| 56 class ToplevelTest(unittest.TestCase): | 70 class ToplevelTest(unittest.TestCase): |
| 57 def testMakeComment(self): | 71 def testMakeComment(self): |
| 58 self.assertEquals("# Line 1\n# Line 2\n#", | 72 self.assertEquals("# Line 1\n# Line 2\n#", |
| 59 MakeComment(" Line 1\n Line 2\n")) | 73 MakeComment(" Line 1\n Line 2\n")) |
| 60 self.assertEquals("#Line 1\n#Line 2", | 74 self.assertEquals("#Line 1\n#Line 2", |
| 61 MakeComment("Line 1\n Line 2")) | 75 MakeComment("Line 1\n Line 2")) |
| 62 | 76 |
| 63 def testStripComments(self): | 77 def testStripComments(self): |
| 64 self.assertEquals(" Line 1\n Line 3\n", | 78 self.assertEquals(" Line 1\n Line 3\n", |
| 65 StripComments(" Line 1\n# Line 2\n Line 3\n#\n")) | 79 StripComments(" Line 1\n# Line 2\n Line 3\n#\n")) |
| (...skipping 187 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 253 f.write(" // Some line...\n") | 267 f.write(" // Some line...\n") |
| 254 f.write("\n") | 268 f.write("\n") |
| 255 f.write("#define MAJOR_VERSION 3\n") | 269 f.write("#define MAJOR_VERSION 3\n") |
| 256 f.write("#define MINOR_VERSION 22\n") | 270 f.write("#define MINOR_VERSION 22\n") |
| 257 f.write("#define BUILD_NUMBER 5\n") | 271 f.write("#define BUILD_NUMBER 5\n") |
| 258 f.write("#define PATCH_LEVEL 0\n") | 272 f.write("#define PATCH_LEVEL 0\n") |
| 259 f.write(" // Some line...\n") | 273 f.write(" // Some line...\n") |
| 260 f.write("#define IS_CANDIDATE_VERSION 0\n") | 274 f.write("#define IS_CANDIDATE_VERSION 0\n") |
| 261 return name | 275 return name |
| 262 | 276 |
| 263 def MakeStep(self, step_class=Step, state=None): | 277 def MakeStep(self, step_class=Step, state=None, options=None): |
| 264 """Convenience wrapper.""" | 278 """Convenience wrapper.""" |
| 279 options = options or MakeOptions() |
| 265 return MakeStep(step_class=step_class, number=0, state=state, | 280 return MakeStep(step_class=step_class, number=0, state=state, |
| 266 config=TEST_CONFIG, options=None, side_effect_handler=self) | 281 config=TEST_CONFIG, options=options, |
| 282 side_effect_handler=self) |
| 267 | 283 |
| 268 def GitMock(self, cmd, args="", pipe=True): | 284 def GitMock(self, cmd, args="", pipe=True): |
| 285 print "%s %s" % (cmd, args) |
| 269 return self._git_mock.Call(args) | 286 return self._git_mock.Call(args) |
| 270 | 287 |
| 271 def LogMock(self, cmd, args=""): | 288 def LogMock(self, cmd, args=""): |
| 272 print "Log: %s %s" % (cmd, args) | 289 print "Log: %s %s" % (cmd, args) |
| 273 | 290 |
| 274 MOCKS = { | 291 MOCKS = { |
| 275 "git": GitMock, | 292 "git": GitMock, |
| 276 "vi": LogMock, | 293 "vi": LogMock, |
| 277 } | 294 } |
| 278 | 295 |
| (...skipping 269 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 548 | 565 |
| 549 msg = FileToText(TEST_CONFIG[COMMITMSG_FILE]) | 566 msg = FileToText(TEST_CONFIG[COMMITMSG_FILE]) |
| 550 self.assertTrue(re.search(r"Version 3\.22\.5", msg)) | 567 self.assertTrue(re.search(r"Version 3\.22\.5", msg)) |
| 551 self.assertTrue(re.search(r"Performance and stability", msg)) | 568 self.assertTrue(re.search(r"Performance and stability", msg)) |
| 552 self.assertTrue(re.search(r"Log text 1\. Chromium issue 12345", msg)) | 569 self.assertTrue(re.search(r"Log text 1\. Chromium issue 12345", msg)) |
| 553 self.assertFalse(re.search(r"\d+\-\d+\-\d+", msg)) | 570 self.assertFalse(re.search(r"\d+\-\d+\-\d+", msg)) |
| 554 | 571 |
| 555 patch = FileToText(TEST_CONFIG[ PATCH_FILE]) | 572 patch = FileToText(TEST_CONFIG[ PATCH_FILE]) |
| 556 self.assertTrue(re.search(r"patch content", patch)) | 573 self.assertTrue(re.search(r"patch content", patch)) |
| 557 | 574 |
| 558 def _PushToTrunk(self, force=False): | 575 def _PushToTrunk(self, force=False, manual=False): |
| 559 TEST_CONFIG[DOT_GIT_LOCATION] = self.MakeEmptyTempFile() | 576 TEST_CONFIG[DOT_GIT_LOCATION] = self.MakeEmptyTempFile() |
| 560 TEST_CONFIG[VERSION_FILE] = self.MakeTempVersionFile() | 577 TEST_CONFIG[VERSION_FILE] = self.MakeTempVersionFile() |
| 561 TEST_CONFIG[CHANGELOG_ENTRY_FILE] = self.MakeEmptyTempFile() | 578 TEST_CONFIG[CHANGELOG_ENTRY_FILE] = self.MakeEmptyTempFile() |
| 562 TEST_CONFIG[CHANGELOG_FILE] = self.MakeEmptyTempFile() | 579 TEST_CONFIG[CHANGELOG_FILE] = self.MakeEmptyTempFile() |
| 563 if not os.path.exists(TEST_CONFIG[CHROMIUM]): | 580 if not os.path.exists(TEST_CONFIG[CHROMIUM]): |
| 564 os.makedirs(TEST_CONFIG[CHROMIUM]) | 581 os.makedirs(TEST_CONFIG[CHROMIUM]) |
| 565 TextToFile("1999-04-05: Version 3.22.4", TEST_CONFIG[CHANGELOG_FILE]) | 582 TextToFile("1999-04-05: Version 3.22.4", TEST_CONFIG[CHANGELOG_FILE]) |
| 566 TextToFile("Some line\n \"v8_revision\": \"123444\",\n some line", | 583 TextToFile("Some line\n \"v8_revision\": \"123444\",\n some line", |
| 567 TEST_CONFIG[DEPS_FILE]) | 584 TEST_CONFIG[DEPS_FILE]) |
| 568 os.environ["EDITOR"] = "vi" | 585 os.environ["EDITOR"] = "vi" |
| (...skipping 17 matching lines...) Expand all Loading... |
| 586 commit = FileToText(TEST_CONFIG[COMMITMSG_FILE]) | 603 commit = FileToText(TEST_CONFIG[COMMITMSG_FILE]) |
| 587 self.assertTrue(re.search(r"Version 3.22.5", commit)) | 604 self.assertTrue(re.search(r"Version 3.22.5", commit)) |
| 588 self.assertTrue(re.search(r"Log text 1 \(issue 321\).", commit)) | 605 self.assertTrue(re.search(r"Log text 1 \(issue 321\).", commit)) |
| 589 version = FileToText(TEST_CONFIG[VERSION_FILE]) | 606 version = FileToText(TEST_CONFIG[VERSION_FILE]) |
| 590 self.assertTrue(re.search(r"#define MINOR_VERSION\s+22", version)) | 607 self.assertTrue(re.search(r"#define MINOR_VERSION\s+22", version)) |
| 591 self.assertTrue(re.search(r"#define BUILD_NUMBER\s+5", version)) | 608 self.assertTrue(re.search(r"#define BUILD_NUMBER\s+5", version)) |
| 592 self.assertFalse(re.search(r"#define BUILD_NUMBER\s+6", version)) | 609 self.assertFalse(re.search(r"#define BUILD_NUMBER\s+6", version)) |
| 593 self.assertTrue(re.search(r"#define PATCH_LEVEL\s+0", version)) | 610 self.assertTrue(re.search(r"#define PATCH_LEVEL\s+0", version)) |
| 594 self.assertTrue(re.search(r"#define IS_CANDIDATE_VERSION\s+0", version)) | 611 self.assertTrue(re.search(r"#define IS_CANDIDATE_VERSION\s+0", version)) |
| 595 | 612 |
| 596 force_flag = " -f" if force else "" | 613 force_flag = " -f" if not manual else "" |
| 614 review_suffix = "\n\nTBR=reviewer@chromium.org" if not manual else "" |
| 597 self.ExpectGit([ | 615 self.ExpectGit([ |
| 598 ["status -s -uno", ""], | 616 ["status -s -uno", ""], |
| 599 ["status -s -b -uno", "## some_branch\n"], | 617 ["status -s -b -uno", "## some_branch\n"], |
| 600 ["svn fetch", ""], | 618 ["svn fetch", ""], |
| 601 ["branch", " branch1\n* branch2\n"], | 619 ["branch", " branch1\n* branch2\n"], |
| 602 ["checkout -b %s" % TEST_CONFIG[TEMP_BRANCH], ""], | 620 ["checkout -b %s" % TEST_CONFIG[TEMP_BRANCH], ""], |
| 603 ["branch", " branch1\n* branch2\n"], | 621 ["branch", " branch1\n* branch2\n"], |
| 604 ["branch", " branch1\n* branch2\n"], | 622 ["branch", " branch1\n* branch2\n"], |
| 605 ["checkout -b %s svn/bleeding_edge" % TEST_CONFIG[BRANCHNAME], ""], | 623 ["checkout -b %s svn/bleeding_edge" % TEST_CONFIG[BRANCHNAME], ""], |
| 606 ["log -1 --format=%H ChangeLog", "1234\n"], | 624 ["log -1 --format=%H ChangeLog", "1234\n"], |
| 607 ["log -1 1234", "Last push ouput\n"], | 625 ["log -1 1234", "Last push ouput\n"], |
| 608 ["log 1234..HEAD --format=%H", "rev1\n"], | 626 ["log 1234..HEAD --format=%H", "rev1\n"], |
| 609 ["log -1 rev1 --format=\"%s\"", "Log text 1.\n"], | 627 ["log -1 rev1 --format=\"%s\"", "Log text 1.\n"], |
| 610 ["log -1 rev1 --format=\"%B\"", "Text\nLOG=YES\nBUG=v8:321\nText\n"], | 628 ["log -1 rev1 --format=\"%B\"", "Text\nLOG=YES\nBUG=v8:321\nText\n"], |
| 611 ["log -1 rev1 --format=\"%an\"", "author1@chromium.org\n"], | 629 ["log -1 rev1 --format=\"%an\"", "author1@chromium.org\n"], |
| 612 [("commit -a -m \"Prepare push to trunk. " | 630 [("commit -a -m \"Prepare push to trunk. " |
| 613 "Now working on version 3.22.6.\""), | 631 "Now working on version 3.22.6.%s\"" % review_suffix), |
| 614 " 2 files changed\n", | 632 " 2 files changed\n", |
| 615 CheckPreparePush], | 633 CheckPreparePush], |
| 616 ["cl upload -r \"reviewer@chromium.org\" --send-mail%s" % force_flag, | 634 ["cl upload -r \"reviewer@chromium.org\" --send-mail%s" % force_flag, |
| 617 "done\n"], | 635 "done\n"], |
| 618 ["cl dcommit -f", "Closing issue\n"], | 636 ["cl dcommit -f", "Closing issue\n"], |
| 619 ["svn fetch", "fetch result\n"], | 637 ["svn fetch", "fetch result\n"], |
| 620 ["checkout svn/bleeding_edge", ""], | 638 ["checkout svn/bleeding_edge", ""], |
| 621 [("log -1 --format=%H --grep=\"Prepare push to trunk. " | 639 [("log -1 --format=%H --grep=\"Prepare push to trunk. " |
| 622 "Now working on version 3.22.6.\""), | 640 "Now working on version 3.22.6.\""), |
| 623 "hash1\n"], | 641 "hash1\n"], |
| (...skipping 10 matching lines...) Expand all Loading... |
| 634 ["checkout -b v8-roll-123456", ""], | 652 ["checkout -b v8-roll-123456", ""], |
| 635 [("commit -am \"Update V8 to version 3.22.5.\n\n" | 653 [("commit -am \"Update V8 to version 3.22.5.\n\n" |
| 636 "TBR=reviewer@chromium.org\""), | 654 "TBR=reviewer@chromium.org\""), |
| 637 ""], | 655 ""], |
| 638 ["cl upload --send-mail%s" % force_flag, ""], | 656 ["cl upload --send-mail%s" % force_flag, ""], |
| 639 ["checkout -f some_branch", ""], | 657 ["checkout -f some_branch", ""], |
| 640 ["branch -D %s" % TEST_CONFIG[TEMP_BRANCH], ""], | 658 ["branch -D %s" % TEST_CONFIG[TEMP_BRANCH], ""], |
| 641 ["branch -D %s" % TEST_CONFIG[BRANCHNAME], ""], | 659 ["branch -D %s" % TEST_CONFIG[BRANCHNAME], ""], |
| 642 ["branch -D %s" % TEST_CONFIG[TRUNKBRANCH], ""], | 660 ["branch -D %s" % TEST_CONFIG[TRUNKBRANCH], ""], |
| 643 ]) | 661 ]) |
| 644 self.ExpectReadline([ | 662 |
| 645 "Y", # Confirm last push. | 663 # Expected keyboard input in manual mode: |
| 646 "", # Open editor. | 664 if manual: |
| 647 "Y", # Increment build number. | 665 self.ExpectReadline([ |
| 648 "reviewer@chromium.org", # V8 reviewer. | 666 "Y", # Confirm last push. |
| 649 "LGTX", # Enter LGTM for V8 CL (wrong). | 667 "", # Open editor. |
| 650 "LGTM", # Enter LGTM for V8 CL. | 668 "Y", # Increment build number. |
| 651 "Y", # Sanity check. | 669 "reviewer@chromium.org", # V8 reviewer. |
| 652 "reviewer@chromium.org", # Chromium reviewer. | 670 "LGTX", # Enter LGTM for V8 CL (wrong). |
| 653 ]) | 671 "LGTM", # Enter LGTM for V8 CL. |
| 654 if force: | 672 "Y", # Sanity check. |
| 655 # TODO(machenbach): The lgtm for the prepare push is just temporary. | 673 "reviewer@chromium.org", # Chromium reviewer. |
| 656 # There should be no user input in "force" mode. | 674 ]) |
| 675 |
| 676 # Expected keyboard input in semi-automatic mode: |
| 677 if not manual and not force: |
| 657 self.ExpectReadline([ | 678 self.ExpectReadline([ |
| 658 "LGTM", # Enter LGTM for V8 CL. | 679 "LGTM", # Enter LGTM for V8 CL. |
| 659 ]) | 680 ]) |
| 660 | 681 |
| 661 class Options( object ): | 682 # No keyboard input in forced mode: |
| 662 pass | 683 if force: |
| 684 self.ExpectReadline([]) |
| 663 | 685 |
| 664 options = Options() | 686 options = MakeOptions(f=force, m=manual, |
| 665 options.s = 0 | 687 r="reviewer@chromium.org" if not manual else None, |
| 666 options.l = None | 688 c = TEST_CONFIG[CHROMIUM]) |
| 667 options.f = force | |
| 668 options.r = "reviewer@chromium.org" if force else None | |
| 669 options.c = TEST_CONFIG[CHROMIUM] | |
| 670 RunPushToTrunk(TEST_CONFIG, options, self) | 689 RunPushToTrunk(TEST_CONFIG, options, self) |
| 671 | 690 |
| 672 deps = FileToText(TEST_CONFIG[DEPS_FILE]) | 691 deps = FileToText(TEST_CONFIG[DEPS_FILE]) |
| 673 self.assertTrue(re.search("\"v8_revision\": \"123456\"", deps)) | 692 self.assertTrue(re.search("\"v8_revision\": \"123456\"", deps)) |
| 674 | 693 |
| 675 cl = FileToText(TEST_CONFIG[CHANGELOG_FILE]) | 694 cl = FileToText(TEST_CONFIG[CHANGELOG_FILE]) |
| 676 self.assertTrue(re.search(r"^\d\d\d\d\-\d+\-\d+: Version 3\.22\.5", cl)) | 695 self.assertTrue(re.search(r"^\d\d\d\d\-\d+\-\d+: Version 3\.22\.5", cl)) |
| 677 self.assertTrue(re.search(r" Log text 1 \(issue 321\).", cl)) | 696 self.assertTrue(re.search(r" Log text 1 \(issue 321\).", cl)) |
| 678 self.assertTrue(re.search(r"1999\-04\-05: Version 3\.22\.4", cl)) | 697 self.assertTrue(re.search(r"1999\-04\-05: Version 3\.22\.4", cl)) |
| 679 | 698 |
| 680 # Note: The version file is on build number 5 again in the end of this test | 699 # Note: The version file is on build number 5 again in the end of this test |
| 681 # since the git command that merges to the bleeding edge branch is mocked | 700 # since the git command that merges to the bleeding edge branch is mocked |
| 682 # out. | 701 # out. |
| 683 | 702 |
| 684 def testPushToTrunk(self): | 703 def testPushToTrunkManual(self): |
| 704 self._PushToTrunk(manual=True) |
| 705 |
| 706 def testPushToTrunkSemiAutomatic(self): |
| 685 self._PushToTrunk() | 707 self._PushToTrunk() |
| 686 | 708 |
| 687 def testPushToTrunkForced(self): | 709 def testPushToTrunkForced(self): |
| 688 self._PushToTrunk(force=True) | 710 self._PushToTrunk(force=True) |
| 689 | 711 |
| 690 def testAutoRoll(self): | 712 def testAutoRoll(self): |
| 691 TEST_CONFIG[DOT_GIT_LOCATION] = self.MakeEmptyTempFile() | 713 TEST_CONFIG[DOT_GIT_LOCATION] = self.MakeEmptyTempFile() |
| 692 | 714 |
| 693 # TODO(machenbach): Get rid of the editor check in automatic mode. | |
| 694 os.environ["EDITOR"] = "vi" | |
| 695 | |
| 696 self.ExpectReadURL([ | 715 self.ExpectReadURL([ |
| 697 ["https://v8-status.appspot.com/lkgr", Exception("Network problem")], | 716 ["https://v8-status.appspot.com/lkgr", Exception("Network problem")], |
| 698 ["https://v8-status.appspot.com/lkgr", "100"], | 717 ["https://v8-status.appspot.com/lkgr", "100"], |
| 699 ]) | 718 ]) |
| 700 | 719 |
| 701 self.ExpectGit([ | 720 self.ExpectGit([ |
| 702 ["status -s -uno", ""], | 721 ["status -s -uno", ""], |
| 703 ["status -s -b -uno", "## some_branch\n"], | 722 ["status -s -b -uno", "## some_branch\n"], |
| 704 ["svn fetch", ""], | 723 ["svn fetch", ""], |
| 705 ["svn log -1 --oneline", "r101 | Text"], | 724 ["svn log -1 --oneline", "r101 | Text"], |
| 706 ]) | 725 ]) |
| 707 | 726 |
| 708 # TODO(machenbach): Make a convenience wrapper for this. | 727 auto_roll.RunAutoRoll(TEST_CONFIG, MakeOptions(m=False, f=True), self) |
| 709 class Options( object ): | |
| 710 pass | |
| 711 | |
| 712 options = Options() | |
| 713 options.s = 0 | |
| 714 | |
| 715 auto_roll.RunAutoRoll(TEST_CONFIG, options, self) | |
| 716 | 728 |
| 717 self.assertEquals("100", self.MakeStep().Restore("lkgr")) | 729 self.assertEquals("100", self.MakeStep().Restore("lkgr")) |
| 718 self.assertEquals("101", self.MakeStep().Restore("latest")) | 730 self.assertEquals("101", self.MakeStep().Restore("latest")) |
| 719 | 731 |
| 720 | 732 |
| 721 class SystemTest(unittest.TestCase): | 733 class SystemTest(unittest.TestCase): |
| 722 def testReload(self): | 734 def testReload(self): |
| 723 step = MakeStep(step_class=PrepareChangeLog, number=0, state={}, config={}, | 735 step = MakeStep(step_class=PrepareChangeLog, number=0, state={}, config={}, |
| 724 options=None, | 736 options=None, |
| 725 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER) | 737 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER) |
| 726 body = step.Reload( | 738 body = step.Reload( |
| 727 """------------------------------------------------------------------------ | 739 """------------------------------------------------------------------------ |
| 728 r17997 | machenbach@chromium.org | 2013-11-22 11:04:04 +0100 (...) | 6 lines | 740 r17997 | machenbach@chromium.org | 2013-11-22 11:04:04 +0100 (...) | 6 lines |
| 729 | 741 |
| 730 Prepare push to trunk. Now working on version 3.23.11. | 742 Prepare push to trunk. Now working on version 3.23.11. |
| 731 | 743 |
| 732 R=danno@chromium.org | 744 R=danno@chromium.org |
| 733 | 745 |
| 734 Review URL: https://codereview.chromium.org/83173002 | 746 Review URL: https://codereview.chromium.org/83173002 |
| 735 | 747 |
| 736 ------------------------------------------------------------------------""") | 748 ------------------------------------------------------------------------""") |
| 737 self.assertEquals( | 749 self.assertEquals( |
| 738 """Prepare push to trunk. Now working on version 3.23.11. | 750 """Prepare push to trunk. Now working on version 3.23.11. |
| 739 | 751 |
| 740 R=danno@chromium.org | 752 R=danno@chromium.org |
| 741 | 753 |
| 742 Committed: https://code.google.com/p/v8/source/detail?r=17997""", body) | 754 Committed: https://code.google.com/p/v8/source/detail?r=17997""", body) |
| OLD | NEW |