Add V8 releases script.
authormachenbach@chromium.org <machenbach@chromium.org@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Wed, 9 Apr 2014 14:30:02 +0000 (14:30 +0000)
committermachenbach@chromium.org <machenbach@chromium.org@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Wed, 9 Apr 2014 14:30:02 +0000 (14:30 +0000)
This script retrieves the history of all V8 branches and trunk revisions and their corresponding Chromium revisions.

TEST=tools/push-to-trunk/releases.py -c <chrome path> --csv test.csv

BUG=
R=jarin@chromium.org

Review URL: https://codereview.chromium.org/227583002

git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@20629 ce2b1a6d-e550-0410-aec6-3dcde31c8c00

tools/push-to-trunk/common_includes.py
tools/push-to-trunk/git_recipes.py
tools/push-to-trunk/releases.py [new file with mode: 0755]
tools/push-to-trunk/test_scripts.py

index 44a6691741d9156592a1962e9d6e6ca0a18ef184..aaa25cfa79389e68a8563b62fa873c804e03b211 100644 (file)
@@ -39,6 +39,7 @@ import time
 import urllib2
 
 from git_recipes import GitRecipesMixin
+from git_recipes import GitFailedException
 
 PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME"
 TEMP_BRANCH = "TEMP_BRANCH"
@@ -216,10 +217,6 @@ class NoRetryException(Exception):
   pass
 
 
-class GitFailedException(Exception):
-  pass
-
-
 class Step(GitRecipesMixin):
   def __init__(self, text, requires, number, config, state, options, handler):
     self._text = text
index c0f0afbede5c17ba122ef1c56ab8a0afe81242b3..8c1e314d7d8c7e493750918295dd044fba4c9b0a 100644 (file)
 
 import re
 
+
+class GitFailedException(Exception):
+  pass
+
+
 def Strip(f):
   def new_f(*args, **kwargs):
     return f(*args, **kwargs).strip()
@@ -59,6 +64,13 @@ class GitRecipesMixin(object):
     assert name
     self.Git(MakeArgs(["branch -D", name]))
 
+  def GitReset(self, name):
+    assert name
+    self.Git(MakeArgs(["reset --hard", name]))
+
+  def GitRemotes(self):
+    return map(str.strip, self.Git(MakeArgs(["branch -r"])).splitlines())
+
   def GitCheckout(self, name):
     assert name
     self.Git(MakeArgs(["checkout -f", name]))
@@ -68,6 +80,26 @@ class GitRecipesMixin(object):
     assert branch_or_hash
     self.Git(MakeArgs(["checkout -f", branch_or_hash, "--", name]))
 
+  def GitCheckoutFileSafe(self, name, branch_or_hash):
+    try:
+      self.GitCheckoutFile(name, branch_or_hash)
+    except GitFailedException:  # pragma: no cover
+      # The file doesn't exist in that revision.
+      return False
+    return True
+
+  def GitChangedFiles(self, git_hash):
+    assert git_hash
+    try:
+      files = self.Git(MakeArgs(["diff --name-only",
+                                 git_hash,
+                                 "%s^" % git_hash]))
+      return map(str.strip, files.splitlines())
+    except GitFailedException:  # pragma: no cover
+      # Git fails using "^" at branch roots.
+      return []
+
+
   @Strip
   def GitCurrentBranch(self):
     for line in self.Git("status -s -b -uno").strip().splitlines():
@@ -99,6 +131,7 @@ class GitRecipesMixin(object):
     assert git_hash
     return self.Git(MakeArgs(["log", "-1", "-p", git_hash]))
 
+  # TODO(machenbach): Unused? Remove.
   def GitAdd(self, name):
     assert name
     self.Git(MakeArgs(["add", Quoted(name)]))
@@ -147,6 +180,7 @@ class GitRecipesMixin(object):
   def GitSVNFetch(self):
     self.Git("svn fetch")
 
+  # TODO(machenbach): Unused? Remove.
   @Strip
   def GitSVNLog(self):
     return self.Git("svn log -1 --oneline")
diff --git a/tools/push-to-trunk/releases.py b/tools/push-to-trunk/releases.py
new file mode 100755 (executable)
index 0000000..adbea2a
--- /dev/null
@@ -0,0 +1,384 @@
+#!/usr/bin/env python
+# Copyright 2014 the V8 project authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This script retrieves the history of all V8 branches and trunk revisions and
+# their corresponding Chromium revisions.
+
+import argparse
+import csv
+import itertools
+import json
+import os
+import re
+import sys
+
+from common_includes import *
+
+DEPS_FILE = "DEPS_FILE"
+CHROMIUM = "CHROMIUM"
+
+CONFIG = {
+  BRANCHNAME: "retrieve-v8-releases",
+  TEMP_BRANCH: "unused-branch", # TODO(machenbach): Remove from infrastructure.
+  PERSISTFILE_BASENAME: "/tmp/v8-releases-tempfile",
+  DOT_GIT_LOCATION: ".git",
+  VERSION_FILE: "src/version.cc",
+  DEPS_FILE: "DEPS",
+}
+
+# Expression for retrieving the bleeding edge revision from a commit message.
+PUSH_MESSAGE_RE = re.compile(r".* \(based on bleeding_edge revision r(\d+)\)$")
+
+# Expression for retrieving the merged patches from a merge commit message
+# (old and new format).
+MERGE_MESSAGE_RE = re.compile(r"^.*[M|m]erged (.+)(\)| into).*$", re.M)
+
+# Expression for retrieving reverted patches from a commit message (old and
+# new format).
+ROLLBACK_MESSAGE_RE = re.compile(r"^.*[R|r]ollback of (.+)(\)| in).*$", re.M)
+
+# Expression with three versions (historical) for extracting the v8 revision
+# from the chromium DEPS file.
+DEPS_RE = re.compile(r'^\s*(?:"v8_revision": "'
+                      '|\(Var\("googlecode_url"\) % "v8"\) \+ "\/trunk@'
+                      '|"http\:\/\/v8\.googlecode\.com\/svn\/trunk@)'
+                      '([0-9]+)".*$', re.M)
+
+
+def SortingKey(version):
+  """Key for sorting version number strings: '3.11' > '3.2.1.1'"""
+  version_keys = map(int, version.split("."))
+  # Fill up to full version numbers to normalize comparison.
+  while len(version_keys) < 4:
+    version_keys.append(0)
+  # Fill digits.
+  return ".".join(map("{0:03d}".format, version_keys))
+
+
+def SortBranches(branches):
+  """Sort branches with version number names."""
+  return sorted(branches, key=SortingKey, reverse=True)
+
+
+def FilterDuplicatesAndReverse(cr_releases):
+  """Returns the chromium releases in reverse order filtered by v8 revision
+  duplicates.
+
+  cr_releases is a list of [cr_rev, v8_rev] reverse-sorted by cr_rev.
+  """
+  last = ""
+  result = []
+  for release in reversed(cr_releases):
+    if last == release[1]:
+      continue
+    last = release[1]
+    result.append(release)
+  return result
+
+
+def BuildRevisionRanges(cr_releases):
+  """Returns a mapping of v8 revision -> chromium ranges.
+  The ranges are comma-separated, each range has the form R1:R2. The newest
+  entry is the only one of the form R1, as there is no end range.
+
+  cr_releases is a list of [cr_rev, v8_rev] reverse-sorted by cr_rev.
+  """
+  range_lists = {}
+  cr_releases = FilterDuplicatesAndReverse(cr_releases)
+
+  # Visit pairs of cr releases from oldest to newest.
+  for cr_from, cr_to in itertools.izip(
+      cr_releases, itertools.islice(cr_releases, 1, None)):
+
+    # Assume the chromium revisions are all different.
+    assert cr_from[0] != cr_to[0]
+
+    # TODO(machenbach): Subtraction is not git friendly.
+    ran = "%s:%d" % (cr_from[0], int(cr_to[0]) - 1)
+
+    # Collect the ranges in lists per revision.
+    range_lists.setdefault(cr_from[1], []).append(ran)
+
+  # Add the newest revision.
+  if cr_releases:
+    range_lists.setdefault(cr_releases[-1][1], []).append(cr_releases[-1][0])
+
+  # Stringify and comma-separate the range lists.
+  return dict((rev, ",".join(ran)) for rev, ran in range_lists.iteritems())
+
+
+def MatchSafe(match):
+  if match:
+    return match.group(1)
+  else:
+    return ""
+
+
+class Preparation(Step):
+  MESSAGE = "Preparation."
+
+  def RunStep(self):
+    self.CommonPrepare()
+    self.PrepareBranch()
+
+
+class RetrieveV8Releases(Step):
+  MESSAGE = "Retrieve all V8 releases."
+
+  def ExceedsMax(self, releases):
+    return (self._options.max_releases > 0
+            and len(releases) > self._options.max_releases)
+
+  def GetBleedingEdgeFromPush(self, title):
+    return MatchSafe(PUSH_MESSAGE_RE.match(title))
+
+  def GetMergedPatches(self, git_hash):
+    body = self.GitLog(n=1, format="%B", git_hash=git_hash)
+    patches = MatchSafe(MERGE_MESSAGE_RE.search(body))
+    if not patches:
+      patches = MatchSafe(ROLLBACK_MESSAGE_RE.search(body))
+      if patches:
+        # Indicate reverted patches with a "-".
+        patches = "-%s" % patches
+    return patches
+
+  def GetRelease(self, git_hash, branch):
+    self.ReadAndPersistVersion()
+    base_version = [self["major"], self["minor"], self["build"]]
+    version = ".".join(base_version)
+
+    patches = ""
+    if self["patch"] != "0":
+      version += ".%s" % self["patch"]
+      patches = self.GetMergedPatches(git_hash)
+
+    title = self.GitLog(n=1, format="%s", git_hash=git_hash)
+    return {
+      # The SVN revision on the branch.
+      "revision": self.GitSVNFindSVNRev(git_hash),
+      # The SVN revision on bleeding edge (only for newer trunk pushes).
+      "bleeding_edge": self.GetBleedingEdgeFromPush(title),
+      # The branch name.
+      "branch": branch,
+      # The version for displaying in the form 3.26.3 or 3.26.3.12.
+      "version": version,
+      # Merged patches if available in the form 'r1234, r2345'.
+      "patches_merged": patches,
+    }, self["patch"]
+
+  def GetReleasesFromBranch(self, branch):
+    self.GitReset("svn/%s" % branch)
+    releases = []
+    try:
+      for git_hash in self.GitLog(format="%H").splitlines():
+        if self._config[VERSION_FILE] not in self.GitChangedFiles(git_hash):
+          continue
+        if self.ExceedsMax(releases):
+          break  # pragma: no cover
+        if not self.GitCheckoutFileSafe(self._config[VERSION_FILE], git_hash):
+          break  # pragma: no cover
+
+        release, patch_level = self.GetRelease(git_hash, branch)
+        releases.append(release)
+
+        # Follow branches only until their creation point.
+        # TODO(machenbach): This omits patches if the version file wasn't
+        # manipulated correctly. Find a better way to detect the point where
+        # the parent of the branch head leads to the trunk branch.
+        if branch != "trunk" and patch_level == "0":
+          break
+
+    # Allow Ctrl-C interrupt.
+    except (KeyboardInterrupt, SystemExit):  # pragma: no cover
+      pass
+
+    # Clean up checked-out version file.
+    self.GitCheckoutFileSafe(self._config[VERSION_FILE], "HEAD")
+    return releases
+
+  def RunStep(self):
+    self.GitCreateBranch(self._config[BRANCHNAME])
+    # Get relevant remote branches, e.g. "svn/3.25".
+    branches = filter(lambda s: re.match(r"^svn/\d+\.\d+$", s),
+                      self.GitRemotes())
+    # Remove 'svn/' prefix.
+    branches = map(lambda s: s[4:], branches)
+
+    releases = []
+    if self._options.branch == 'recent':
+      # Get only recent development on trunk, beta and stable.
+      if self._options.max_releases == 0:  # pragma: no cover
+        self._options.max_releases = 10
+      beta, stable = SortBranches(branches)[0:2]
+      releases += self.GetReleasesFromBranch(stable)
+      releases += self.GetReleasesFromBranch(beta)
+      releases += self.GetReleasesFromBranch("trunk")
+    elif self._options.branch == 'all':  # pragma: no cover
+      # Retrieve the full release history.
+      for branch in branches:
+        releases += self.GetReleasesFromBranch(branch)
+      releases += self.GetReleasesFromBranch("trunk")
+    else:  # pragma: no cover
+      # Retrieve history for a specified branch.
+      assert self._options.branch in branches + ["trunk"]
+      releases += self.GetReleasesFromBranch(self._options.branch)
+
+    self["releases"] = sorted(releases,
+                              key=lambda r: SortingKey(r["version"]),
+                              reverse=True)
+
+
+# TODO(machenbach): Parts of the Chromium setup are c/p from the chromium_roll
+# script -> unify.
+class CheckChromium(Step):
+  MESSAGE = "Check the chromium checkout."
+
+  def Run(self):
+    self["chrome_path"] = self._options.chromium
+
+
+class SwitchChromium(Step):
+  MESSAGE = "Switch to Chromium checkout."
+  REQUIRES = "chrome_path"
+
+  def RunStep(self):
+    self["v8_path"] = os.getcwd()
+    os.chdir(self["chrome_path"])
+    # Check for a clean workdir.
+    if not self.GitIsWorkdirClean():  # pragma: no cover
+      self.Die("Workspace is not clean. Please commit or undo your changes.")
+    # Assert that the DEPS file is there.
+    if not os.path.exists(self.Config(DEPS_FILE)):  # pragma: no cover
+      self.Die("DEPS file not present.")
+
+
+class UpdateChromiumCheckout(Step):
+  MESSAGE = "Update the checkout and create a new branch."
+  REQUIRES = "chrome_path"
+
+  def RunStep(self):
+    os.chdir(self["chrome_path"])
+    self.GitCheckout("master")
+    self.GitPull()
+    self.GitCreateBranch(self.Config(BRANCHNAME))
+
+
+class RetrieveChromiumV8Releases(Step):
+  MESSAGE = "Retrieve V8 releases from Chromium DEPS."
+  REQUIRES = "chrome_path"
+
+  def RunStep(self):
+    os.chdir(self["chrome_path"])
+
+    trunk_releases = filter(lambda r: r["branch"] == "trunk", self["releases"])
+    if not trunk_releases:  # pragma: no cover
+      print "No trunk releases detected. Skipping chromium history."
+      return True
+
+    oldest_v8_rev = int(trunk_releases[-1]["revision"])
+
+    cr_releases = []
+    try:
+      for git_hash in self.GitLog(format="%H", grep="V8").splitlines():
+        if self._config[DEPS_FILE] not in self.GitChangedFiles(git_hash):
+          continue
+        if not self.GitCheckoutFileSafe(self._config[DEPS_FILE], git_hash):
+          break  # pragma: no cover
+        deps = FileToText(self.Config(DEPS_FILE))
+        match = DEPS_RE.search(deps)
+        if match:
+          svn_rev = self.GitSVNFindSVNRev(git_hash)
+          v8_rev = match.group(1)
+          cr_releases.append([svn_rev, v8_rev])
+
+          # Stop after reaching beyond the last v8 revision we want to update.
+          # We need a small buffer for possible revert/reland frenzies.
+          # TODO(machenbach): Subtraction is not git friendly.
+          if int(v8_rev) < oldest_v8_rev - 100:
+            break  # pragma: no cover
+
+    # Allow Ctrl-C interrupt.
+    except (KeyboardInterrupt, SystemExit):  # pragma: no cover
+      pass
+
+    # Clean up.
+    self.GitCheckoutFileSafe(self._config[DEPS_FILE], "HEAD")
+
+    # Add the chromium ranges to the v8 trunk releases.
+    all_ranges = BuildRevisionRanges(cr_releases)
+    trunk_dict = dict((r["revision"], r) for r in trunk_releases)
+    for revision, ranges in all_ranges.iteritems():
+      trunk_dict.get(revision, {})["chromium_revision"] = ranges
+
+class SwitchV8(Step):
+  MESSAGE = "Returning to V8 checkout."
+  REQUIRES = "chrome_path"
+
+  def RunStep(self):
+    self.GitCheckout("master")
+    self.GitDeleteBranch(self.Config(BRANCHNAME))
+    os.chdir(self["v8_path"])
+
+
+class CleanUp(Step):
+  MESSAGE = "Clean up."
+
+  def RunStep(self):
+    self.CommonCleanup()
+
+
+class WriteOutput(Step):
+  MESSAGE = "Print output."
+
+  def Run(self):
+    if self._options.csv:
+      with open(self._options.csv, "w") as f:
+        writer = csv.DictWriter(f,
+                                ["version", "branch", "revision",
+                                 "chromium_revision", "patches_merged"],
+                                restval="",
+                                extrasaction="ignore")
+        for release in self["releases"]:
+          writer.writerow(release)
+    if self._options.json:
+      with open(self._options.json, "w") as f:
+        f.write(json.dumps(self["releases"]))
+    if not self._options.csv and not self._options.json:
+      print self["releases"]  # pragma: no cover
+
+
+class Releases(ScriptsBase):
+  def _PrepareOptions(self, parser):
+    parser.add_argument("-b", "--branch", default="recent",
+                        help=("The branch to analyze. If 'all' is specified, "
+                              "analyze all branches. If 'recent' (default) "
+                              "is specified, track beta, stable and trunk."))
+    parser.add_argument("-c", "--chromium",
+                        help=("The path to your Chromium src/ "
+                              "directory to automate the V8 roll."))
+    parser.add_argument("--csv", help="Path to a CSV file for export.")
+    parser.add_argument("-m", "--max-releases", type=int, default=0,
+                        help="The maximum number of releases to track.")
+    parser.add_argument("--json", help="Path to a JSON file for export.")
+
+  def _ProcessOptions(self, options):  # pragma: no cover
+    return True
+
+  def _Steps(self):
+    return [
+      Preparation,
+      RetrieveV8Releases,
+      CheckChromium,
+      SwitchChromium,
+      UpdateChromiumCheckout,
+      RetrieveChromiumV8Releases,
+      SwitchV8,
+      CleanUp,
+      WriteOutput,
+    ]
+
+
+if __name__ == "__main__":  # pragma: no cover
+  sys.exit(Releases(CONFIG).Run())
index 2df45300197a4c7b120123fbb2db983ded565fc3..34f4c9326d84167ed966efbd321078ecd52b7710 100644 (file)
@@ -45,6 +45,8 @@ import chromium_roll
 from chromium_roll import CHROMIUM
 from chromium_roll import DEPS_FILE
 from chromium_roll import ChromiumRoll
+import releases
+from releases import Releases
 
 
 TEST_CONFIG = {
@@ -75,6 +77,38 @@ AUTO_PUSH_ARGS = [
 
 
 class ToplevelTest(unittest.TestCase):
+  def testSortBranches(self):
+    S = releases.SortBranches
+    self.assertEquals(["3.1", "2.25"], S(["2.25", "3.1"])[0:2])
+    self.assertEquals(["3.0", "2.25"], S(["2.25", "3.0", "2.24"])[0:2])
+    self.assertEquals(["3.11", "3.2"], S(["3.11", "3.2", "2.24"])[0:2])
+
+  def testFilterDuplicatesAndReverse(self):
+    F = releases.FilterDuplicatesAndReverse
+    self.assertEquals([], F([]))
+    self.assertEquals([["100", "10"]], F([["100", "10"]]))
+    self.assertEquals([["99", "9"], ["100", "10"]],
+                      F([["100", "10"], ["99", "9"]]))
+    self.assertEquals([["98", "9"], ["100", "10"]],
+                      F([["100", "10"], ["99", "9"], ["98", "9"]]))
+    self.assertEquals([["98", "9"], ["99", "10"]],
+                      F([["100", "10"], ["99", "10"], ["98", "9"]]))
+
+  def testBuildRevisionRanges(self):
+    B = releases.BuildRevisionRanges
+    self.assertEquals({}, B([]))
+    self.assertEquals({"10": "100"}, B([["100", "10"]]))
+    self.assertEquals({"10": "100", "9": "99:99"},
+                      B([["100", "10"], ["99", "9"]]))
+    self.assertEquals({"10": "100", "9": "97:99"},
+                      B([["100", "10"], ["98", "9"], ["97", "9"]]))
+    self.assertEquals({"10": "100", "9": "99:99", "3": "91:98"},
+                      B([["100", "10"], ["99", "9"], ["91", "3"]]))
+    self.assertEquals({"13": "101", "12": "100:100", "9": "94:97",
+                       "3": "91:93,98:99"},
+                      B([["101", "13"], ["100", "12"], ["98", "3"],
+                         ["94", "9"], ["91", "3"]]))
+
   def testMakeComment(self):
     self.assertEquals("#   Line 1\n#   Line 2\n#",
                       MakeComment("    Line 1\n    Line 2\n"))
@@ -297,14 +331,14 @@ class ScriptTest(unittest.TestCase):
     self._tmp_files.append(name)
     return name
 
-  def WriteFakeVersionFile(self, build=4):
+  def WriteFakeVersionFile(self, minor=22, build=4, patch=0):
     with open(TEST_CONFIG[VERSION_FILE], "w") as f:
       f.write("  // Some line...\n")
       f.write("\n")
       f.write("#define MAJOR_VERSION    3\n")
-      f.write("#define MINOR_VERSION    22\n")
+      f.write("#define MINOR_VERSION    %s\n" % minor)
       f.write("#define BUILD_NUMBER     %s\n" % build)
-      f.write("#define PATCH_LEVEL      0\n")
+      f.write("#define PATCH_LEVEL      %s\n" % patch)
       f.write("  // Some line...\n")
       f.write("#define IS_CANDIDATE_VERSION 0\n")
 
@@ -1116,6 +1150,107 @@ LOG=N
     args += ["-s", "3"]
     MergeToBranch(TEST_CONFIG, self).Run(args)
 
+  def testReleases(self):
+    json_output = self.MakeEmptyTempFile()
+    csv_output = self.MakeEmptyTempFile()
+    TEST_CONFIG[VERSION_FILE] = self.MakeEmptyTempFile()
+    self.WriteFakeVersionFile()
+
+    TEST_CONFIG[DOT_GIT_LOCATION] = self.MakeEmptyTempFile()
+    if not os.path.exists(TEST_CONFIG[CHROMIUM]):
+      os.makedirs(TEST_CONFIG[CHROMIUM])
+    def WriteDEPS(revision):
+      TextToFile("Line\n   \"v8_revision\": \"%s\",\n  line\n" % revision,
+                 TEST_CONFIG[DEPS_FILE])
+    WriteDEPS(567)
+
+    def ResetVersion(minor, build, patch=0):
+      return lambda: self.WriteFakeVersionFile(minor=minor,
+                                               build=build,
+                                               patch=patch)
+
+    def ResetDEPS(revision):
+      return lambda: WriteDEPS(revision)
+
+    self.ExpectGit([
+      Git("status -s -uno", ""),
+      Git("status -s -b -uno", "## some_branch\n"),
+      Git("svn fetch", ""),
+      Git("branch", "  branch1\n* branch2\n"),
+      Git("checkout -b %s" % TEST_CONFIG[TEMP_BRANCH], ""),
+      Git("branch", "  branch1\n* branch2\n"),
+      Git("checkout -b %s" % TEST_CONFIG[BRANCHNAME], ""),
+      Git("branch -r", "  svn/3.21\n  svn/3.3\n"),
+      Git("reset --hard svn/3.3", ""),
+      Git("log --format=%H", "hash1\nhash2"),
+      Git("diff --name-only hash1 hash1^", ""),
+      Git("diff --name-only hash2 hash2^", TEST_CONFIG[VERSION_FILE]),
+      Git("checkout -f hash2 -- %s" % TEST_CONFIG[VERSION_FILE], "",
+          cb=ResetVersion(3, 1, 1)),
+      Git("log -1 --format=%B hash2", "Version 3.3.1.1 (merged 12)"),
+      Git("log -1 --format=%s hash2", ""),
+      Git("svn find-rev hash2", "234"),
+      Git("checkout -f HEAD -- %s" % TEST_CONFIG[VERSION_FILE], "",
+          cb=ResetVersion(22, 5)),
+      Git("reset --hard svn/3.21", ""),
+      Git("log --format=%H", "hash3\nhash4\nhash5\n"),
+      Git("diff --name-only hash3 hash3^", TEST_CONFIG[VERSION_FILE]),
+      Git("checkout -f hash3 -- %s" % TEST_CONFIG[VERSION_FILE], "",
+          cb=ResetVersion(21, 2)),
+      Git("log -1 --format=%s hash3", ""),
+      Git("svn find-rev hash3", "123"),
+      Git("checkout -f HEAD -- %s" % TEST_CONFIG[VERSION_FILE], "",
+          cb=ResetVersion(22, 5)),
+      Git("reset --hard svn/trunk", ""),
+      Git("log --format=%H", "hash6\n"),
+      Git("diff --name-only hash6 hash6^", TEST_CONFIG[VERSION_FILE]),
+      Git("checkout -f hash6 -- %s" % TEST_CONFIG[VERSION_FILE], "",
+          cb=ResetVersion(22, 3)),
+      Git("log -1 --format=%s hash6", ""),
+      Git("svn find-rev hash6", "345"),
+      Git("checkout -f HEAD -- %s" % TEST_CONFIG[VERSION_FILE], "",
+          cb=ResetVersion(22, 5)),
+      Git("status -s -uno", ""),
+      Git("checkout -f master", ""),
+      Git("pull", ""),
+      Git("checkout -b %s" % TEST_CONFIG[BRANCHNAME], ""),
+      Git("log --format=%H --grep=\"V8\"", "c_hash1\nc_hash2\n"),
+      Git("diff --name-only c_hash1 c_hash1^", ""),
+      Git("diff --name-only c_hash2 c_hash2^", TEST_CONFIG[DEPS_FILE]),
+      Git("checkout -f c_hash2 -- %s" % TEST_CONFIG[DEPS_FILE], "",
+          cb=ResetDEPS(345)),
+      Git("svn find-rev c_hash2", "4567"),
+      Git("checkout -f HEAD -- %s" % TEST_CONFIG[DEPS_FILE], "",
+          cb=ResetDEPS(567)),
+      Git("checkout -f master", ""),
+      Git("branch -D %s" % TEST_CONFIG[BRANCHNAME], ""),
+      Git("checkout -f some_branch", ""),
+      Git("branch -D %s" % TEST_CONFIG[TEMP_BRANCH], ""),
+      Git("branch -D %s" % TEST_CONFIG[BRANCHNAME], ""),
+    ])
+
+    args = ["-c", TEST_CONFIG[CHROMIUM],
+            "--json", json_output,
+            "--csv", csv_output,
+            "--max-releases", "1"]
+    Releases(TEST_CONFIG, self).Run(args)
+
+    # Check expected output.
+    csv = ("3.22.3,trunk,345,4567,\r\n"
+           "3.21.2,3.21,123,,\r\n"
+           "3.3.1.1,3.3,234,,12\r\n")
+    self.assertEquals(csv, FileToText(csv_output))
+
+    expected_json = [
+      {"bleeding_edge": "", "patches_merged": "", "version": "3.22.3",
+       "chromium_revision": "4567", "branch": "trunk", "revision": "345"},
+      {"patches_merged": "", "bleeding_edge": "", "version": "3.21.2",
+       "branch": "3.21", "revision": "123"},
+      {"patches_merged": "12", "bleeding_edge": "", "version": "3.3.1.1",
+       "branch": "3.3", "revision": "234"}
+    ]
+    self.assertEquals(expected_json, json.loads(FileToText(json_output)))
+
 
 class SystemTest(unittest.TestCase):
   def testReload(self):