From bebf3798e554288129e2a253b2b9dccd169822ee Mon Sep 17 00:00:00 2001 From: Chris Sosa Date: Tue, 13 Jul 2010 15:34:11 -0700 Subject: [PATCH] First cut at stable script TEST=Tested by using crash-reporter and crash_ids set to "12345" Review URL: http://codereview.chromium.org/2873016 --- cros_mark_as_stable | 1 + cros_mark_as_stable.py | 329 ++++++++++++++++++++++++++++++++ cros_mark_as_stable_unittest.py | 203 ++++++++++++++++++++ 3 files changed, 533 insertions(+) create mode 120000 cros_mark_as_stable create mode 100755 cros_mark_as_stable.py create mode 100755 cros_mark_as_stable_unittest.py diff --git a/cros_mark_as_stable b/cros_mark_as_stable new file mode 120000 index 0000000000..9a52f1b8e8 --- /dev/null +++ b/cros_mark_as_stable @@ -0,0 +1 @@ +cros_mark_as_stable.py \ No newline at end of file diff --git a/cros_mark_as_stable.py b/cros_mark_as_stable.py new file mode 100755 index 0000000000..56978f95a2 --- /dev/null +++ b/cros_mark_as_stable.py @@ -0,0 +1,329 @@ +#!/usr/bin/python + +# Copyright (c) 2010 The Chromium OS 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 module uprevs a given package's ebuild to the next revision.""" + + +import fileinput +import gflags +import os +import re +import shutil +import subprocess +import sys + +# TODO(sosa): Refactor Die into common library. +sys.path.append(os.path.dirname(__file__)) +import generate_test_report + + +gflags.DEFINE_string('board', 'x86-generic', + 'Board for which the package belongs.', short_name='b') +gflags.DEFINE_string('commit_ids', '', + '''Optional list of commit ids for each package. + This list must either be empty or have the same length as + the packages list. If not set all rev'd ebuilds will have + empty commit id's.''', + short_name='i') +gflags.DEFINE_string('packages', '', + 'Space separated list of packages to mark as stable.', + short_name='p') +gflags.DEFINE_boolean('push', False, + 'Creates, commits and pushes the stable ebuild.') +gflags.DEFINE_boolean('verbose', False, + 'Prints out verbose information about what is going on.', + short_name='v') + + +# TODO(sosa): Remove hard-coding of overlays directory once there is a better +# way. +_CHROMIUMOS_OVERLAYS_DIRECTORY = \ + '%s/trunk/src/third_party/chromiumos-overlay' % os.environ['HOME'] + +# Takes two strings, package_name and commit_id. +_GIT_COMMIT_MESSAGE = \ + 'Marking 9999 ebuild for %s with commit %s as stable.' + + +# ======================= Global Helper Functions ======================== + + +def _Print(message): + """Verbose print function.""" + if gflags.FLAGS.verbose: + print message + + +def _CheckSaneArguments(package_list, commit_id_list): + """Checks to make sure the flags are sane. Dies if arguments are not sane""" + if not gflags.FLAGS.packages: + generate_test_report.Die('Please specify at least one package') + if not gflags.FLAGS.board: + generate_test_report.Die('Please specify a board') + if commit_id_list and (len(package_list) != len(commit_id_list)): + print commit_id_list + print len(commit_id_list) + generate_test_report.Die( + 'Package list is not the same length as the commit id list') + + +def _PrintUsageAndDie(): + """Prints the usage and returns an error exit code.""" + generate_test_report.Die('Usage: %s ARGS\n%s' % (sys.argv[0], gflags.FLAGS)) + + +def _RunCommand(command): + """Runs a shell command and returns stdout back to caller.""" + _Print(' + %s' % command) + proc_handle = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) + return proc_handle.communicate()[0] + + +# ======================= End Global Helper Functions ======================== + + +class _GitBranch(object): + """Wrapper class for a git branch.""" + + def __init__(self, branch_name): + """Sets up variables but does not create the branch.""" + self.branch_name = branch_name + self._cleaned_up = False + + def __del__(self): + """Ensures we're checked back out to the master branch.""" + if not self._cleaned_up: + self.CleanUp() + + def CreateBranch(self): + """Creates a new git branch or replaces an existing one.""" + if self.Exists(): + self.Delete() + self._Checkout(self.branch_name) + + def CleanUp(self): + """Does a git checkout back to the master branch.""" + self._Checkout('master', create=False) + self._cleaned_up = True + + def _Checkout(self, target, create=True): + """Function used internally to create and move between branches.""" + if create: + git_cmd = 'git checkout -b %s origin' % target + else: + git_cmd = 'git checkout %s' % target + _RunCommand(git_cmd) + + def Exists(self): + """Returns True if the branch exists.""" + branch_cmd = 'git branch' + branches = _RunCommand(branch_cmd) + return self.branch_name in branches.split() + + def Delete(self): + """Deletes the branch and returns the user to the master branch. + + Returns True on success. + """ + self._Checkout('master', create=False) + delete_cmd = 'git branch -D %s' % self.branch_name + _RunCommand(delete_cmd) + + +class _EBuild(object): + """Wrapper class for an ebuild.""" + + def __init__(self, package, commit_id=None): + """Initializes all data about an ebuild. + + Uses equery to find the ebuild path and sets data about an ebuild for + easy reference. + """ + self.package = package + self.ebuild_path = self._FindEBuildPath(package) + (self.ebuild_path_no_revision, + self.ebuild_path_no_version, + self.current_revision) = self._ParseEBuildPath(self.ebuild_path) + self.commit_id = commit_id + + @classmethod + def _FindEBuildPath(cls, package): + """Static method that returns the full path of an ebuild.""" + _Print('Looking for unstable ebuild for %s' % package) + equery_cmd = 'equery-%s which %s 2> /dev/null' \ + % (gflags.FLAGS.board, package) + path = _RunCommand(equery_cmd) + if path: + _Print('Unstable ebuild found at %s' % path) + return path + + @classmethod + def _ParseEBuildPath(cls, ebuild_path): + """Static method that parses the path of an ebuild + + Returns a tuple containing the (ebuild path without the revision + string, without the version string, and the current revision number for + the ebuild). + """ + # Get the ebuild name without the revision string. + (ebuild_no_rev, _, rev_string) = ebuild_path.rpartition('-') + + # Verify the revision string starts with the revision character. + if rev_string.startswith('r'): + # Get the ebuild name without the revision and version strings. + ebuild_no_version = ebuild_no_rev.rpartition('-')[0] + rev_string = rev_string[1:].rpartition('.ebuild')[0] + else: + # Has no revision so we stripped the version number instead. + ebuild_no_version = ebuild_no_rev + ebuild_no_rev = ebuild_path.rpartition('.ebuild')[0] + rev_string = "0" + revision = int(rev_string) + return (ebuild_no_rev, ebuild_no_version, revision) + + +class EBuildStableMarker(object): + """Class that revs the ebuild and commits locally or pushes the change.""" + + def __init__(self, ebuild): + self._ebuild = ebuild + + def RevEBuild(self, commit_id="", redirect_file=None): + """Revs an ebuild given the git commit id. + + By default this class overwrites a new ebuild given the normal + ebuild rev'ing logic. However, a user can specify a redirect_file + to redirect the new stable ebuild to another file. + + Args: + commit_id: String corresponding to the commit hash of the developer + package to rev. + redirect_file: Optional file to write the new ebuild. By default + it is written using the standard rev'ing logic. This file must be + opened and closed by the caller. + + Raises: + OSError: Error occurred while creating a new ebuild. + IOError: Error occurred while writing to the new revved ebuild file. + """ + # TODO(sosa): Change to a check. + if not self._ebuild: + generate_test_report.Die('Invalid ebuild given to EBuildStableMarker') + + new_ebuild_path = '%s-r%d.ebuild' % (self._ebuild.ebuild_path_no_revision, + self._ebuild.current_revision + 1) + + _Print('Creating new stable ebuild %s' % new_ebuild_path) + shutil.copyfile('%s-9999.ebuild' % self._ebuild.ebuild_path_no_version, + new_ebuild_path) + + for line in fileinput.input(new_ebuild_path, inplace=1): + # Has to be done here to get changes to sys.stdout from fileinput.input. + if not redirect_file: + redirect_file = sys.stdout + if line.startswith('KEYWORDS'): + # Actually mark this file as stable by removing ~'s. + redirect_file.write(line.replace("~", "")) + elif line.startswith('EAPI'): + # Always add new commit_id after EAPI definition. + redirect_file.write(line) + redirect_file.write('EGIT_COMMIT="%s"' % commit_id) + elif not line.startswith('EGIT_COMMIT'): + # Skip old EGIT_COMMIT definition. + redirect_file.write(line) + fileinput.close() + + _Print('Adding new stable ebuild to git') + _RunCommand('git add %s' % new_ebuild_path) + + _Print('Removing old ebuild from git') + _RunCommand('git rm %s' % self._ebuild.ebuild_path) + + def CommitChange(self, message): + """Commits current changes in git locally. + + This method will take any changes from invocations to RevEBuild + and commits them locally in the git repository that contains os.pwd. + + Args: + message: the commit string to write when committing to git. + + Raises: + OSError: Error occurred while committing. + """ + _Print('Committing changes for %s with commit message %s' % \ + (self._ebuild.package, message)) + git_commit_cmd = 'git commit -am "%s"' % message + _RunCommand(git_commit_cmd) + + # TODO(sosa): This doesn't work yet. Want to directly push without a prompt. + def PushChange(self): + """Pushes changes to the git repository. + + Pushes locals commits from calls to CommitChange to the remote git + repository specified by os.pwd. + + Raises: + OSError: Error occurred while pushing. + """ + print 'Push currently not implemented' + # TODO(sosa): Un-comment once PushChange works. + # _Print('Pushing changes for %s' % self._ebuild.package) + # git_commit_cmd = 'git push' + # _RunCommand(git_commit_cmd) + + +def main(argv): + try: + argv = gflags.FLAGS(argv) + except gflags.FlagsError: + _PrintUsageAndDie() + + package_list = gflags.FLAGS.packages.split(' ') + if gflags.FLAGS.commit_ids: + commit_id_list = gflags.FLAGS.commit_ids.split(' ') + else: + commit_id_list = None + _CheckSaneArguments(package_list, commit_id_list) + + pwd = os.curdir + os.chdir(_CHROMIUMOS_OVERLAYS_DIRECTORY) + + work_branch = _GitBranch('stabilizing_branch') + work_branch.CreateBranch() + if not work_branch.Exists(): + generate_test_report.Die('Unable to create stabilizing branch') + index = 0 + try: + for index in range(len(package_list)): + # Gather the package and optional commit id to work on. + package = package_list[index] + commit_id = "" + if commit_id_list: + commit_id = commit_id_list[index] + + _Print('Working on %s' % package) + worker = EBuildStableMarker(_EBuild(package, commit_id)) + worker.RevEBuild(commit_id) + worker.CommitChange(_GIT_COMMIT_MESSAGE % (package, commit_id)) + if gflags.FLAGS.push: + worker.PushChange() + + except (OSError, IOError): + print 'An exception occurred %s' % sys.exc_info()[0] + print 'Only the following packages were revved: %s' % package_list[:index] + print '''Note you will have to go into the chromiumos-overlay directory and + reset the git repo yourself. + ''' + finally: + # Always run the last two cleanup functions. + work_branch.CleanUp() + os.chdir(pwd) + + +if __name__ == '__main__': + main(sys.argv) + diff --git a/cros_mark_as_stable_unittest.py b/cros_mark_as_stable_unittest.py new file mode 100755 index 0000000000..ade4c655d5 --- /dev/null +++ b/cros_mark_as_stable_unittest.py @@ -0,0 +1,203 @@ +#!/usr/bin/python + +# Copyright (c) 2010 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Unit tests for cros_mark_as_stable.py.""" + + +import mox +import os +import sys +import unittest + +# Required to include '.' in the python path. +sys.path.append(os.path.dirname(__file__)) +import cros_mark_as_stable + +class GitBranchTest(mox.MoxTestBase): + + def setUp(self): + mox.MoxTestBase.setUp(self) + # Always stub RunCommmand out as we use it in every method. + self.mox.StubOutWithMock(cros_mark_as_stable, '_RunCommand') + self._branch = 'test_branch' + + def testCreateBranchNoPrevious(self): + # Test init with no previous branch existing. + branch = cros_mark_as_stable._GitBranch(self._branch) + self.mox.StubOutWithMock(branch, 'Exists') + self.mox.StubOutWithMock(branch, '_Checkout') + branch.Exists().AndReturn(False) + branch._Checkout(self._branch) + self.mox.ReplayAll() + branch.CreateBranch() + self.mox.VerifyAll() + + def testCreateBranchWithPrevious(self): + # Test init with previous branch existing. + branch = cros_mark_as_stable._GitBranch(self._branch) + self.mox.StubOutWithMock(branch, 'Exists') + self.mox.StubOutWithMock(branch, 'Delete') + self.mox.StubOutWithMock(branch, '_Checkout') + branch.Exists().AndReturn(True) + branch.Delete() + branch._Checkout(self._branch) + self.mox.ReplayAll() + branch.CreateBranch() + self.mox.VerifyAll() + + def testCheckoutCreate(self): + # Test init with no previous branch existing. + cros_mark_as_stable._RunCommand('git checkout -b %s origin' % self._branch) + self.mox.ReplayAll() + branch = cros_mark_as_stable._GitBranch(self._branch) + branch._Checkout(self._branch) + self.mox.VerifyAll() + + def testCheckoutNoCreate(self): + # Test init with previous branch existing. + cros_mark_as_stable._RunCommand('git checkout master') + self.mox.ReplayAll() + branch = cros_mark_as_stable._GitBranch(self._branch) + branch._Checkout('master', False) + self.mox.VerifyAll() + + def testDelete(self): + branch = cros_mark_as_stable._GitBranch(self._branch) + self.mox.StubOutWithMock(branch, '_Checkout') + branch._Checkout('master', create=False) + cros_mark_as_stable._RunCommand('git branch -D ' + self._branch) + self.mox.ReplayAll() + branch.Delete() + self.mox.VerifyAll() + + def testExists(self): + branch = cros_mark_as_stable._GitBranch(self._branch) + + # Test if branch exists that is created + cros_mark_as_stable._RunCommand('git branch').AndReturn( + '%s %s' % (self._branch, 'master')) + self.mox.ReplayAll() + self.assertTrue(branch.Exists()) + self.mox.VerifyAll() + + +class EBuildTest(mox.MoxTestBase): + + def setUp(self): + mox.MoxTestBase.setUp(self) + self.package = 'test_package' + self.ebuild_path = '/path/test_package-0.0.1-r1.ebuild' + self.ebuild_path_no_rev = '/path/test_package-0.0.1.ebuild' + + def testInit(self): + self.mox.StubOutWithMock(cros_mark_as_stable._EBuild, '_FindEBuildPath') + self.mox.StubOutWithMock(cros_mark_as_stable._EBuild, '_ParseEBuildPath') + + cros_mark_as_stable._EBuild._FindEBuildPath( + self.package).AndReturn(self.ebuild_path) + cros_mark_as_stable._EBuild._ParseEBuildPath( + self.ebuild_path).AndReturn(['/path/test_package-0.0.1', + '/path/test_package', + 1]) + self.mox.ReplayAll() + ebuild = cros_mark_as_stable._EBuild(self.package, 'my_id') + self.mox.VerifyAll() + self.assertEquals(ebuild.package, self.package) + self.assertEquals(ebuild.ebuild_path, self.ebuild_path) + self.assertEquals(ebuild.ebuild_path_no_revision, + '/path/test_package-0.0.1') + self.assertEquals(ebuild.ebuild_path_no_version, '/path/test_package') + self.assertEquals(ebuild.current_revision, 1) + self.assertEquals(ebuild.commit_id, 'my_id') + + def testFindEBuildPath(self): + self.mox.StubOutWithMock(cros_mark_as_stable, '_RunCommand') + cros_mark_as_stable._RunCommand( + 'equery-x86-generic which %s 2> /dev/null' % self.package).AndReturn( + self.ebuild_path) + self.mox.ReplayAll() + path = cros_mark_as_stable._EBuild._FindEBuildPath(self.package) + self.mox.VerifyAll() + self.assertEquals(path, self.ebuild_path) + + def testParseEBuildPath(self): + # Test with ebuild with revision number. + no_rev, no_version, revision = cros_mark_as_stable._EBuild._ParseEBuildPath( + self.ebuild_path) + self.assertEquals(no_rev, '/path/test_package-0.0.1') + self.assertEquals(no_version, '/path/test_package') + self.assertEquals(revision, 1) + + def testParseEBuildPathNoRevisionNumber(self): + # Test with ebuild without revision number. + no_rev, no_version, revision = cros_mark_as_stable._EBuild._ParseEBuildPath( + self.ebuild_path_no_rev) + self.assertEquals(no_rev, '/path/test_package-0.0.1') + self.assertEquals(no_version, '/path/test_package') + self.assertEquals(revision, 0) + + +class EBuildStableMarkerTest(mox.MoxTestBase): + + def setUp(self): + mox.MoxTestBase.setUp(self) + self.mox.StubOutWithMock(cros_mark_as_stable, '_RunCommand') + self.m_ebuild = self.mox.CreateMock(cros_mark_as_stable._EBuild) + self.m_ebuild.package = 'test_package' + self.m_ebuild.current_revision = 1 + self.m_ebuild.ebuild_path_no_revision = '/path/test_package-0.0.1' + self.m_ebuild.ebuild_path_no_version = '/path/test_package' + self.m_ebuild.ebuild_path = '/path/test_package-0.0.1-r1.ebuild' + self.revved_ebuild_path = '/path/test_package-0.0.1-r2.ebuild' + + def testRevEBuild(self): + self.mox.StubOutWithMock(cros_mark_as_stable.fileinput, 'input') + self.mox.StubOutWithMock(cros_mark_as_stable.shutil, 'copyfile') + m_file = self.mox.CreateMock(file) + + # Prepare mock fileinput. This tests to make sure both the commit id + # and keywords are changed correctly. + mock_file = ['EAPI=2', 'EGIT_COMMIT=old_id', 'KEYWORDS=\"~x86 ~arm\"', + 'src_unpack(){}'] + + cros_mark_as_stable.shutil.copyfile( + self.m_ebuild.ebuild_path_no_version + '-9999.ebuild', + self.revved_ebuild_path) + cros_mark_as_stable.fileinput.input(self.revved_ebuild_path, + inplace=1).AndReturn(mock_file) + m_file.write('EAPI=2') + m_file.write('EGIT_COMMIT="my_id"') + m_file.write('KEYWORDS="x86 arm"') + m_file.write('src_unpack(){}') + cros_mark_as_stable._RunCommand('git add ' + self.revved_ebuild_path) + cros_mark_as_stable._RunCommand('git rm ' + self.m_ebuild.ebuild_path) + + self.mox.ReplayAll() + marker = cros_mark_as_stable.EBuildStableMarker(self.m_ebuild) + marker.RevEBuild('my_id', redirect_file=m_file) + self.mox.VerifyAll() + + + def testCommitChange(self): + mock_message = 'Commit me' + cros_mark_as_stable._RunCommand( + 'git commit -am "%s"' % mock_message) + self.mox.ReplayAll() + marker = cros_mark_as_stable.EBuildStableMarker(self.m_ebuild) + marker.CommitChange(mock_message) + self.mox.VerifyAll() + + def testPushChange(self): + #cros_mark_as_stable._RunCommand('git push') + #self.mox.ReplayAll() + #marker = cros_mark_as_stable.EBuildStableMarker(self.m_ebuild) + #marker.PushChange() + #self.mox.VerifyAll() + pass + + +if __name__ == '__main__': + unittest.main()