1#!/usr/bin/env python3
2# Copyright 2013 the V8 project authors. All rights reserved.
3# Redistribution and use in source and binary forms, with or without
4# modification, are permitted provided that the following conditions are
5# met:
6#
7#     * Redistributions of source code must retain the above copyright
8#       notice, this list of conditions and the following disclaimer.
9#     * Redistributions in binary form must reproduce the above
10#       copyright notice, this list of conditions and the following
11#       disclaimer in the documentation and/or other materials provided
12#       with the distribution.
13#     * Neither the name of Google Inc. nor the names of its
14#       contributors may be used to endorse or promote products derived
15#       from this software without specific prior written permission.
16#
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29import json
30import os
31import shutil
32import tempfile
33import traceback
34import unittest
35
36import auto_push
37from auto_push import LastReleaseBailout
38import auto_roll
39import common_includes
40from common_includes import *
41import create_release
42from create_release import *
43import merge_to_branch
44from merge_to_branch import MergeToBranch
45import roll_merge
46from roll_merge import RollMerge
47
48TEST_CONFIG = {
49  "DEFAULT_CWD": None,
50  "BRANCHNAME": "test-prepare-push",
51  "PERSISTFILE_BASENAME": "/tmp/test-create-releases-tempfile",
52  "PATCH_FILE": "/tmp/test-v8-create-releases-tempfile-tempfile-patch",
53  "COMMITMSG_FILE": "/tmp/test-v8-create-releases-tempfile-commitmsg",
54  "CHROMIUM": "/tmp/test-create-releases-tempfile-chromium",
55  "SETTINGS_LOCATION": None,
56  "ALREADY_MERGING_SENTINEL_FILE":
57      "/tmp/test-merge-to-branch-tempfile-already-merging",
58  "TEMPORARY_PATCH_FILE": "/tmp/test-merge-to-branch-tempfile-temporary-patch",
59}
60
61
62AUTO_PUSH_ARGS = [
63  "-a", "author@chromium.org",
64  "-r", "reviewer@chromium.org",
65]
66
67
68class ToplevelTest(unittest.TestCase):
69  def testSaniniziteVersionTags(self):
70    self.assertEquals("4.8.230", SanitizeVersionTag("4.8.230"))
71    self.assertEquals("4.8.230", SanitizeVersionTag("tags/4.8.230"))
72    self.assertEquals(None, SanitizeVersionTag("candidate"))
73
74  def testNormalizeVersionTags(self):
75    input = ["4.8.230",
76              "tags/4.8.230",
77              "tags/4.8.224.1",
78              "4.8.224.1",
79              "4.8.223.1",
80              "tags/4.8.223",
81              "tags/4.8.231",
82              "candidates"]
83    expected = ["4.8.230",
84                "4.8.230",
85                "4.8.224.1",
86                "4.8.224.1",
87                "4.8.223.1",
88                "4.8.223",
89                "4.8.231",
90                ]
91    self.assertEquals(expected, NormalizeVersionTags(input))
92
93  def testCommand(self):
94    """Ensure json can decode the output of commands."""
95    json.dumps(Command('ls', pipe=True))
96
97
98def Cmd(*args, **kwargs):
99  """Convenience function returning a shell command test expectation."""
100  return {
101    "name": "command",
102    "args": args,
103    "ret": args[-1],
104    "cb": kwargs.get("cb"),
105    "cwd": kwargs.get("cwd", TEST_CONFIG["DEFAULT_CWD"]),
106  }
107
108
109def RL(text, cb=None):
110  """Convenience function returning a readline test expectation."""
111  return {
112    "name": "readline",
113    "args": [],
114    "ret": text,
115    "cb": cb,
116    "cwd": None,
117  }
118
119
120def URL(*args, **kwargs):
121  """Convenience function returning a readurl test expectation."""
122  return {
123    "name": "readurl",
124    "args": args[:-1],
125    "ret": args[-1],
126    "cb": kwargs.get("cb"),
127    "cwd": None,
128  }
129
130
131class SimpleMock(object):
132  def __init__(self):
133    self._recipe = []
134    self._index = -1
135
136  def Expect(self, recipe):
137    self._recipe = recipe
138
139  def Call(self, name, *args, **kwargs):  # pragma: no cover
140    self._index += 1
141
142    try:
143      expected_call = self._recipe[self._index]
144    except IndexError:
145      raise NoRetryException("Calling %s %s" % (name, " ".join(args)))
146
147    if not isinstance(expected_call, dict):
148      raise NoRetryException("Found wrong expectation type for %s %s" %
149                             (name, " ".join(args)))
150
151    if expected_call["name"] != name:
152      raise NoRetryException("Expected action: %s %s - Actual: %s" %
153          (expected_call["name"], expected_call["args"], name))
154
155    # Check if the given working directory matches the expected one.
156    if expected_call["cwd"] != kwargs.get("cwd"):
157      raise NoRetryException("Expected cwd: %s in %s %s - Actual: %s" %
158          (expected_call["cwd"],
159           expected_call["name"],
160           expected_call["args"],
161           kwargs.get("cwd")))
162
163    # The number of arguments in the expectation must match the actual
164    # arguments.
165    if len(args) > len(expected_call['args']):
166      raise NoRetryException("When calling %s with arguments, the "
167          "expectations must consist of at least as many arguments." %
168          name)
169
170    # Compare expected and actual arguments.
171    for (expected_arg, actual_arg) in zip(expected_call['args'], args):
172      if expected_arg != actual_arg:
173        raise NoRetryException("Expected: %s - Actual: %s" %
174                               (expected_arg, actual_arg))
175
176    # The expected call contains an optional callback for checking the context
177    # at the time of the call.
178    if expected_call['cb']:
179      try:
180        expected_call['cb']()
181      except:
182        tb = traceback.format_exc()
183        raise NoRetryException("Caught exception from callback: %s" % tb)
184
185    # If the return value is an exception, raise it instead of returning.
186    if isinstance(expected_call['ret'], Exception):
187      raise expected_call['ret']
188    return expected_call['ret']
189
190  def AssertFinished(self):  # pragma: no cover
191    if self._index < len(self._recipe) -1:
192      raise NoRetryException("Called mock too seldom: %d vs. %d" %
193                             (self._index, len(self._recipe)))
194
195
196class ScriptTest(unittest.TestCase):
197  def MakeEmptyTempFile(self):
198    handle, name = tempfile.mkstemp()
199    os.close(handle)
200    self._tmp_files.append(name)
201    return name
202
203  def MakeEmptyTempDirectory(self):
204    name = tempfile.mkdtemp()
205    self._tmp_files.append(name)
206    return name
207
208
209  def WriteFakeVersionFile(self, major=3, minor=22, build=4, patch=0):
210    version_file = os.path.join(TEST_CONFIG["DEFAULT_CWD"], VERSION_FILE)
211    if not os.path.exists(os.path.dirname(version_file)):
212      os.makedirs(os.path.dirname(version_file))
213    with open(version_file, "w") as f:
214      f.write("  // Some line...\n")
215      f.write("\n")
216      f.write("#define V8_MAJOR_VERSION    %s\n" % major)
217      f.write("#define V8_MINOR_VERSION    %s\n" % minor)
218      f.write("#define V8_BUILD_NUMBER     %s\n" % build)
219      f.write("#define V8_PATCH_LEVEL      %s\n" % patch)
220      f.write("  // Some line...\n")
221      f.write("#define V8_IS_CANDIDATE_VERSION 0\n")
222
223  def WriteFakeWatchlistsFile(self):
224    watchlists_file = os.path.join(TEST_CONFIG["DEFAULT_CWD"], WATCHLISTS_FILE)
225    if not os.path.exists(os.path.dirname(watchlists_file)):
226      os.makedirs(os.path.dirname(watchlists_file))
227    with open(watchlists_file, "w") as f:
228
229      content = """
230    'merges': [
231      # Only enabled on branches created with tools/release/create_release.py
232      # 'v8-merges@googlegroups.com',
233    ],
234"""
235      f.write(content)
236
237  def MakeStep(self):
238    """Convenience wrapper."""
239    options = ScriptsBase(TEST_CONFIG, self, self._state).MakeOptions([])
240    return MakeStep(step_class=Step, state=self._state,
241                    config=TEST_CONFIG, side_effect_handler=self,
242                    options=options)
243
244  def RunStep(self, script=CreateRelease, step_class=Step, args=None):
245    """Convenience wrapper."""
246    args = args if args is not None else ["-m", "-a=author", "-r=reviewer", ]
247    return script(TEST_CONFIG, self, self._state).RunSteps([step_class], args)
248
249  def Call(self, fun, *args, **kwargs):
250    print("Calling %s with %s and %s" % (str(fun), str(args), str(kwargs)))
251
252  def Command(self, cmd, args="", prefix="", pipe=True, cwd=None):
253    print("%s %s" % (cmd, args))
254    print("in %s" % cwd)
255    return self._mock.Call("command", cmd + " " + args, cwd=cwd)
256
257  def ReadLine(self):
258    return self._mock.Call("readline")
259
260  def ReadURL(self, url, params):
261    if params is not None:
262      return self._mock.Call("readurl", url, params)
263    else:
264      return self._mock.Call("readurl", url)
265
266  def Sleep(self, seconds):
267    pass
268
269  def GetUTCStamp(self):
270    return "1000000"
271
272  def Expect(self, *args):
273    """Convenience wrapper."""
274    self._mock.Expect(*args)
275
276  def setUp(self):
277    self._mock = SimpleMock()
278    self._tmp_files = []
279    self._state = {}
280    TEST_CONFIG["DEFAULT_CWD"] = self.MakeEmptyTempDirectory()
281
282  def tearDown(self):
283    if os.path.exists(TEST_CONFIG["PERSISTFILE_BASENAME"]):
284      shutil.rmtree(TEST_CONFIG["PERSISTFILE_BASENAME"])
285
286    # Clean up temps. Doesn't work automatically.
287    for name in self._tmp_files:
288      if os.path.isfile(name):
289        os.remove(name)
290      if os.path.isdir(name):
291        shutil.rmtree(name)
292
293    self._mock.AssertFinished()
294
295  def testGitMock(self):
296    self.Expect([Cmd("git --version", "git version 1.2.3"),
297                 Cmd("git dummy", "")])
298    self.assertEquals("git version 1.2.3", self.MakeStep().Git("--version"))
299    self.assertEquals("", self.MakeStep().Git("dummy"))
300
301  def testCommonPrepareDefault(self):
302    self.Expect([
303      Cmd("git status -s -uno", ""),
304      Cmd("git checkout -f origin/main", ""),
305      Cmd("git fetch", ""),
306      Cmd("git branch", "  branch1\n* %s" % TEST_CONFIG["BRANCHNAME"]),
307      RL("Y"),
308      Cmd("git branch -D %s" % TEST_CONFIG["BRANCHNAME"], ""),
309    ])
310    self.MakeStep().CommonPrepare()
311    self.MakeStep().PrepareBranch()
312
313  def testCommonPrepareNoConfirm(self):
314    self.Expect([
315      Cmd("git status -s -uno", ""),
316      Cmd("git checkout -f origin/main", ""),
317      Cmd("git fetch", ""),
318      Cmd("git branch", "  branch1\n* %s" % TEST_CONFIG["BRANCHNAME"]),
319      RL("n"),
320    ])
321    self.MakeStep().CommonPrepare()
322    self.assertRaises(Exception, self.MakeStep().PrepareBranch)
323
324  def testCommonPrepareDeleteBranchFailure(self):
325    self.Expect([
326      Cmd("git status -s -uno", ""),
327      Cmd("git checkout -f origin/main", ""),
328      Cmd("git fetch", ""),
329      Cmd("git branch", "  branch1\n* %s" % TEST_CONFIG["BRANCHNAME"]),
330      RL("Y"),
331      Cmd("git branch -D %s" % TEST_CONFIG["BRANCHNAME"], None),
332    ])
333    self.MakeStep().CommonPrepare()
334    self.assertRaises(Exception, self.MakeStep().PrepareBranch)
335
336  def testInitialEnvironmentChecks(self):
337    TextToFile("", os.path.join(TEST_CONFIG["DEFAULT_CWD"], ".git"))
338    os.environ["EDITOR"] = "vi"
339    self.Expect([
340      Cmd("which vi", "/usr/bin/vi"),
341    ])
342    self.MakeStep().InitialEnvironmentChecks(TEST_CONFIG["DEFAULT_CWD"])
343
344  def testTagTimeout(self):
345    self.Expect([
346      Cmd("git fetch", ""),
347      Cmd("git log -1 --format=%H --grep=\"Title\" origin/tag_name", ""),
348      Cmd("git fetch", ""),
349      Cmd("git log -1 --format=%H --grep=\"Title\" origin/tag_name", ""),
350      Cmd("git fetch", ""),
351      Cmd("git log -1 --format=%H --grep=\"Title\" origin/tag_name", ""),
352      Cmd("git fetch", ""),
353      Cmd("git log -1 --format=%H --grep=\"Title\" origin/tag_name", ""),
354    ])
355    args = ["--branch", "candidates", "ab12345"]
356    self._state["version"] = "tag_name"
357    self._state["commit_title"] = "Title"
358    self.assertRaises(Exception,
359        lambda: self.RunStep(RollMerge, TagRevision, args))
360
361  def testReadAndPersistVersion(self):
362    self.WriteFakeVersionFile(build=5)
363    step = self.MakeStep()
364    step.ReadAndPersistVersion()
365    self.assertEquals("3", step["major"])
366    self.assertEquals("22", step["minor"])
367    self.assertEquals("5", step["build"])
368    self.assertEquals("0", step["patch"])
369
370  def testRegex(self):
371    self.assertEqual("(issue 321)",
372                     re.sub(r"BUG=v8:(.*)$", r"(issue \1)", "BUG=v8:321"))
373    self.assertEqual("(Chromium issue 321)",
374                     re.sub(r"BUG=(.*)$", r"(Chromium issue \1)", "BUG=321"))
375
376    cl = "  too little\n\ttab\ttab\n         too much\n        trailing  "
377    cl = MSub(r"\t", r"        ", cl)
378    cl = MSub(r"^ {1,7}([^ ])", r"        \1", cl)
379    cl = MSub(r"^ {9,80}([^ ])", r"        \1", cl)
380    cl = MSub(r" +$", r"", cl)
381    self.assertEqual("        too little\n"
382                     "        tab        tab\n"
383                     "        too much\n"
384                     "        trailing", cl)
385
386    self.assertEqual("//\n#define V8_BUILD_NUMBER  3\n",
387                     MSub(r"(?<=#define V8_BUILD_NUMBER)(?P<space>\s+)\d*$",
388                          r"\g<space>3",
389                          "//\n#define V8_BUILD_NUMBER  321\n"))
390
391  TAGS = """
3924425.0
3930.0.0.0
3943.9.6
3953.22.4
396test_tag
397"""
398
399  # Version as tag: 3.22.4.0. Version on main: 3.22.6.
400  # Make sure that the latest version is 3.22.6.0.
401  def testIncrementVersion(self):
402    self.Expect([
403      Cmd("git fetch origin +refs/tags/*:refs/tags/*", ""),
404      Cmd("git tag", self.TAGS),
405      Cmd("git checkout -f origin/main -- include/v8-version.h",
406          "", cb=lambda: self.WriteFakeVersionFile(3, 22, 6)),
407    ])
408
409    self.RunStep(CreateRelease, IncrementVersion)
410
411    self.assertEquals("3", self._state["new_major"])
412    self.assertEquals("22", self._state["new_minor"])
413    self.assertEquals("7", self._state["new_build"])
414    self.assertEquals("0", self._state["new_patch"])
415
416  def testBootstrapper(self):
417    work_dir = self.MakeEmptyTempDirectory()
418    class FakeScript(ScriptsBase):
419      def _Steps(self):
420        return []
421
422    # Use the test configuration without the fake testing default work dir.
423    fake_config = dict(TEST_CONFIG)
424    del(fake_config["DEFAULT_CWD"])
425
426    self.Expect([
427      Cmd("fetch v8", "", cwd=work_dir),
428    ])
429    FakeScript(fake_config, self).Run(["--work-dir", work_dir])
430
431  def testCreateRelease(self):
432    TextToFile("", os.path.join(TEST_CONFIG["DEFAULT_CWD"], ".git"))
433
434    # The version file on main has build level 5.
435    self.WriteFakeVersionFile(build=5)
436
437    commit_msg = """Version 3.22.5"""
438
439    def CheckVersionCommit():
440      commit = FileToText(TEST_CONFIG["COMMITMSG_FILE"])
441      self.assertEquals(commit_msg, commit)
442      version = FileToText(
443          os.path.join(TEST_CONFIG["DEFAULT_CWD"], VERSION_FILE))
444      self.assertTrue(re.search(r"#define V8_MINOR_VERSION\s+22", version))
445      self.assertTrue(re.search(r"#define V8_BUILD_NUMBER\s+5", version))
446      self.assertFalse(re.search(r"#define V8_BUILD_NUMBER\s+6", version))
447      self.assertTrue(re.search(r"#define V8_PATCH_LEVEL\s+0", version))
448      self.assertTrue(
449          re.search(r"#define V8_IS_CANDIDATE_VERSION\s+0", version))
450
451    expectations = [
452      Cmd("git fetch origin +refs/heads/*:refs/heads/*", ""),
453      Cmd("git checkout -f origin/main", "", cb=self.WriteFakeWatchlistsFile),
454      Cmd("git branch", ""),
455      Cmd("git fetch origin +refs/tags/*:refs/tags/*", ""),
456      Cmd("git tag", self.TAGS),
457      Cmd("git checkout -f origin/main -- include/v8-version.h",
458          "", cb=self.WriteFakeVersionFile),
459      Cmd("git log -1 --format=%H 3.22.4", "release_hash\n"),
460      Cmd("git log -1 --format=%s release_hash", "Version 3.22.4\n"),
461      Cmd("git log -1 --format=%H release_hash^", "abc3\n"),
462      Cmd("git log --format=%H abc3..push_hash", "rev1\n"),
463      Cmd("git push origin push_hash:refs/heads/3.22.5", ""),
464      Cmd("git reset --hard origin/main", ""),
465      Cmd("git new-branch work-branch --upstream origin/3.22.5", ""),
466      Cmd("git checkout -f 3.22.4 -- include/v8-version.h", "",
467          cb=self.WriteFakeVersionFile),
468      Cmd("git commit -aF \"%s\"" % TEST_CONFIG["COMMITMSG_FILE"], "",
469          cb=CheckVersionCommit),
470      Cmd("git cl upload --send-mail "
471          "-f --set-bot-commit --bypass-hooks --no-autocc --message-file "
472          "\"%s\"" % TEST_CONFIG["COMMITMSG_FILE"], ""),
473      Cmd("git cl land --bypass-hooks -f", ""),
474      Cmd("git fetch", ""),
475      Cmd("git log -1 --format=%H --grep="
476          "\"Version 3.22.5\" origin/3.22.5", "hsh_to_tag"),
477      Cmd("git tag 3.22.5 hsh_to_tag", ""),
478      Cmd("git push origin refs/tags/3.22.5:refs/tags/3.22.5", ""),
479      Cmd("git checkout -f origin/main", ""),
480      Cmd("git branch", "* main\n  work-branch\n"),
481      Cmd("git branch -D work-branch", ""),
482      Cmd("git gc", ""),
483    ]
484    self.Expect(expectations)
485
486    args = ["-a", "author@chromium.org",
487            "-r", "reviewer@chromium.org",
488            "--revision", "push_hash"]
489    CreateRelease(TEST_CONFIG, self).Run(args)
490
491    # Note: The version file is on build number 5 again in the end of this test
492    # since the git command that merges to main is mocked out.
493
494    # Check for correct content of the WATCHLISTS file
495
496    watchlists_content = FileToText(os.path.join(TEST_CONFIG["DEFAULT_CWD"],
497                                          WATCHLISTS_FILE))
498    expected_watchlists_content = """
499    'merges': [
500      # Only enabled on branches created with tools/release/create_release.py
501      'v8-merges@googlegroups.com',
502    ],
503"""
504    self.assertEqual(watchlists_content, expected_watchlists_content)
505
506  C_V8_22624_LOG = """V8 CL.
507
508git-svn-id: https://v8.googlecode.com/svn/branches/bleeding_edge@22624 123
509
510"""
511
512  C_V8_123455_LOG = """V8 CL.
513
514git-svn-id: https://v8.googlecode.com/svn/branches/bleeding_edge@123455 123
515
516"""
517
518  C_V8_123456_LOG = """V8 CL.
519
520git-svn-id: https://v8.googlecode.com/svn/branches/bleeding_edge@123456 123
521
522"""
523
524  ROLL_COMMIT_MSG = """Update V8 to version 3.22.4.
525
526Summary of changes available at:
527https://chromium.googlesource.com/v8/v8/+log/last_rol..roll_hsh
528
529Please follow these instructions for assigning/CC'ing issues:
530https://v8.dev/docs/triage-issues
531
532Please close rolling in case of a roll revert:
533https://v8-roll.appspot.com/
534This only works with a Google account.
535
536CQ_INCLUDE_TRYBOTS=luci.chromium.try:linux-blink-rel
537CQ_INCLUDE_TRYBOTS=luci.chromium.try:linux_optional_gpu_tests_rel
538CQ_INCLUDE_TRYBOTS=luci.chromium.try:mac_optional_gpu_tests_rel
539CQ_INCLUDE_TRYBOTS=luci.chromium.try:win_optional_gpu_tests_rel
540CQ_INCLUDE_TRYBOTS=luci.chromium.try:android_optional_gpu_tests_rel
541
542R=reviewer@chromium.org"""
543
544  # Snippet from the original DEPS file.
545  FAKE_DEPS = """
546vars = {
547  "v8_revision": "last_roll_hsh",
548}
549deps = {
550  "src/v8":
551    (Var("googlecode_url") % "v8") + "/" + Var("v8_branch") + "@" +
552    Var("v8_revision"),
553}
554"""
555
556  def testChromiumRollUpToDate(self):
557    TEST_CONFIG["CHROMIUM"] = self.MakeEmptyTempDirectory()
558    json_output_file = os.path.join(TEST_CONFIG["CHROMIUM"], "out.json")
559    TextToFile(self.FAKE_DEPS, os.path.join(TEST_CONFIG["CHROMIUM"], "DEPS"))
560    chrome_dir = TEST_CONFIG["CHROMIUM"]
561    self.Expect([
562      Cmd("git fetch origin", ""),
563      Cmd("git fetch origin +refs/tags/*:refs/tags/*", ""),
564      Cmd("gclient getdep -r src/v8", "last_roll_hsh", cwd=chrome_dir),
565      Cmd("git describe --tags last_roll_hsh", "3.22.4"),
566      Cmd("git fetch origin +refs/tags/*:refs/tags/*", ""),
567      Cmd("git rev-list --max-age=395200 --tags",
568          "bad_tag\nroll_hsh\nhash_123"),
569      Cmd("git describe --tags bad_tag", ""),
570      Cmd("git describe --tags roll_hsh", "3.22.4"),
571      Cmd("git describe --tags hash_123", "3.22.3"),
572      Cmd("git describe --tags roll_hsh", "3.22.4"),
573      Cmd("git describe --tags hash_123", "3.22.3"),
574    ])
575
576    result = auto_roll.AutoRoll(TEST_CONFIG, self).Run(
577        AUTO_PUSH_ARGS + [
578          "-c", TEST_CONFIG["CHROMIUM"],
579          "--json-output", json_output_file])
580    self.assertEquals(0, result)
581    json_output = json.loads(FileToText(json_output_file))
582    self.assertEquals("up_to_date", json_output["monitoring_state"])
583
584
585  def testChromiumRoll(self):
586    # Setup fake directory structures.
587    TEST_CONFIG["CHROMIUM"] = self.MakeEmptyTempDirectory()
588    json_output_file = os.path.join(TEST_CONFIG["CHROMIUM"], "out.json")
589    TextToFile(self.FAKE_DEPS, os.path.join(TEST_CONFIG["CHROMIUM"], "DEPS"))
590    TextToFile("", os.path.join(TEST_CONFIG["CHROMIUM"], ".git"))
591    chrome_dir = TEST_CONFIG["CHROMIUM"]
592    os.makedirs(os.path.join(chrome_dir, "v8"))
593
594    def WriteDeps():
595      TextToFile("Some line\n   \"v8_revision\": \"22624\",\n  some line",
596                 os.path.join(chrome_dir, "DEPS"))
597
598    expectations = [
599      Cmd("git fetch origin", ""),
600      Cmd("git fetch origin +refs/tags/*:refs/tags/*", ""),
601      Cmd("gclient getdep -r src/v8", "last_roll_hsh", cwd=chrome_dir),
602      Cmd("git describe --tags last_roll_hsh", "3.22.3.1"),
603      Cmd("git fetch origin +refs/tags/*:refs/tags/*", ""),
604      Cmd("git rev-list --max-age=395200 --tags",
605          "bad_tag\nroll_hsh\nhash_123"),
606      Cmd("git describe --tags bad_tag", ""),
607      Cmd("git describe --tags roll_hsh", "3.22.4"),
608      Cmd("git describe --tags hash_123", "3.22.3"),
609      Cmd("git describe --tags roll_hsh", "3.22.4"),
610      Cmd("git log -1 --format=%s roll_hsh", "Version 3.22.4\n"),
611      Cmd("git describe --tags roll_hsh", "3.22.4"),
612      Cmd("git describe --tags last_roll_hsh", "3.22.2.1"),
613      Cmd("git status -s -uno", "", cwd=chrome_dir),
614      Cmd("git checkout -f main", "", cwd=chrome_dir),
615      Cmd("git branch", "", cwd=chrome_dir),
616      Cmd("git pull", "", cwd=chrome_dir),
617      Cmd("git fetch origin", ""),
618      Cmd("git new-branch work-branch", "", cwd=chrome_dir),
619      Cmd("gclient setdep -r src/v8@roll_hsh", "", cb=WriteDeps,
620          cwd=chrome_dir),
621      Cmd(("git commit -am \"%s\" "
622           "--author \"author@chromium.org <author@chromium.org>\"" %
623           self.ROLL_COMMIT_MSG),
624          "", cwd=chrome_dir),
625      Cmd("git cl upload --send-mail -f "
626          "--cq-dry-run --set-bot-commit --bypass-hooks", "",
627          cwd=chrome_dir),
628      Cmd("git checkout -f main", "", cwd=chrome_dir),
629      Cmd("git branch -D work-branch", "", cwd=chrome_dir),
630    ]
631    self.Expect(expectations)
632
633    args = ["-a", "author@chromium.org", "-c", chrome_dir,
634            "-r", "reviewer@chromium.org", "--json-output", json_output_file]
635    auto_roll.AutoRoll(TEST_CONFIG, self).Run(args)
636
637    deps = FileToText(os.path.join(chrome_dir, "DEPS"))
638    self.assertTrue(re.search("\"v8_revision\": \"22624\"", deps))
639
640    json_output = json.loads(FileToText(json_output_file))
641    self.assertEquals("success", json_output["monitoring_state"])
642
643  def testCheckLastPushRecently(self):
644    self.Expect([
645      Cmd("git fetch origin +refs/tags/*:refs/tags/*", ""),
646      Cmd("git tag", self.TAGS),
647      Cmd("git log -1 --format=%H 3.22.4", "release_hash\n"),
648      Cmd("git log -1 --format=%s release_hash",
649          "Version 3.22.4 (based on abc3)\n"),
650      Cmd("git log --format=%H abc3..abc123", "\n"),
651    ])
652
653    self._state["candidate"] = "abc123"
654    self.assertEquals(0, self.RunStep(
655        auto_push.AutoPush, LastReleaseBailout, AUTO_PUSH_ARGS))
656
657  def testAutoPush(self):
658    self.Expect([
659      Cmd("git fetch", ""),
660      Cmd("git fetch origin +refs/heads/lkgr:refs/heads/lkgr", ""),
661      Cmd("git show-ref -s refs/heads/lkgr", "abc123\n"),
662      Cmd("git fetch origin +refs/tags/*:refs/tags/*", ""),
663      Cmd("git tag", self.TAGS),
664      Cmd("git log -1 --format=%H 3.22.4", "release_hash\n"),
665      Cmd("git log -1 --format=%s release_hash",
666          "Version 3.22.4 (based on abc3)\n"),
667      Cmd("git log --format=%H abc3..abc123", "some_stuff\n"),
668    ])
669
670    auto_push.AutoPush(TEST_CONFIG, self).Run(AUTO_PUSH_ARGS + ["--push"])
671
672    state = json.loads(FileToText("%s-state.json"
673                                  % TEST_CONFIG["PERSISTFILE_BASENAME"]))
674
675    self.assertEquals("abc123", state["candidate"])
676
677  def testRollMerge(self):
678    TEST_CONFIG["ALREADY_MERGING_SENTINEL_FILE"] = self.MakeEmptyTempFile()
679    TextToFile("", os.path.join(TEST_CONFIG["DEFAULT_CWD"], ".git"))
680    self.WriteFakeVersionFile(build=5)
681    os.environ["EDITOR"] = "vi"
682    extra_patch = self.MakeEmptyTempFile()
683
684    def VerifyPatch(patch):
685      return lambda: self.assertEquals(patch,
686          FileToText(TEST_CONFIG["TEMPORARY_PATCH_FILE"]))
687
688    msg = """Version 3.22.5.1 (cherry-pick)
689
690Merged ab12345
691Merged ab23456
692Merged ab34567
693Merged ab45678
694Merged ab56789
695
696Title4
697
698Title2
699
700Title3
701
702Title1
703
704Revert "Something"
705
706BUG=123,234,345,456,567,v8:123
707"""
708
709    def VerifyLand():
710      commit = FileToText(TEST_CONFIG["COMMITMSG_FILE"])
711      self.assertEquals(msg, commit)
712      version = FileToText(
713          os.path.join(TEST_CONFIG["DEFAULT_CWD"], VERSION_FILE))
714      self.assertTrue(re.search(r"#define V8_MINOR_VERSION\s+22", version))
715      self.assertTrue(re.search(r"#define V8_BUILD_NUMBER\s+5", version))
716      self.assertTrue(re.search(r"#define V8_PATCH_LEVEL\s+1", version))
717      self.assertTrue(
718          re.search(r"#define V8_IS_CANDIDATE_VERSION\s+0", version))
719
720    self.Expect([
721      Cmd("git status -s -uno", ""),
722      Cmd("git checkout -f origin/main", ""),
723      Cmd("git fetch", ""),
724      Cmd("git branch", "  branch1\n* branch2\n"),
725      Cmd("git new-branch %s --upstream refs/remotes/origin/candidates" %
726          TEST_CONFIG["BRANCHNAME"], ""),
727      Cmd(("git log --format=%H --grep=\"Port ab12345\" "
728           "--reverse origin/main"),
729          "ab45678\nab23456"),
730      Cmd("git log -1 --format=%s ab45678", "Title1"),
731      Cmd("git log -1 --format=%s ab23456", "Title2"),
732      Cmd(("git log --format=%H --grep=\"Port ab23456\" "
733           "--reverse origin/main"),
734          ""),
735      Cmd(("git log --format=%H --grep=\"Port ab34567\" "
736           "--reverse origin/main"),
737          "ab56789"),
738      Cmd("git log -1 --format=%s ab56789", "Title3"),
739      RL("Y"),  # Automatically add corresponding ports (ab34567, ab56789)?
740      # Simulate git being down which stops the script.
741      Cmd("git log -1 --format=%s ab12345", None),
742      # Restart script in the failing step.
743      Cmd("git log -1 --format=%s ab12345", "Title4"),
744      Cmd("git log -1 --format=%s ab23456", "Title2"),
745      Cmd("git log -1 --format=%s ab34567", "Title3"),
746      Cmd("git log -1 --format=%s ab45678", "Title1"),
747      Cmd("git log -1 --format=%s ab56789", "Revert \"Something\""),
748      Cmd("git log -1 ab12345", "Title4\nBUG=123\nBUG=234"),
749      Cmd("git log -1 ab23456", "Title2\n BUG = v8:123,345"),
750      Cmd("git log -1 ab34567", "Title3\nBUG=567, 456"),
751      Cmd("git log -1 ab45678", "Title1\nBUG="),
752      Cmd("git log -1 ab56789", "Revert \"Something\"\nBUG=none"),
753      Cmd("git log -1 -p ab12345", "patch4"),
754      Cmd(("git apply --index --reject \"%s\"" %
755           TEST_CONFIG["TEMPORARY_PATCH_FILE"]),
756          "", cb=VerifyPatch("patch4")),
757      Cmd("git log -1 -p ab23456", "patch2"),
758      Cmd(("git apply --index --reject \"%s\"" %
759           TEST_CONFIG["TEMPORARY_PATCH_FILE"]),
760          "", cb=VerifyPatch("patch2")),
761      Cmd("git log -1 -p ab34567", "patch3"),
762      Cmd(("git apply --index --reject \"%s\"" %
763           TEST_CONFIG["TEMPORARY_PATCH_FILE"]),
764          "", cb=VerifyPatch("patch3")),
765      Cmd("git log -1 -p ab45678", "patch1"),
766      Cmd(("git apply --index --reject \"%s\"" %
767           TEST_CONFIG["TEMPORARY_PATCH_FILE"]),
768          "", cb=VerifyPatch("patch1")),
769      Cmd("git log -1 -p ab56789", "patch5\n"),
770      Cmd(("git apply --index --reject \"%s\"" %
771           TEST_CONFIG["TEMPORARY_PATCH_FILE"]),
772          "", cb=VerifyPatch("patch5\n")),
773      Cmd("git apply --index --reject \"%s\"" % extra_patch, ""),
774      RL("Y"),  # Automatically increment patch level?
775      Cmd("git commit -aF \"%s\"" % TEST_CONFIG["COMMITMSG_FILE"], ""),
776      RL("reviewer@chromium.org"),  # V8 reviewer.
777      Cmd("git cl upload --send-mail -r \"reviewer@chromium.org\" "
778          "--bypass-hooks", ""),
779      Cmd("git checkout -f %s" % TEST_CONFIG["BRANCHNAME"], ""),
780      RL("LGTM"),  # Enter LGTM for V8 CL.
781      Cmd("git cl presubmit", "Presubmit successfull\n"),
782      Cmd("git cl land -f --bypass-hooks", "Closing issue\n",
783          cb=VerifyLand),
784      Cmd("git fetch", ""),
785      Cmd("git log -1 --format=%H --grep=\""
786          "Version 3.22.5.1 (cherry-pick)"
787          "\" refs/remotes/origin/candidates",
788          ""),
789      Cmd("git fetch", ""),
790      Cmd("git log -1 --format=%H --grep=\""
791          "Version 3.22.5.1 (cherry-pick)"
792          "\" refs/remotes/origin/candidates",
793          "hsh_to_tag"),
794      Cmd("git tag 3.22.5.1 hsh_to_tag", ""),
795      Cmd("git push origin refs/tags/3.22.5.1:refs/tags/3.22.5.1", ""),
796      Cmd("git checkout -f origin/main", ""),
797      Cmd("git branch -D %s" % TEST_CONFIG["BRANCHNAME"], ""),
798    ])
799
800    # ab12345 and ab34567 are patches. ab23456 (included) and ab45678 are the
801    # MIPS ports of ab12345. ab56789 is the MIPS port of ab34567.
802    args = ["-f", "-p", extra_patch, "--branch", "candidates",
803            "ab12345", "ab23456", "ab34567"]
804
805    # The first run of the script stops because of git being down.
806    self.assertRaises(GitFailedException,
807        lambda: RollMerge(TEST_CONFIG, self).Run(args))
808
809    # Test that state recovery after restarting the script works.
810    args += ["-s", "4"]
811    RollMerge(TEST_CONFIG, self).Run(args)
812
813  def testMergeToBranch(self):
814    TEST_CONFIG["ALREADY_MERGING_SENTINEL_FILE"] = self.MakeEmptyTempFile()
815    TextToFile("", os.path.join(TEST_CONFIG["DEFAULT_CWD"], ".git"))
816    self.WriteFakeVersionFile(build=5)
817    os.environ["EDITOR"] = "vi"
818    extra_patch = self.MakeEmptyTempFile()
819
820
821    def VerifyPatch(patch):
822      return lambda: self.assertEquals(patch,
823          FileToText(TEST_CONFIG["TEMPORARY_PATCH_FILE"]))
824
825    info_msg = ("NOTE: This script will no longer automatically "
826     "update include/v8-version.h "
827     "and create a tag. This is done automatically by the autotag bot. "
828     "Please call the merge_to_branch.py with --help for more information.")
829
830    msg = """Merged: Squashed multiple commits.
831
832Merged: Title4
833Revision: ab12345
834
835Merged: Title2
836Revision: ab23456
837
838Merged: Title3
839Revision: ab34567
840
841Merged: Title1
842Revision: ab45678
843
844Merged: Revert \"Something\"
845Revision: ab56789
846
847BUG=123,234,345,456,567,v8:123
848NOTRY=true
849NOPRESUBMIT=true
850NOTREECHECKS=true
851"""
852
853    def VerifyLand():
854      commit = FileToText(TEST_CONFIG["COMMITMSG_FILE"])
855      self.assertEquals(msg, commit)
856
857    self.Expect([
858      Cmd("git status -s -uno", ""),
859      Cmd("git checkout -f origin/main", ""),
860      Cmd("git fetch", ""),
861      Cmd("git branch", "  branch1\n* branch2\n"),
862      Cmd("git new-branch %s --upstream refs/remotes/origin/candidates" %
863          TEST_CONFIG["BRANCHNAME"], ""),
864      Cmd(("git log --format=%H --grep=\"^[Pp]ort ab12345\" "
865           "--reverse origin/main"),
866          "ab45678\nab23456"),
867      Cmd("git log -1 --format=%s ab45678", "Title1"),
868      Cmd("git log -1 --format=%s ab23456", "Title2"),
869      Cmd(("git log --format=%H --grep=\"^[Pp]ort ab23456\" "
870           "--reverse origin/main"),
871          ""),
872      Cmd(("git log --format=%H --grep=\"^[Pp]ort ab34567\" "
873           "--reverse origin/main"),
874          "ab56789"),
875      Cmd("git log -1 --format=%s ab56789", "Title3"),
876      RL("Y"),  # Automatically add corresponding ports (ab34567, ab56789)?
877      # Simulate git being down which stops the script.
878      Cmd("git log -1 --format=%s ab12345", None),
879      # Restart script in the failing step.
880      Cmd("git log -1 --format=%s ab12345", "Title4"),
881      Cmd("git log -1 --format=%s ab23456", "Title2"),
882      Cmd("git log -1 --format=%s ab34567", "Title3"),
883      Cmd("git log -1 --format=%s ab45678", "Title1"),
884      Cmd("git log -1 --format=%s ab56789", "Revert \"Something\""),
885      Cmd("git log -1 ab12345", "Title4\nBUG=123\nBUG=234"),
886      Cmd("git log -1 ab23456", "Title2\n BUG = v8:123,345"),
887      Cmd("git log -1 ab34567", "Title3\nBug: 567, 456,345"),
888      Cmd("git log -1 ab45678", "Title1\nBug:"),
889      Cmd("git log -1 ab56789", "Revert \"Something\"\nBUG=none"),
890      Cmd("git log -1 -p ab12345", "patch4"),
891      Cmd(("git apply --index --reject \"%s\"" %
892           TEST_CONFIG["TEMPORARY_PATCH_FILE"]),
893          "", cb=VerifyPatch("patch4")),
894      Cmd("git log -1 -p ab23456", "patch2"),
895      Cmd(("git apply --index --reject \"%s\"" %
896           TEST_CONFIG["TEMPORARY_PATCH_FILE"]),
897          "", cb=VerifyPatch("patch2")),
898      Cmd("git log -1 -p ab34567", "patch3"),
899      Cmd(("git apply --index --reject \"%s\"" %
900           TEST_CONFIG["TEMPORARY_PATCH_FILE"]),
901          "", cb=VerifyPatch("patch3")),
902      Cmd("git log -1 -p ab45678", "patch1"),
903      Cmd(("git apply --index --reject \"%s\"" %
904           TEST_CONFIG["TEMPORARY_PATCH_FILE"]),
905          "", cb=VerifyPatch("patch1")),
906      Cmd("git log -1 -p ab56789", "patch5\n"),
907      Cmd(("git apply --index --reject \"%s\"" %
908           TEST_CONFIG["TEMPORARY_PATCH_FILE"]),
909          "", cb=VerifyPatch("patch5\n")),
910      Cmd("git apply --index --reject \"%s\"" % extra_patch, ""),
911      Cmd("git commit -aF \"%s\"" % TEST_CONFIG["COMMITMSG_FILE"], ""),
912      RL("reviewer@chromium.org"),  # V8 reviewer.
913      Cmd("git cl upload --send-mail -r \"reviewer@chromium.org\" "
914          "--bypass-hooks", ""),
915      Cmd("git checkout -f %s" % TEST_CONFIG["BRANCHNAME"], ""),
916      RL("LGTM"),  # Enter LGTM for V8 CL.
917      Cmd("git cl presubmit", "Presubmit successfull\n"),
918      Cmd("git cl land -f --bypass-hooks", "Closing issue\n",
919          cb=VerifyLand),
920      Cmd("git checkout -f origin/main", ""),
921      Cmd("git branch -D %s" % TEST_CONFIG["BRANCHNAME"], ""),
922    ])
923
924    # ab12345 and ab34567 are patches. ab23456 (included) and ab45678 are the
925    # MIPS ports of ab12345. ab56789 is the MIPS port of ab34567.
926    args = ["-f", "-p", extra_patch, "--branch", "candidates",
927            "ab12345", "ab23456", "ab34567"]
928
929    # The first run of the script stops because of git being down.
930    self.assertRaises(GitFailedException,
931        lambda: MergeToBranch(TEST_CONFIG, self).Run(args))
932
933    # Test that state recovery after restarting the script works.
934    args += ["-s", "4"]
935    MergeToBranch(TEST_CONFIG, self).Run(args)
936
937if __name__ == '__main__':
938  unittest.main()
939