15bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
25bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone# Use of this source code is governed by a BSD-style license that can be
35bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone# found in the LICENSE file.
45bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
5c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnetteimport glob
6c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnetteimport logging
7c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnetteimport os
8c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnetteimport re
9c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnetteimport time
1067f06d6003bb11f03f925810272f595d79dce44aChris Masonefrom distutils import version
115bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
12c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnetteimport common
13c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnettefrom autotest_lib.client.common_lib import autotemp
14c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnettefrom autotest_lib.client.common_lib import error
15c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnettefrom autotest_lib.client.common_lib import utils
16c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette
175bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
185bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masoneclass ManifestVersionsException(Exception):
195bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    """Base class for exceptions from this package."""
205bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    pass
215bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
225bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
235bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masoneclass QueryException(ManifestVersionsException):
245bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    """Raised to indicate a failure while searching for manifests."""
255bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    pass
265bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
275bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
28c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnetteclass CloneException(ManifestVersionsException):
29c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette    """Raised when `git clone` fails to create the repository."""
30c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette    pass
31c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette
32c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette
3328424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masonedef _SystemOutput(command, timeout=None, args=()):
3428424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    """Shell out to run a command, expecting data on stderr. Return stdout.
3528424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone
3628424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    Shells out to run |command|, optionally passing escaped |args|.
3728424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    Instead of logging stderr at ERROR level, will log at default
3828424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    stdout log level.  Normal stdout is returned.
3928424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone
4028424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    @param command: command string to execute.
4128424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    @param timeout: time limit in seconds before attempting to kill the
4228424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone            running process. The function will take a few seconds longer
4328424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone            than 'timeout' to complete if it has to kill the process.
4428424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    @param args: sequence of strings of arguments to be given to the command
4528424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone            inside " quotes after they have been escaped for that; each
4628424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone            element in the sequence will be given as a separate command
4728424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone            argument.
4828424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone
4928424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    @return a string with the stdout output of the command.
5028424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    """
5128424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    out = utils.run(command, timeout=timeout, ignore_status=False,
5228424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone                    stderr_is_expected=True, args=args).stdout
5328424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    return out.rstrip('\n')
5428424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone
5528424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone
5628424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masonedef _System(command, timeout=None):
5728424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    """Run a command, expecting data on stderr.
5828424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone
5928424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    @param command: command string to execute.
6028424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    @param timeout: timeout in seconds
6128424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    """
6228424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone    utils.run(command, timeout=timeout, ignore_status=False,
6328424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone              stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
6428424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone              stderr_is_expected=True)
6528424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone
6628424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone
675bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masoneclass ManifestVersions(object):
685bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    """Class to allow discovery of manifests for new successful CrOS builds.
695bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
70f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian    Manifest versions is a repository that contains information on which
71f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian    builds passed/failed. This class is responsible for keeping a temp
72f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian    copy of the repository up to date.
73f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian
74c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette    @var _CLONE_RETRY_SECONDS: Number of seconds to wait before retrying
75c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette                                a failed `git clone` operation.
76c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette    @var _CLONE_MAX_RETRIES: Maximum number of times to retry a failed
77c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette                             a failed `git clone` operation.
785bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    @var _MANIFEST_VERSIONS_URL: URL of the internal manifest-versions git repo.
795bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    @var _BOARD_MANIFEST_GLOB_PATTERN: pattern for shell glob for passed-build
805bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone                                       manifests for a given board.
815bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    @var _BOARD_MANIFEST_RE_PATTERN: pattern for regex that parses paths to
825bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone                                     manifests for a given board.
835bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
845bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    @var _git: absolute path of git binary.
855bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    @var _tempdir: a scoped tempdir.  Will be destroyed on instance deletion.
865bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    """
875bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
88c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette    _CLONE_RETRY_SECONDS = 5 * 60
89c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette    _CLONE_MAX_RETRIES = 60 * 60 / _CLONE_RETRY_SECONDS
903397c7c94475a37ec14a0e5648b1453cf5018aa3beeps    _MANIFEST_VERSIONS_URL = ('https://chrome-internal-review.googlesource.com/'
915bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone                              'chromeos/manifest-versions.git')
9293f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone    _ANY_MANIFEST_GLOB_PATTERN = 'build-name/*/pass/'
935bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    _BOARD_MANIFEST_GLOB_PATTERN = 'build-name/%s-*/pass/'
940d00357df2bb69ac799fa1ac6ca81bb68b137c78Alex Miller    _BOARD_MANIFEST_RE_PATTERN = (r'build-name/%s-([^-]+)'
95c7960d16423e63510f105c007e0e22b5a1d4a01fAlex Miller                                  r'(?:-group)?/pass/(\d+)/([0-9.]+)\.xml')
96a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi    _BOARD_BRANCH_MANIFEST_GLOB_PATTERN = 'build-name/%s-%s/pass/'
975bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
985bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
99f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian    def __init__(self, tmp_repo_dir=None):
100f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian        """Create a manifest versions manager.
101f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian
102f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian        @param tmp_repo_dir: For use in testing, if one does not wish to repeatedly
103f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian            clone the manifest versions repo that is currently a few GB in size.
104f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian        """
10528424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone        self._git = _SystemOutput('which git')
106f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian        if tmp_repo_dir:
107f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian            self._tempdir = autotemp.dummy_dir(tmp_repo_dir)
108f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian        else:
109f571aa6b964930e4cd7ea0569105bebe06b97070Prashanth Balasubramanian            self._tempdir = autotemp.tempdir(unique_id='_suite_scheduler')
1105bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
1115bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
11293f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone    def AnyManifestsSinceRev(self, revision):
113a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        """Determine if any builds passed since git |revision|.
114a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi
115a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        @param revision: the git revision to look back to.
116a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        @return True if any builds have passed; False otherwise.
117a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        """
118a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        manifest_paths = self._ExpandGlobMinusPrefix(
119a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                self._tempdir.name, self._ANY_MANIFEST_GLOB_PATTERN)
120a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        if not manifest_paths:
121a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi            logging.error('No paths to check for manifests???')
122a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi            return False
123a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        logging.info('Checking if any manifests landed since %s', revision)
124a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        log_cmd = self._BuildCommand('log',
125a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                                     revision + '..HEAD',
126a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                                     '--pretty="format:%H"',
127a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                                     '--',
128a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                                     ' '.join(manifest_paths))
129a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        return _SystemOutput(log_cmd).strip() != ''
13093f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
13193f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
13267f06d6003bb11f03f925810272f595d79dce44aChris Masone    def Initialize(self):
13367f06d6003bb11f03f925810272f595d79dce44aChris Masone        """Set up internal state.  Must be called before other methods.
13467f06d6003bb11f03f925810272f595d79dce44aChris Masone
135c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        Clone manifest-versions.git into tempdir managed by this instance.
13667f06d6003bb11f03f925810272f595d79dce44aChris Masone        """
137c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        # If gerrit goes down during suite_scheduler operation,
138c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        # we'll enter a loop like the following:
139c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        #  1. suite_scheduler fails executing some `git` command.
140c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        #  2. The failure is logged at ERROR level, causing an
141c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        #     e-mail notification of the failure.
142c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        #  3. suite_scheduler terminates.
143c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        #  4. Upstart respawns suite_scheduler.
144c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        #  5. suite_scheduler comes here to restart with a new
145c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        #     manifest-versions repo.
146c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        #  6. `git clone` fails, and we go back to step 2.
147c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        #
148c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        # We want to rate limit the e-mail notifications, so we
149c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        # retry failed `git clone` operations for a time before we
150c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        # finally give up.
151c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        retry_count = 0
152c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        msg = None
153c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        while retry_count <= self._CLONE_MAX_RETRIES:
154c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette            if retry_count:
155c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette                time.sleep(self._CLONE_RETRY_SECONDS)
156c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette            retry_count += 1
157c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette            try:
158c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette                logging.debug('Cloning manifest-versions.git,'
159c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette                              ' attempt %d.', retry_count)
160c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette                self._Clone()
161c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette                logging.debug('manifest-versions.git cloned.')
162c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette                return
163c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette            except error.CmdError as e:
164c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette                msg = str(e)
165c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette                logging.debug('Clone failed: %s', msg)
166c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette        raise CloneException('Failed to clone %s after %d attempts: %s' %
167c9c2a8620c256fa17d16e6ddfa9a007a63f79fe2J. Richard Barnette                             (self._MANIFEST_VERSIONS_URL, retry_count, msg))
16867f06d6003bb11f03f925810272f595d79dce44aChris Masone
16967f06d6003bb11f03f925810272f595d79dce44aChris Masone
17033ea03b85c3f3b40be2813b4e5a87c8a2f3e9f8dFang Deng    def ManifestsSinceDate(self, since_date, board):
17133ea03b85c3f3b40be2813b4e5a87c8a2f3e9f8dFang Deng        """Return map of branch:manifests for |board| since |since_date|.
17267f06d6003bb11f03f925810272f595d79dce44aChris Masone
17367f06d6003bb11f03f925810272f595d79dce44aChris Masone        To fully specify a 'branch', one needs both the type and the numeric
17467f06d6003bb11f03f925810272f595d79dce44aChris Masone        milestone the branch was cut for, e.g. ('release', '19') or
17567f06d6003bb11f03f925810272f595d79dce44aChris Masone        ('factory', '17').
17667f06d6003bb11f03f925810272f595d79dce44aChris Masone
17733ea03b85c3f3b40be2813b4e5a87c8a2f3e9f8dFang Deng        @param since_date: a datetime object, return all manifest files
17833ea03b85c3f3b40be2813b4e5a87c8a2f3e9f8dFang Deng                           since |since_date|
17967f06d6003bb11f03f925810272f595d79dce44aChris Masone        @param board: the board whose manifests we want to check for.
18067f06d6003bb11f03f925810272f595d79dce44aChris Masone        @return {(branch_type, milestone): [manifests, oldest, to, newest]}
18167f06d6003bb11f03f925810272f595d79dce44aChris Masone        """
18293f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        return self._GetManifests(
18393f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone            re.compile(self._BOARD_MANIFEST_RE_PATTERN % board),
18433ea03b85c3f3b40be2813b4e5a87c8a2f3e9f8dFang Deng            self._QueryManifestsSinceDate(since_date, board))
18593f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
18693f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
18793f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone    def ManifestsSinceRev(self, rev, board):
18893f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        """Return map of branch:manifests for |board| since git |rev|.
18993f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
19093f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        To fully specify a 'branch', one needs both the type and the numeric
19193f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        milestone the branch was cut for, e.g. ('release', '19') or
19293f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        ('factory', '17').
19393f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
19493f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        @param rev: return all manifest files from |rev| up to HEAD.
19593f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        @param board: the board whose manifests we want to check for.
19693f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        @return {(branch_type, milestone): [manifests, oldest, to, newest]}
19793f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        """
19893f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        return self._GetManifests(
19993f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone            re.compile(self._BOARD_MANIFEST_RE_PATTERN % board),
20093f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone            self._QueryManifestsSinceRev(rev, board))
20193f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
20293f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
203a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi    def GetLatestManifest(self, board, build_type, milestone=None):
204a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        """Get the latest manifest of a given board and type.
205a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi
206a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        @param board: the board whose manifests we want to check for.
207a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        @param build_type: Type of a build, e.g., release, factory or firmware.
208a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        @param milestone: Milestone to look for the latest manifest. Default to
209a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                          None, i.e., use the latest milestone.
210a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi
211a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        @return: (milestone, manifest), e.g., (46, '7268.0.0')
212a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi
213a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        """
214a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        milestones_folder = os.path.join(
215a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                self._tempdir.name,
216a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                self._BOARD_BRANCH_MANIFEST_GLOB_PATTERN % (board, build_type))
217a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        if not milestone:
218a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi            try:
219a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                milestone_names = os.listdir(milestones_folder)
220a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi            except OSError:
221a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                milestone_names = None
222a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi            if not milestone_names:
223a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                raise QueryException('There is no milestone existed in %s.' %
224a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                                     milestones_folder)
225a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi            milestone = max([m for m in milestone_names if m.isdigit()])
226a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        manifests_folder = os.path.join(milestones_folder, str(milestone))
227a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        manifests = [m.strip('.xml') for m in  os.listdir(manifests_folder)
228a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                     if m.endswith('.xml')]
229a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        if not manifests:
230a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi            raise QueryException('There is no build existed in %s.' %
231a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                                 manifests_folder)
232a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        manifests.sort(key=version.LooseVersion)
233a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi        return milestone, manifests[-1]
234a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi
235a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi
23693f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone    def _GetManifests(self, matcher, manifest_paths):
23793f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        """Parse a list of manifest_paths into a map of branch:manifests.
23893f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
23993f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        Given a regexp |matcher| and a list of paths to manifest files,
24093f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        parse the paths and build up a map of branches to manifests of
24193f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        builds on those branches.
24293f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
24393f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        To fully specify a 'branch', one needs both the type and the numeric
24493f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        milestone the branch was cut for, e.g. ('release', '19') or
24593f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        ('factory', '17').
24693f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
24793f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        @param matcher: a compiled regexp that can be used to parse info
24893f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone                        out of the path to a manifest file.
24993f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        @param manifest_paths: an iterable of paths to manifest files.
25093f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        @return {(branch_type, milestone): [manifests, oldest, to, newest]}
25193f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        """
25267f06d6003bb11f03f925810272f595d79dce44aChris Masone        branch_manifests = {}
25393f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        for manifest_path in manifest_paths:
2549273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone            logging.debug('parsing manifest path %s', manifest_path)
2550d00357df2bb69ac799fa1ac6ca81bb68b137c78Alex Miller            match = matcher.match(manifest_path)
2560d00357df2bb69ac799fa1ac6ca81bb68b137c78Alex Miller            if not match:
25752ac9370d76ce669435589b71d0fdc57ef85535bFang Deng                logging.warning('Failed to parse path %s, regex: %s',
258a25e0d48f2c15b7c4d430b671aaee9cb56c8fb1eDan Shi                                manifest_path, matcher.pattern)
2590d00357df2bb69ac799fa1ac6ca81bb68b137c78Alex Miller                continue
2600d00357df2bb69ac799fa1ac6ca81bb68b137c78Alex Miller            groups = match.groups()
261cdcc0ee70346096cbc88b2bef20daab1cb0dc4d6Alex Miller            config_type, milestone, manifest = groups
262cdcc0ee70346096cbc88b2bef20daab1cb0dc4d6Alex Miller            branch = branch_manifests.setdefault((config_type, milestone), [])
263cdcc0ee70346096cbc88b2bef20daab1cb0dc4d6Alex Miller            branch.append(manifest)
26467f06d6003bb11f03f925810272f595d79dce44aChris Masone        for manifest_list in branch_manifests.itervalues():
26567f06d6003bb11f03f925810272f595d79dce44aChris Masone            manifest_list.sort(key=version.LooseVersion)
26667f06d6003bb11f03f925810272f595d79dce44aChris Masone        return branch_manifests
26767f06d6003bb11f03f925810272f595d79dce44aChris Masone
26867f06d6003bb11f03f925810272f595d79dce44aChris Masone
26993f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone    def GetCheckpoint(self):
27093f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        """Return the latest checked-out git revision in manifest-versions.git.
27193f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
27293f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        @return the git hash of the latest git revision.
27393f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        """
27428424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone        return _SystemOutput(self._BuildCommand('log',
27528424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone                                                '--pretty="format:%H"',
27628424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone                                                '--max-count=1')).strip()
27793f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
27893f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone
27967f06d6003bb11f03f925810272f595d79dce44aChris Masone    def Update(self):
28067f06d6003bb11f03f925810272f595d79dce44aChris Masone        """Get latest manifest information."""
28128424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone        return _System(self._BuildCommand('pull'))
28267f06d6003bb11f03f925810272f595d79dce44aChris Masone
28367f06d6003bb11f03f925810272f595d79dce44aChris Masone
2845bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    def _BuildCommand(self, command, *args):
2855bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        """Build a git CLI |command|, passing space-delineated |args|.
2865bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
2875bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        @param command: the git sub-command to use.
2885bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        @param args: args for the git sub-command.  Will be space-delineated.
2895bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        @return a string with the above formatted into it.
2905bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        """
291bb3388570efebf3f3d07b9d71238190ce9fe0088Chris Masone        return '%s --git-dir=%s --work-tree=%s %s %s' % (
2925bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone            self._git, os.path.join(self._tempdir.name, '.git'),
293bb3388570efebf3f3d07b9d71238190ce9fe0088Chris Masone            self._tempdir.name, command, ' '.join(args))
2945bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
2955bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
2965bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    def _Clone(self):
2975bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        """Clone self._MANIFEST_VERSIONS_URL into a local temp dir."""
2985bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        # Can't use --depth here because the internal gerrit server doesn't
2995bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        # support it.  Wish we could.  http://crosbug.com/29047
300bb3388570efebf3f3d07b9d71238190ce9fe0088Chris Masone        # Also, note that --work-tree and --git-dir interact oddly with
301bb3388570efebf3f3d07b9d71238190ce9fe0088Chris Masone        # 'git clone', so we don't use them.
30228424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone        _System('%s clone %s %s' % (self._git,
30328424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone                                    self._MANIFEST_VERSIONS_URL,
30428424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone                                    self._tempdir.name))
3055bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
3065bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
3075bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    def _ShowCmd(self):
3085bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        """Return a git command that shows file names added by commits."""
3095bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        return self._BuildCommand('show',
3105bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone                                  '--pretty="format:"',
3115bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone                                  '--name-only',
3125bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone                                  '--diff-filter=A')
3135bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
3145bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
31593f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone    def _QueryManifestsSinceRev(self, git_rev, board):
31693f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        """Get manifest filenames for |board|, since |git_rev|.
3175bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
31893f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        @param git_rev: check for manifests newer than this git commit.
3195bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        @param board: the board whose manifests we want to check for.
3205bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        @return whitespace-delineated
3215bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        @raise QueryException if errors occur.
3225bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        """
32393f51d4da152538007d5b44a2dc9d2bbb1fe3429Chris Masone        return self._QueryManifestsSince(git_rev + '..HEAD', board)
3245bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
3255bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
32633ea03b85c3f3b40be2813b4e5a87c8a2f3e9f8dFang Deng    def _QueryManifestsSinceDate(self, since_date, board):
32733ea03b85c3f3b40be2813b4e5a87c8a2f3e9f8dFang Deng        """Return list of manifest files for |board| since |since_date|.
3285bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
32933ea03b85c3f3b40be2813b4e5a87c8a2f3e9f8dFang Deng        @param sync_date: a datetime object, return all manifest files
33033ea03b85c3f3b40be2813b4e5a87c8a2f3e9f8dFang Deng                          since |since_date|.
3315bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        @param board: the board whose manifests we want to check for.
3325bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        @raise QueryException if errors occur.
3335bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        """
33433ea03b85c3f3b40be2813b4e5a87c8a2f3e9f8dFang Deng        return self._QueryManifestsSince('--since="%s"' % since_date, board)
3355bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
3365bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
3379273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone    def _ExpandGlobMinusPrefix(self, prefix, path_glob):
3389273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        """Expand |path_glob| under dir |prefix|, then remove |prefix|.
3399273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone
3409273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        Path-concatenate prefix and path_glob, then expand the resulting glob.
3419273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        Take the results and remove |prefix| (and path separator) from each.
3429273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        Return the resulting list.
3439273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone
3449273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        Assuming /tmp/foo/baz and /tmp/bar/baz both exist,
3459273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        _ExpandGlobMinusPrefix('/tmp', '*/baz')  # ['bar/baz', 'foo/baz']
3469273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone
3479273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        @param prefix: a path under which to expand |path_glob|.
3489273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        @param path_glob: the glob to expand.
3499273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        @return a list of paths relative to |prefix|, based on |path_glob|.
3509273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        """
3519273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        full_glob = os.path.join(prefix, path_glob)
3529273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        return [p[len(prefix)+1:] for p in glob.iglob(full_glob)]
3539273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone
3549273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone
3555bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone    def _QueryManifestsSince(self, since_spec, board):
3565bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        """Return list of manifest files for |board|, since |since_spec|.
3575bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone
3585bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        @param since_spec: a formatted arg to git log that specifies a starting
3595bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone                           point to list commits from, e.g.
3605bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone                             '--since="2 days ago"' or 'd34db33f..'
3615bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        @param board: the board whose manifests we want to check for.
3625bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        @raise QueryException if git log or git show errors occur.
3635bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        """
3649273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        manifest_paths = self._ExpandGlobMinusPrefix(
3659273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone            self._tempdir.name, self._BOARD_MANIFEST_GLOB_PATTERN % board)
3665bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        log_cmd = self._BuildCommand('log',
3675bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone                                     since_spec,
3685bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone                                     '--pretty="format:%H"',
3695bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone                                     '--',
3709273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone                                     ' '.join(manifest_paths))
3715bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        try:
3729273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone            # If we pass nothing to git show, we get unexpected results.
3739273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone            # So, return early if git log is going to give us nothing.
37428424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone            if not manifest_paths or not _SystemOutput(log_cmd):
3759273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone                return []
37628424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone            manifests = _SystemOutput('%s|xargs %s' % (log_cmd,
37728424c52f29f690c2dfa2a6c4d649f25edfada11Chris Masone                                                       self._ShowCmd()))
3785bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        except (IOError, OSError) as e:
3795bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone            raise QueryException(e)
3809273c0ddfcdd07a298c2145270d7bf8cb9865e23Chris Masone        logging.debug('found %s', manifests)
3815bde7dc7d8b0608feb43491d5ac648d11c890f54Chris Masone        return [m for m in re.split('\s+', manifests) if m]
382