diff --git a/appengine_mapper.py b/appengine_mapper.py deleted file mode 100644 index ddb8e6a13..000000000 --- a/appengine_mapper.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2016 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Ensures that all depot_tools talks directly to appengine to avoid SNI.""" - -import urlparse - - -mapping = { - 'codereview.chromium.org': 'chromiumcodereview.appspot.com', - 'crashpad.chromium.org': 'crashpad-home.appspot.com', - 'bugs.chromium.org': 'monorail-prod.appspot.com', - 'bugs-staging.chromium.org': 'monorail-staging.appspot.com', -} - - -def MapUrl(url): - parts = list(urlparse.urlsplit(url)) - new_netloc = mapping.get(parts[1]) - if new_netloc: - parts[1] = new_netloc - return urlparse.urlunsplit(parts) diff --git a/checkout.py b/checkout.py deleted file mode 100644 index 9155446b1..000000000 --- a/checkout.py +++ /dev/null @@ -1,433 +0,0 @@ -# coding=utf-8 -# Copyright (c) 2012 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. -"""Manages a project checkout. - -Includes support only for git. -""" - -from __future__ import print_function - -import fnmatch -import logging -import os -import re -import shutil -import subprocess -import sys -import tempfile - -# The configparser module was renamed in Python 3. -try: - import configparser -except ImportError: - import ConfigParser as configparser - -import patch -import scm -import subprocess2 - - -if sys.platform in ('cygwin', 'win32'): - # Disable timeouts on Windows since we can't have shells with timeouts. - GLOBAL_TIMEOUT = None - FETCH_TIMEOUT = None -else: - # Default timeout of 15 minutes. - GLOBAL_TIMEOUT = 15*60 - # Use a larger timeout for checkout since it can be a genuinely slower - # operation. - FETCH_TIMEOUT = 30*60 - - -def get_code_review_setting(path, key, - codereview_settings_file='codereview.settings'): - """Parses codereview.settings and return the value for the key if present. - - Don't cache the values in case the file is changed.""" - # TODO(maruel): Do not duplicate code. - settings = {} - try: - settings_file = open(os.path.join(path, codereview_settings_file), 'r') - try: - for line in settings_file.readlines(): - if not line or line.startswith('#'): - continue - if not ':' in line: - # Invalid file. - return None - k, v = line.split(':', 1) - settings[k.strip()] = v.strip() - finally: - settings_file.close() - except IOError: - return None - return settings.get(key, None) - - -def align_stdout(stdout): - """Returns the aligned output of multiple stdouts.""" - output = '' - for item in stdout: - item = item.strip() - if not item: - continue - output += ''.join(' %s\n' % line for line in item.splitlines()) - return output - - -class PatchApplicationFailed(Exception): - """Patch failed to be applied.""" - def __init__(self, errors, verbose): - super(PatchApplicationFailed, self).__init__(errors, verbose) - self.errors = errors - self.verbose = verbose - - def __str__(self): - out = [] - for e in self.errors: - p, status = e - if p and p.filename: - out.append('Failed to apply patch for %s:' % p.filename) - if status: - out.append(status) - if p and self.verbose: - out.append('Patch: %s' % p.dump()) - return '\n'.join(out) - - -class CheckoutBase(object): - # Set to None to have verbose output. - VOID = subprocess2.VOID - - def __init__(self, root_dir, project_name, post_processors): - """ - Args: - post_processor: list of lambda(checkout, patches) to call on each of the - modified files. - """ - super(CheckoutBase, self).__init__() - self.root_dir = root_dir - self.project_name = project_name - if self.project_name is None: - self.project_path = self.root_dir - else: - self.project_path = os.path.join(self.root_dir, self.project_name) - # Only used for logging purposes. - self._last_seen_revision = None - self.post_processors = post_processors - assert self.root_dir - assert self.project_path - assert os.path.isabs(self.project_path) - - def get_settings(self, key): - return get_code_review_setting(self.project_path, key) - - def prepare(self, revision): - """Checks out a clean copy of the tree and removes any local modification. - - This function shouldn't throw unless the remote repository is inaccessible, - there is no free disk space or hard issues like that. - - Args: - revision: The revision it should sync to, SCM specific. - """ - raise NotImplementedError() - - def apply_patch(self, patches, post_processors=None, verbose=False): - """Applies a patch and returns the list of modified files. - - This function should throw patch.UnsupportedPatchFormat or - PatchApplicationFailed when relevant. - - Args: - patches: patch.PatchSet object. - """ - raise NotImplementedError() - - def commit(self, commit_message, user): - """Commits the patch upstream, while impersonating 'user'.""" - raise NotImplementedError() - - def revisions(self, rev1, rev2): - """Returns the count of revisions from rev1 to rev2, e.g. len(]rev1, rev2]). - - If rev2 is None, it means 'HEAD'. - - Returns None if there is no link between the two. - """ - raise NotImplementedError() - - -class GitCheckout(CheckoutBase): - """Manages a git checkout.""" - def __init__(self, root_dir, project_name, remote_branch, git_url, - commit_user, post_processors=None): - super(GitCheckout, self).__init__(root_dir, project_name, post_processors) - self.git_url = git_url - self.commit_user = commit_user - self.remote_branch = remote_branch - # The working branch where patches will be applied. It will track the - # remote branch. - self.working_branch = 'working_branch' - # There is no reason to not hardcode origin. - self.remote = 'origin' - # There is no reason to not hardcode master. - self.master_branch = 'master' - - def prepare(self, revision): - """Resets the git repository in a clean state. - - Checks it out if not present and deletes the working branch. - """ - assert self.remote_branch - assert self.git_url - - if not os.path.isdir(self.project_path): - # Clone the repo if the directory is not present. - logging.info( - 'Checking out %s in %s', self.project_name, self.project_path) - self._check_call_git( - ['clone', self.git_url, '-b', self.remote_branch, self.project_path], - cwd=None, timeout=FETCH_TIMEOUT) - else: - # Throw away all uncommitted changes in the existing checkout. - self._check_call_git(['checkout', self.remote_branch]) - self._check_call_git( - ['reset', '--hard', '--quiet', - '%s/%s' % (self.remote, self.remote_branch)]) - - if revision: - try: - # Look if the commit hash already exist. If so, we can skip a - # 'git fetch' call. - revision = self._check_output_git(['rev-parse', revision]).rstrip() - except subprocess.CalledProcessError: - self._check_call_git( - ['fetch', self.remote, self.remote_branch, '--quiet']) - revision = self._check_output_git(['rev-parse', revision]).rstrip() - self._check_call_git(['checkout', '--force', '--quiet', revision]) - else: - branches, active = self._branches() - if active != self.master_branch: - self._check_call_git( - ['checkout', '--force', '--quiet', self.master_branch]) - self._sync_remote_branch() - - if self.working_branch in branches: - self._call_git(['branch', '-D', self.working_branch]) - return self._get_head_commit_hash() - - def _sync_remote_branch(self): - """Syncs the remote branch.""" - # We do a 'git pull origin master:refs/remotes/origin/master' instead of - # 'git pull origin master' because from the manpage for git-pull: - # A parameter without a colon is equivalent to : when - # pulling/fetching, so it merges into the current branch without - # storing the remote branch anywhere locally. - remote_tracked_path = 'refs/remotes/%s/%s' % ( - self.remote, self.remote_branch) - self._check_call_git( - ['pull', self.remote, - '%s:%s' % (self.remote_branch, remote_tracked_path), - '--quiet']) - - def _get_head_commit_hash(self): - """Gets the current revision (in unicode) from the local branch.""" - return unicode(self._check_output_git(['rev-parse', 'HEAD']).strip()) - - def apply_patch(self, patches, post_processors=None, verbose=False): - """Applies a patch on 'working_branch' and switches to it. - - The changes remain staged on the current branch. - """ - post_processors = post_processors or self.post_processors or [] - # It this throws, the checkout is corrupted. Maybe worth deleting it and - # trying again? - if self.remote_branch: - self._check_call_git( - ['checkout', '-b', self.working_branch, '-t', self.remote_branch, - '--quiet']) - - errors = [] - for index, p in enumerate(patches): - stdout = [] - try: - filepath = os.path.join(self.project_path, p.filename) - if p.is_delete: - if (not os.path.exists(filepath) and - any(p1.source_filename == p.filename for p1 in patches[0:index])): - # The file was already deleted if a prior patch with file rename - # was already processed because 'git apply' did it for us. - pass - else: - stdout.append(self._check_output_git(['rm', p.filename])) - assert(not os.path.exists(filepath)) - stdout.append('Deleted.') - else: - dirname = os.path.dirname(p.filename) - full_dir = os.path.join(self.project_path, dirname) - if dirname and not os.path.isdir(full_dir): - os.makedirs(full_dir) - stdout.append('Created missing directory %s.' % dirname) - if p.is_binary: - content = p.get() - with open(filepath, 'wb') as f: - f.write(content) - stdout.append('Added binary file %d bytes' % len(content)) - cmd = ['add', p.filename] - if verbose: - cmd.append('--verbose') - stdout.append(self._check_output_git(cmd)) - else: - # No need to do anything special with p.is_new or if not - # p.diff_hunks. git apply manages all that already. - cmd = ['apply', '--index', '-3', '-p%s' % p.patchlevel] - if verbose: - cmd.append('--verbose') - stdout.append(self._check_output_git(cmd, stdin=p.get(True))) - for post in post_processors: - post(self, p) - if verbose: - print(p.filename) - print(align_stdout(stdout)) - except OSError as e: - errors.append((p, '%s%s' % (align_stdout(stdout), e))) - except subprocess.CalledProcessError as e: - errors.append((p, - 'While running %s;\n%s%s' % ( - ' '.join(e.cmd), - align_stdout(stdout), - align_stdout([getattr(e, 'stdout', '')])))) - if errors: - raise PatchApplicationFailed(errors, verbose) - found_files = self._check_output_git( - ['-c', 'core.quotePath=false', 'diff', '--ignore-submodules', - '--name-only', '--staged']).splitlines(False) - if sorted(patches.filenames) != sorted(found_files): - extra_files = sorted(set(found_files) - set(patches.filenames)) - unpatched_files = sorted(set(patches.filenames) - set(found_files)) - if extra_files: - print('Found extra files: %r' % extra_files) - if unpatched_files: - print('Found unpatched files: %r' % unpatched_files) - - - def commit(self, commit_message, user): - """Commits, updates the commit message and pushes.""" - # TODO(hinoka): CQ no longer uses this, I think its deprecated. - # Delete this. - assert self.commit_user - assert isinstance(commit_message, unicode) - current_branch = self._check_output_git( - ['rev-parse', '--abbrev-ref', 'HEAD']).strip() - assert current_branch == self.working_branch - - commit_cmd = ['commit', '-m', commit_message] - if user and user != self.commit_user: - # We do not have the first or last name of the user, grab the username - # from the email and call it the original author's name. - # TODO(rmistry): Do not need the below if user is already in - # "Name " format. - name = user.split('@')[0] - commit_cmd.extend(['--author', '%s <%s>' % (name, user)]) - self._check_call_git(commit_cmd) - - # Push to the remote repository. - self._check_call_git( - ['push', 'origin', '%s:%s' % (self.working_branch, self.remote_branch), - '--quiet']) - # Get the revision after the push. - revision = self._get_head_commit_hash() - # Switch back to the remote_branch and sync it. - self._check_call_git(['checkout', self.remote_branch]) - self._sync_remote_branch() - # Delete the working branch since we are done with it. - self._check_call_git(['branch', '-D', self.working_branch]) - - return revision - - def _check_call_git(self, args, **kwargs): - kwargs.setdefault('cwd', self.project_path) - kwargs.setdefault('stdout', self.VOID) - kwargs.setdefault('timeout', GLOBAL_TIMEOUT) - return subprocess2.check_call_out(['git'] + args, **kwargs) - - def _call_git(self, args, **kwargs): - """Like check_call but doesn't throw on failure.""" - kwargs.setdefault('cwd', self.project_path) - kwargs.setdefault('stdout', self.VOID) - kwargs.setdefault('timeout', GLOBAL_TIMEOUT) - return subprocess2.call(['git'] + args, **kwargs) - - def _check_output_git(self, args, **kwargs): - kwargs.setdefault('cwd', self.project_path) - kwargs.setdefault('timeout', GLOBAL_TIMEOUT) - return subprocess2.check_output( - ['git'] + args, stderr=subprocess2.STDOUT, **kwargs) - - def _branches(self): - """Returns the list of branches and the active one.""" - out = self._check_output_git(['branch']).splitlines(False) - branches = [l[2:] for l in out] - active = None - for l in out: - if l.startswith('*'): - active = l[2:] - break - return branches, active - - def revisions(self, rev1, rev2): - """Returns the number of actual commits between both hash.""" - self._fetch_remote() - - rev2 = rev2 or '%s/%s' % (self.remote, self.remote_branch) - # Revision range is ]rev1, rev2] and ordering matters. - try: - out = self._check_output_git( - ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)]) - except subprocess.CalledProcessError: - return None - return len(out.splitlines()) - - def _fetch_remote(self): - """Fetches the remote without rebasing.""" - # git fetch is always verbose even with -q, so redirect its output. - self._check_output_git(['fetch', self.remote, self.remote_branch], - timeout=FETCH_TIMEOUT) - - -class ReadOnlyCheckout(object): - """Converts a checkout into a read-only one.""" - def __init__(self, checkout, post_processors=None): - super(ReadOnlyCheckout, self).__init__() - self.checkout = checkout - self.post_processors = (post_processors or []) + ( - self.checkout.post_processors or []) - - def prepare(self, revision): - return self.checkout.prepare(revision) - - def get_settings(self, key): - return self.checkout.get_settings(key) - - def apply_patch(self, patches, post_processors=None, verbose=False): - return self.checkout.apply_patch( - patches, post_processors or self.post_processors, verbose) - - def commit(self, message, user): # pylint: disable=no-self-use - logging.info('Would have committed for %s with message: %s' % ( - user, message)) - return 'FAKE' - - def revisions(self, rev1, rev2): - return self.checkout.revisions(rev1, rev2) - - @property - def project_name(self): - return self.checkout.project_name - - @property - def project_path(self): - return self.checkout.project_path diff --git a/git_cl.py b/git_cl.py index a2695cf79..a52b0ae98 100755 --- a/git_cl.py +++ b/git_cl.py @@ -40,7 +40,6 @@ import zlib from third_party import colorama from third_party import httplib2 import auth -import checkout import clang_format import dart_format import setup_color diff --git a/patch.py b/patch.py deleted file mode 100644 index 4084ffdf5..000000000 --- a/patch.py +++ /dev/null @@ -1,548 +0,0 @@ -# coding=utf-8 -# Copyright (c) 2012 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. -"""Utility functions to handle patches.""" - -import posixpath -import os -import re - - -class UnsupportedPatchFormat(Exception): - def __init__(self, filename, status): - super(UnsupportedPatchFormat, self).__init__(filename, status) - self.filename = filename - self.status = status - - def __str__(self): - out = 'Can\'t process patch for file %s.' % self.filename - if self.status: - out += '\n%s' % self.status - return out - - -class FilePatchBase(object): - """Defines a single file being modified. - - '/' is always used instead of os.sep for consistency. - """ - is_delete = False - is_binary = False - is_new = False - - def __init__(self, filename): - assert self.__class__ is not FilePatchBase - self.filename = self._process_filename(filename) - # Set when the file is copied or moved. - self.source_filename = None - - @property - def filename_utf8(self): - return self.filename.encode('utf-8') - - @property - def source_filename_utf8(self): - if self.source_filename is not None: - return self.source_filename.encode('utf-8') - - @staticmethod - def _process_filename(filename): - filename = filename.replace('\\', '/') - # Blacklist a few characters for simplicity. - for i in ('$', '..', '\'', '"', '<', '>', ':', '|', '?', '*'): - if i in filename: - raise UnsupportedPatchFormat( - filename, 'Can\'t use \'%s\' in filename.' % i) - if filename.startswith('/'): - raise UnsupportedPatchFormat( - filename, 'Filename can\'t start with \'/\'.') - if filename == 'CON': - raise UnsupportedPatchFormat( - filename, 'Filename can\'t be \'CON\'.') - if re.match(r'COM\d', filename): - raise UnsupportedPatchFormat( - filename, 'Filename can\'t be \'%s\'.' % filename) - return filename - - def set_relpath(self, relpath): - if not relpath: - return - relpath = relpath.replace('\\', '/') - if relpath[0] == '/': - self._fail('Relative path starts with %s' % relpath[0]) - self.filename = self._process_filename( - posixpath.join(relpath, self.filename)) - if self.source_filename: - self.source_filename = self._process_filename( - posixpath.join(relpath, self.source_filename)) - - def _fail(self, msg): - """Shortcut function to raise UnsupportedPatchFormat.""" - raise UnsupportedPatchFormat(self.filename, msg) - - def __str__(self): - # Use a status-like board. - out = '' - if self.is_binary: - out += 'B' - else: - out += ' ' - if self.is_delete: - out += 'D' - else: - out += ' ' - if self.is_new: - out += 'N' - else: - out += ' ' - if self.source_filename: - out += 'R' - else: - out += ' ' - out += ' ' - if self.source_filename: - out += '%s->' % self.source_filename_utf8 - return out + self.filename_utf8 - - def dump(self): - """Dumps itself in a verbose way to help diagnosing.""" - return str(self) - - -class FilePatchDelete(FilePatchBase): - """Deletes a file.""" - is_delete = True - - def __init__(self, filename, is_binary): - super(FilePatchDelete, self).__init__(filename) - self.is_binary = is_binary - - -class FilePatchBinary(FilePatchBase): - """Content of a new binary file.""" - is_binary = True - - def __init__(self, filename, data, svn_properties, is_new): - super(FilePatchBinary, self).__init__(filename) - self.data = data - self.svn_properties = svn_properties or [] - self.is_new = is_new - - def get(self): - return self.data - - def __str__(self): - return str(super(FilePatchBinary, self)) + ' %d bytes' % len(self.data) - - -class Hunk(object): - """Parsed hunk data container.""" - - def __init__(self, start_src, lines_src, start_dst, lines_dst): - self.start_src = start_src - self.lines_src = lines_src - self.start_dst = start_dst - self.lines_dst = lines_dst - self.variation = self.lines_dst - self.lines_src - self.text = [] - - def __repr__(self): - return '%s<(%d, %d) to (%d, %d)>' % ( - self.__class__.__name__, - self.start_src, self.lines_src, self.start_dst, self.lines_dst) - - -class FilePatchDiff(FilePatchBase): - """Patch for a single file.""" - - def __init__(self, filename, diff, svn_properties): - super(FilePatchDiff, self).__init__(filename) - if not diff: - self._fail('File doesn\'t have a diff.') - self.diff_header, self.diff_hunks = self._split_header(diff) - self.svn_properties = svn_properties or [] - self.is_git_diff = self._is_git_diff_header(self.diff_header) - self.patchlevel = 0 - if self.is_git_diff: - self._verify_git_header() - else: - self._verify_svn_header() - self.hunks = self._split_hunks() - if self.source_filename and not self.is_new: - self._fail('If source_filename is set, is_new must be also be set') - - def get(self, for_git): - if for_git or not self.source_filename: - return self.diff_header + self.diff_hunks - else: - # patch is stupid. It patches the source_filename instead so get rid of - # any source_filename reference if needed. - return ( - self.diff_header.replace( - self.source_filename_utf8, self.filename_utf8) + - self.diff_hunks) - - def set_relpath(self, relpath): - old_filename = self.filename_utf8 - old_source_filename = self.source_filename_utf8 or self.filename_utf8 - super(FilePatchDiff, self).set_relpath(relpath) - # Update the header too. - filename = self.filename_utf8 - source_filename = self.source_filename_utf8 or self.filename_utf8 - lines = self.diff_header.splitlines(True) - for i, line in enumerate(lines): - if line.startswith('diff --git'): - lines[i] = line.replace( - 'a/' + old_source_filename, source_filename).replace( - 'b/' + old_filename, filename) - elif re.match(r'^\w+ from .+$', line) or line.startswith('---'): - lines[i] = line.replace(old_source_filename, source_filename) - elif re.match(r'^\w+ to .+$', line) or line.startswith('+++'): - lines[i] = line.replace(old_filename, filename) - self.diff_header = ''.join(lines) - - def _split_header(self, diff): - """Splits a diff in two: the header and the hunks.""" - header = [] - hunks = diff.splitlines(True) - while hunks: - header.append(hunks.pop(0)) - if header[-1].startswith('--- '): - break - else: - # Some diff may not have a ---/+++ set like a git rename with no change or - # a svn diff with only property change. - pass - - if hunks: - if not hunks[0].startswith('+++ '): - self._fail('Inconsistent header') - header.append(hunks.pop(0)) - if hunks: - if not hunks[0].startswith('@@ '): - self._fail('Inconsistent hunk header') - - # Mangle any \\ in the header to /. - header_lines = ('Index:', 'diff', 'copy', 'rename', '+++', '---') - basename = os.path.basename(self.filename_utf8) - for i in xrange(len(header)): - if (header[i].split(' ', 1)[0] in header_lines or - header[i].endswith(basename)): - header[i] = header[i].replace('\\', '/') - return ''.join(header), ''.join(hunks) - - @staticmethod - def _is_git_diff_header(diff_header): - """Returns True if the diff for a single files was generated with git.""" - # Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff - # Rename partial change: - # http://codereview.chromium.org/download/issue6250123_3013_6010.diff - # Rename no change: - # http://codereview.chromium.org/download/issue6287022_3001_4010.diff - return any(l.startswith('diff --git') for l in diff_header.splitlines()) - - def _split_hunks(self): - """Splits the hunks and does verification.""" - hunks = [] - for line in self.diff_hunks.splitlines(True): - if line.startswith('@@'): - match = re.match(r'^@@ -([\d,]+) \+([\d,]+) @@.*$', line) - # File add will result in "-0,0 +1" but file deletion will result in - # "-1,N +0,0" where N is the number of lines deleted. That's from diff - # and svn diff. git diff doesn't exhibit this behavior. - # svn diff for a single line file rewrite "@@ -1 +1 @@". Fun. - # "@@ -1 +1,N @@" is also valid where N is the length of the new file. - if not match: - self._fail('Hunk header is unparsable') - count = match.group(1).count(',') - if not count: - start_src = int(match.group(1)) - lines_src = 1 - elif count == 1: - start_src, lines_src = map(int, match.group(1).split(',', 1)) - else: - self._fail('Hunk header is malformed') - - count = match.group(2).count(',') - if not count: - start_dst = int(match.group(2)) - lines_dst = 1 - elif count == 1: - start_dst, lines_dst = map(int, match.group(2).split(',', 1)) - else: - self._fail('Hunk header is malformed') - new_hunk = Hunk(start_src, lines_src, start_dst, lines_dst) - if hunks: - if new_hunk.start_src <= hunks[-1].start_src: - self._fail('Hunks source lines are not ordered') - if new_hunk.start_dst <= hunks[-1].start_dst: - self._fail('Hunks destination lines are not ordered') - hunks.append(new_hunk) - continue - hunks[-1].text.append(line) - - if len(hunks) == 1: - if hunks[0].start_src == 0 and hunks[0].lines_src == 0: - self.is_new = True - if hunks[0].start_dst == 0 and hunks[0].lines_dst == 0: - self.is_delete = True - - if self.is_new and self.is_delete: - self._fail('Hunk header is all 0') - - if not self.is_new and not self.is_delete: - for hunk in hunks: - variation = ( - len([1 for i in hunk.text if i.startswith('+')]) - - len([1 for i in hunk.text if i.startswith('-')])) - if variation != hunk.variation: - self._fail( - 'Hunk header is incorrect: %d vs %d; %r' % ( - variation, hunk.variation, hunk)) - if not hunk.start_src: - self._fail( - 'Hunk header start line is incorrect: %d' % hunk.start_src) - if not hunk.start_dst: - self._fail( - 'Hunk header start line is incorrect: %d' % hunk.start_dst) - hunk.start_src -= 1 - hunk.start_dst -= 1 - if self.is_new and hunks: - hunks[0].start_dst -= 1 - if self.is_delete and hunks: - hunks[0].start_src -= 1 - return hunks - - def mangle(self, string): - """Mangle a file path.""" - return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:]) - - def _verify_git_header(self): - """Sanity checks the header. - - Expects the following format: - - - diff --git (|a/) (|b/) - - - - - - --- - +++ - - Everything is optional except the diff --git line. - """ - lines = self.diff_header.splitlines() - - # Verify the diff --git line. - old = None - new = None - while lines: - match = re.match(r'^diff \-\-git (.*?) (.*)$', lines.pop(0)) - if not match: - continue - if match.group(1).startswith('a/') and match.group(2).startswith('b/'): - self.patchlevel = 1 - old = self.mangle(match.group(1)) - new = self.mangle(match.group(2)) - - # The rename is about the new file so the old file can be anything. - if new not in (self.filename_utf8, 'dev/null'): - self._fail('Unexpected git diff output name %s.' % new) - if old == 'dev/null' and new == 'dev/null': - self._fail('Unexpected /dev/null git diff.') - break - - if not old or not new: - self._fail('Unexpected git diff; couldn\'t find git header.') - - if old not in (self.filename_utf8, 'dev/null'): - # Copy or rename. - self.source_filename = old.decode('utf-8') - self.is_new = True - - last_line = '' - - while lines: - line = lines.pop(0) - self._verify_git_header_process_line(lines, line, last_line) - last_line = line - - # Cheap check to make sure the file name is at least mentioned in the - # 'diff' header. That the only remaining invariant. - if not self.filename_utf8 in self.diff_header: - self._fail('Diff seems corrupted.') - - def _verify_git_header_process_line(self, lines, line, last_line): - """Processes a single line of the header. - - Returns True if it should continue looping. - - Format is described to - http://www.kernel.org/pub/software/scm/git/docs/git-diff.html - """ - match = re.match(r'^(rename|copy) from (.+)$', line) - old = self.source_filename_utf8 or self.filename_utf8 - if match: - if old != match.group(2): - self._fail('Unexpected git diff input name for line %s.' % line) - if not lines or not lines[0].startswith('%s to ' % match.group(1)): - self._fail( - 'Confused %s from/to git diff for line %s.' % - (match.group(1), line)) - return - - match = re.match(r'^(rename|copy) to (.+)$', line) - if match: - if self.filename_utf8 != match.group(2): - self._fail('Unexpected git diff output name for line %s.' % line) - if not last_line.startswith('%s from ' % match.group(1)): - self._fail( - 'Confused %s from/to git diff for line %s.' % - (match.group(1), line)) - return - - match = re.match(r'^deleted file mode (\d{6})$', line) - if match: - # It is necessary to parse it because there may be no hunk, like when the - # file was empty. - self.is_delete = True - return - - match = re.match(r'^new(| file) mode (\d{6})$', line) - if match: - mode = match.group(2) - # Only look at owner ACL for executable. - if bool(int(mode[4]) & 1): - self.svn_properties.append(('svn:executable', '.')) - elif not self.source_filename and self.is_new: - # It's a new file, not from a rename/copy, then there's no property to - # delete. - self.svn_properties.append(('svn:executable', None)) - return - - match = re.match(r'^--- (.*)$', line) - if match: - if last_line[:3] in ('---', '+++'): - self._fail('--- and +++ are reversed') - if match.group(1) == '/dev/null': - self.is_new = True - elif self.mangle(match.group(1)) != old: - # git patches are always well formatted, do not allow random filenames. - self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1))) - if not lines or not lines[0].startswith('+++'): - self._fail('Missing git diff output name.') - return - - match = re.match(r'^\+\+\+ (.*)$', line) - if match: - if not last_line.startswith('---'): - self._fail('Unexpected git diff: --- not following +++.') - if '/dev/null' == match.group(1): - self.is_delete = True - elif self.filename_utf8 != self.mangle(match.group(1)): - self._fail( - 'Unexpected git diff: %s != %s.' % (self.filename, match.group(1))) - if lines: - self._fail('Crap after +++') - # We're done. - return - - def _verify_svn_header(self): - """Sanity checks the header. - - A svn diff can contain only property changes, in that case there will be no - proper header. To make things worse, this property change header is - localized. - """ - lines = self.diff_header.splitlines() - last_line = '' - - while lines: - line = lines.pop(0) - self._verify_svn_header_process_line(lines, line, last_line) - last_line = line - - # Cheap check to make sure the file name is at least mentioned in the - # 'diff' header. That the only remaining invariant. - if not self.filename_utf8 in self.diff_header: - self._fail('Diff seems corrupted.') - - def _verify_svn_header_process_line(self, lines, line, last_line): - """Processes a single line of the header. - - Returns True if it should continue looping. - """ - match = re.match(r'^--- ([^\t]+).*$', line) - if match: - if last_line[:3] in ('---', '+++'): - self._fail('--- and +++ are reversed') - if match.group(1) == '/dev/null': - self.is_new = True - elif self.mangle(match.group(1)) != self.filename_utf8: - # guess the source filename. - self.source_filename = match.group(1).decode('utf-8') - self.is_new = True - if not lines or not lines[0].startswith('+++'): - self._fail('Nothing after header.') - return - - match = re.match(r'^\+\+\+ ([^\t]+).*$', line) - if match: - if not last_line.startswith('---'): - self._fail('Unexpected diff: --- not following +++.') - if match.group(1) == '/dev/null': - self.is_delete = True - elif self.mangle(match.group(1)) != self.filename_utf8: - self._fail('Unexpected diff: %s.' % match.group(1)) - if lines: - self._fail('Crap after +++') - # We're done. - return - - def dump(self): - """Dumps itself in a verbose way to help diagnosing.""" - return str(self) + '\n' + self.get(True) - - -class PatchSet(object): - """A list of FilePatch* objects.""" - - def __init__(self, patches): - for p in patches: - assert isinstance(p, FilePatchBase) - - def key(p): - """Sort by ordering of application. - - File move are first. - Deletes are last. - """ - # The bool is necessary because None < 'string' but the reverse is needed. - return ( - p.is_delete, - # False is before True, so files *with* a source file will be first. - not bool(p.source_filename), - p.source_filename_utf8, - p.filename_utf8) - - self.patches = sorted(patches, key=key) - - def set_relpath(self, relpath): - """Used to offset the patch into a subdirectory.""" - for patch in self.patches: - patch.set_relpath(relpath) - - def __iter__(self): - for patch in self.patches: - yield patch - - def __getitem__(self, key): - return self.patches[key] - - @property - def filenames(self): - return [p.filename for p in self.patches] diff --git a/testing_support/gerrit_test_case.py b/testing_support/gerrit_test_case.py deleted file mode 100644 index 45df909f0..000000000 --- a/testing_support/gerrit_test_case.py +++ /dev/null @@ -1,480 +0,0 @@ -# Copyright (c) 2013 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. - -"""Test framework for code that interacts with gerrit. - -class GerritTestCase --------------------------------------------------------------------------------- -This class initializes and runs an a gerrit instance on localhost. To use the -framework, define a class that extends GerritTestCase, and then do standard -python unittest development as described here: - -http://docs.python.org/2.7/library/unittest.html#basic-example - -When your test code runs, the framework will: - -- Download the latest stable(-ish) binary release of the gerrit code. -- Start up a live gerrit instance running in a temp directory on the localhost. -- Set up a single gerrit user account with admin priveleges. -- Supply credential helpers for interacting with the gerrit instance via http - or ssh. - -Refer to depot_tools/testing_support/gerrit-init.sh for details about how the -gerrit instance is set up, and refer to helper methods defined below -(createProject, cloneProject, uploadChange, etc.) for ways to interact with the -gerrit instance from your test methods. - - -class RepoTestCase --------------------------------------------------------------------------------- -This class extends GerritTestCase, and creates a set of project repositories -and a manifest repository that can be used in conjunction with the 'repo' tool. - -Each test method will initialize and sync a brand-new repo working directory. -The 'repo' command may be invoked in a subprocess as part of your tests. - -One gotcha: 'repo upload' will always attempt to use the ssh interface to talk -to gerrit. -""" - -from __future__ import print_function - -import collections -import errno -import netrc -import os -import re -import shutil -import signal -import socket -import stat -import subprocess -import sys -import tempfile -import unittest -import urllib - -import gerrit_util - - -DEPOT_TOOLS_DIR = os.path.normpath(os.path.join( - os.path.realpath(__file__), '..', '..')) - - -# When debugging test code, it's sometimes helpful to leave the test gerrit -# instance intact and running after the test code exits. Setting TEARDOWN -# to False will do that. -TEARDOWN = True - -class GerritTestCase(unittest.TestCase): - """Test class for tests that interact with a gerrit server. - - The class setup creates and launches a stand-alone gerrit instance running on - localhost, for test methods to interact with. Class teardown stops and - deletes the gerrit instance. - - Note that there is a single gerrit instance for ALL test methods in a - GerritTestCase sub-class. - """ - - COMMIT_RE = re.compile(r'^commit ([0-9a-fA-F]{40})$') - CHANGEID_RE = re.compile(r'^\s+Change-Id:\s*(\S+)$') - DEVNULL = open(os.devnull, 'w') - TEST_USERNAME = 'test-username' - TEST_EMAIL = 'test-username@test.org' - - GerritInstance = collections.namedtuple('GerritInstance', [ - 'credential_file', - 'gerrit_dir', - 'gerrit_exe', - 'gerrit_host', - 'gerrit_pid', - 'gerrit_url', - 'git_dir', - 'git_host', - 'git_url', - 'http_port', - 'netrc_file', - 'ssh_ident', - 'ssh_port', - ]) - - @classmethod - def check_call(cls, *args, **kwargs): - kwargs.setdefault('stdout', cls.DEVNULL) - kwargs.setdefault('stderr', cls.DEVNULL) - subprocess.check_call(*args, **kwargs) - - @classmethod - def check_output(cls, *args, **kwargs): - kwargs.setdefault('stderr', cls.DEVNULL) - return subprocess.check_output(*args, **kwargs) - - @classmethod - def _create_gerrit_instance(cls, gerrit_dir): - gerrit_init_script = os.path.join( - DEPOT_TOOLS_DIR, 'testing_support', 'gerrit-init.sh') - http_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - http_sock.bind(('', 0)) - http_port = str(http_sock.getsockname()[1]) - ssh_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ssh_sock.bind(('', 0)) - ssh_port = str(ssh_sock.getsockname()[1]) - - # NOTE: this is not completely safe. These port numbers could be - # re-assigned by the OS between the calls to socket.close() and gerrit - # starting up. The only safe way to do this would be to pass file - # descriptors down to the gerrit process, which is not even remotely - # supported. Alas. - http_sock.close() - ssh_sock.close() - cls.check_call(['bash', gerrit_init_script, '--http-port', http_port, - '--ssh-port', ssh_port, gerrit_dir]) - - gerrit_exe = os.path.join(gerrit_dir, 'bin', 'gerrit.sh') - cls.check_call(['bash', gerrit_exe, 'start']) - with open(os.path.join(gerrit_dir, 'logs', 'gerrit.pid')) as fh: - gerrit_pid = int(fh.read().rstrip()) - - return cls.GerritInstance( - credential_file=os.path.join(gerrit_dir, 'tmp', '.git-credentials'), - gerrit_dir=gerrit_dir, - gerrit_exe=gerrit_exe, - gerrit_host='localhost:%s' % http_port, - gerrit_pid=gerrit_pid, - gerrit_url='http://localhost:%s' % http_port, - git_dir=os.path.join(gerrit_dir, 'git'), - git_host='%s/git' % gerrit_dir, - git_url='file://%s/git' % gerrit_dir, - http_port=http_port, - netrc_file=os.path.join(gerrit_dir, 'tmp', '.netrc'), - ssh_ident=os.path.join(gerrit_dir, 'tmp', 'id_rsa'), - ssh_port=ssh_port,) - - @classmethod - def setUpClass(cls): - """Sets up the gerrit instances in a class-specific temp dir.""" - # Create gerrit instance. - gerrit_dir = tempfile.mkdtemp() - os.chmod(gerrit_dir, 0o700) - gi = cls.gerrit_instance = cls._create_gerrit_instance(gerrit_dir) - - # Set netrc file for http authentication. - cls.gerrit_util_netrc_orig = gerrit_util.NETRC - gerrit_util.NETRC = netrc.netrc(gi.netrc_file) - - # gerrit_util.py defaults to using https, but for testing, it's much - # simpler to use http connections. - cls.gerrit_util_protocol_orig = gerrit_util.GERRIT_PROTOCOL - gerrit_util.GERRIT_PROTOCOL = 'http' - - # Because we communicate with the test server via http, rather than https, - # libcurl won't add authentication headers to raw git requests unless the - # gerrit server returns 401. That works for pushes, but for read operations - # (like git-ls-remote), gerrit will simply omit any ref that requires - # authentication. By default gerrit doesn't permit anonymous read access to - # refs/meta/config. Override that behavior so tests can access - # refs/meta/config if necessary. - clone_path = os.path.join(gi.gerrit_dir, 'tmp', 'All-Projects') - cls._CloneProject('All-Projects', clone_path) - project_config = os.path.join(clone_path, 'project.config') - cls.check_call(['git', 'config', '--file', project_config, '--add', - 'access.refs/meta/config.read', 'group Anonymous Users']) - cls.check_call(['git', 'add', project_config], cwd=clone_path) - cls.check_call( - ['git', 'commit', '-m', 'Anonyous read for refs/meta/config'], - cwd=clone_path) - cls.check_call(['git', 'push', 'origin', 'HEAD:refs/meta/config'], - cwd=clone_path) - - def setUp(self): - self.tempdir = tempfile.mkdtemp() - os.chmod(self.tempdir, 0o700) - - def tearDown(self): - if TEARDOWN: - shutil.rmtree(self.tempdir) - - @classmethod - def createProject(cls, name, description='Test project', owners=None, - submit_type='CHERRY_PICK'): - """Create a project on the test gerrit server.""" - if owners is None: - owners = ['Administrators'] - body = { - 'description': description, - 'submit_type': submit_type, - 'owners': owners, - } - path = 'projects/%s' % urllib.quote(name, '') - conn = gerrit_util.CreateHttpConn( - cls.gerrit_instance.gerrit_host, path, reqtype='PUT', body=body) - jmsg = gerrit_util.ReadHttpJsonResponse(conn, accept_statuses=[200, 201]) - assert jmsg['name'] == name - - @classmethod - def _post_clone_bookkeeping(cls, clone_path): - config_path = os.path.join(clone_path, '.git', 'config') - cls.check_call( - ['git', 'config', '--file', config_path, 'user.email', cls.TEST_EMAIL]) - cls.check_call( - ['git', 'config', '--file', config_path, 'credential.helper', - 'store --file=%s' % cls.gerrit_instance.credential_file]) - - @classmethod - def _CloneProject(cls, name, path): - """Clone a project from the test gerrit server.""" - gi = cls.gerrit_instance - parent_dir = os.path.dirname(path) - if not os.path.exists(parent_dir): - os.makedirs(parent_dir) - url = '/'.join((gi.gerrit_url, name)) - cls.check_call(['git', 'clone', url, path]) - cls._post_clone_bookkeeping(path) - # Install commit-msg hook to add Change-Id lines. - hook_path = os.path.join(path, '.git', 'hooks', 'commit-msg') - cls.check_call(['curl', '-o', hook_path, - '/'.join((gi.gerrit_url, 'tools/hooks/commit-msg'))]) - os.chmod(hook_path, stat.S_IRWXU) - return path - - def cloneProject(self, name, path=None): - """Clone a project from the test gerrit server.""" - if path is None: - path = os.path.basename(name) - if path.endswith('.git'): - path = path[:-4] - path = os.path.join(self.tempdir, path) - return self._CloneProject(name, path) - - @classmethod - def _CreateCommit(cls, clone_path, fn=None, msg=None, text=None): - """Create a commit in the given git checkout.""" - if not fn: - fn = 'test-file.txt' - if not msg: - msg = 'Test Message' - if not text: - text = 'Another day, another dollar.' - fpath = os.path.join(clone_path, fn) - with open(fpath, 'a') as fh: - fh.write('%s\n' % text) - cls.check_call(['git', 'add', fn], cwd=clone_path) - cls.check_call(['git', 'commit', '-m', msg], cwd=clone_path) - return cls._GetCommit(clone_path) - - def createCommit(self, clone_path, fn=None, msg=None, text=None): - """Create a commit in the given git checkout.""" - clone_path = os.path.join(self.tempdir, clone_path) - return self._CreateCommit(clone_path, fn, msg, text) - - @classmethod - def _GetCommit(cls, clone_path, ref='HEAD'): - """Get the sha1 and change-id for a ref in the git checkout.""" - log_proc = cls.check_output(['git', 'log', '-n', '1', ref], cwd=clone_path) - sha1 = None - change_id = None - for line in log_proc.splitlines(): - match = cls.COMMIT_RE.match(line) - if match: - sha1 = match.group(1) - continue - match = cls.CHANGEID_RE.match(line) - if match: - change_id = match.group(1) - continue - assert sha1 - assert change_id - return (sha1, change_id) - - def getCommit(self, clone_path, ref='HEAD'): - """Get the sha1 and change-id for a ref in the git checkout.""" - clone_path = os.path.join(self.tempdir, clone_path) - return self._GetCommit(clone_path, ref) - - @classmethod - def _UploadChange(cls, clone_path, branch='master', remote='origin'): - """Create a gerrit CL from the HEAD of a git checkout.""" - cls.check_call( - ['git', 'push', remote, 'HEAD:refs/for/%s' % branch], cwd=clone_path) - - def uploadChange(self, clone_path, branch='master', remote='origin'): - """Create a gerrit CL from the HEAD of a git checkout.""" - clone_path = os.path.join(self.tempdir, clone_path) - self._UploadChange(clone_path, branch, remote) - - @classmethod - def _PushBranch(cls, clone_path, branch='master'): - """Push a branch directly to gerrit, bypassing code review.""" - cls.check_call( - ['git', 'push', 'origin', 'HEAD:refs/heads/%s' % branch], - cwd=clone_path) - - def pushBranch(self, clone_path, branch='master'): - """Push a branch directly to gerrit, bypassing code review.""" - clone_path = os.path.join(self.tempdir, clone_path) - self._PushBranch(clone_path, branch) - - @classmethod - def createAccount(cls, name='Test User', email='test-user@test.org', - password=None, groups=None): - """Create a new user account on gerrit.""" - username = email.partition('@')[0] - gerrit_cmd = 'gerrit create-account %s --full-name "%s" --email %s' % ( - username, name, email) - if password: - gerrit_cmd += ' --http-password "%s"' % password - if groups: - gerrit_cmd += ' '.join(['--group %s' % x for x in groups]) - ssh_cmd = ['ssh', '-p', cls.gerrit_instance.ssh_port, - '-i', cls.gerrit_instance.ssh_ident, - '-o', 'NoHostAuthenticationForLocalhost=yes', - '-o', 'StrictHostKeyChecking=no', - '%s@localhost' % cls.TEST_USERNAME, gerrit_cmd] - cls.check_call(ssh_cmd) - - @classmethod - def _stop_gerrit(cls, gerrit_instance): - """Stops the running gerrit instance and deletes it.""" - try: - # This should terminate the gerrit process. - cls.check_call(['bash', gerrit_instance.gerrit_exe, 'stop']) - finally: - try: - # cls.gerrit_pid should have already terminated. If it did, then - # os.waitpid will raise OSError. - os.waitpid(gerrit_instance.gerrit_pid, os.WNOHANG) - except OSError as e: - if e.errno == errno.ECHILD: - # If gerrit shut down cleanly, os.waitpid will land here. - # pylint: disable=lost-exception - return - - # If we get here, the gerrit process is still alive. Send the process - # SIGTERM for good measure. - try: - os.kill(gerrit_instance.gerrit_pid, signal.SIGTERM) - except OSError: - if e.errno == errno.ESRCH: - # os.kill raised an error because the process doesn't exist. Maybe - # gerrit shut down cleanly after all. - # pylint: disable=lost-exception - return - - # Announce that gerrit didn't shut down cleanly. - msg = 'Test gerrit server (pid=%d) did not shut down cleanly.' % ( - gerrit_instance.gerrit_pid) - print(msg, file=sys.stderr) - - @classmethod - def tearDownClass(cls): - gerrit_util.NETRC = cls.gerrit_util_netrc_orig - gerrit_util.GERRIT_PROTOCOL = cls.gerrit_util_protocol_orig - if TEARDOWN: - cls._stop_gerrit(cls.gerrit_instance) - shutil.rmtree(cls.gerrit_instance.gerrit_dir) - - -class RepoTestCase(GerritTestCase): - """Test class which runs in a repo checkout.""" - - REPO_URL = 'https://chromium.googlesource.com/external/repo' - MANIFEST_PROJECT = 'remotepath/manifest' - MANIFEST_TEMPLATE = """ - - - - - - - - - -""" - - @classmethod - def setUpClass(cls): - GerritTestCase.setUpClass() - gi = cls.gerrit_instance - - # Create local mirror of repo tool repository. - repo_mirror_path = os.path.join(gi.git_dir, 'repo.git') - cls.check_call( - ['git', 'clone', '--mirror', cls.REPO_URL, repo_mirror_path]) - - # Check out the top-level repo script; it will be used for invocation. - repo_clone_path = os.path.join(gi.gerrit_dir, 'tmp', 'repo') - cls.check_call(['git', 'clone', '-n', repo_mirror_path, repo_clone_path]) - cls.check_call( - ['git', 'checkout', 'origin/stable', 'repo'], cwd=repo_clone_path) - shutil.rmtree(os.path.join(repo_clone_path, '.git')) - cls.repo_exe = os.path.join(repo_clone_path, 'repo') - - # Create manifest repository. - cls.createProject(cls.MANIFEST_PROJECT) - clone_path = os.path.join(gi.gerrit_dir, 'tmp', 'manifest') - cls._CloneProject(cls.MANIFEST_PROJECT, clone_path) - manifest_path = os.path.join(clone_path, 'default.xml') - with open(manifest_path, 'w') as fh: - fh.write(cls.MANIFEST_TEMPLATE % gi.__dict__) - cls.check_call(['git', 'add', 'default.xml'], cwd=clone_path) - cls.check_call(['git', 'commit', '-m', 'Test manifest.'], cwd=clone_path) - cls._PushBranch(clone_path) - - # Create project repositories. - for i in xrange(1, 5): - proj = 'testproj%d' % i - cls.createProject('remotepath/%s' % proj) - clone_path = os.path.join(gi.gerrit_dir, 'tmp', proj) - cls._CloneProject('remotepath/%s' % proj, clone_path) - cls._CreateCommit(clone_path) - cls._PushBranch(clone_path, 'master') - - def setUp(self): - super(RepoTestCase, self).setUp() - manifest_url = '/'.join((self.gerrit_instance.gerrit_url, - self.MANIFEST_PROJECT)) - repo_url = '/'.join((self.gerrit_instance.gerrit_url, 'repo')) - self.check_call( - [self.repo_exe, 'init', '-u', manifest_url, '--repo-url', - repo_url, '--no-repo-verify'], cwd=self.tempdir) - self.check_call([self.repo_exe, 'sync'], cwd=self.tempdir) - for i in xrange(1, 5): - clone_path = os.path.join(self.tempdir, 'localpath', 'testproj%d' % i) - self._post_clone_bookkeeping(clone_path) - # Tell 'repo upload' to upload this project without prompting. - config_path = os.path.join(clone_path, '.git', 'config') - self.check_call( - ['git', 'config', '--file', config_path, 'review.%s.upload' % - self.gerrit_instance.gerrit_host, 'true']) - - @classmethod - def runRepo(cls, *args, **kwargs): - # Unfortunately, munging $HOME appears to be the only way to control the - # netrc file used by repo. - munged_home = os.path.join(cls.gerrit_instance.gerrit_dir, 'tmp') - if 'env' not in kwargs: - env = kwargs['env'] = os.environ.copy() - env['HOME'] = munged_home - else: - env.setdefault('HOME', munged_home) - args[0].insert(0, cls.repo_exe) - cls.check_call(*args, **kwargs) - - def uploadChange(self, clone_path, branch='master', remote='origin'): - review_host = self.check_output( - ['git', 'config', 'remote.%s.review' % remote], - cwd=clone_path).strip() - assert(review_host) - projectname = self.check_output( - ['git', 'config', 'remote.%s.projectname' % remote], - cwd=clone_path).strip() - assert(projectname) - GerritTestCase._UploadChange( - clone_path, branch=branch, remote='%s://%s/%s' % ( - gerrit_util.GERRIT_PROTOCOL, review_host, projectname)) diff --git a/testing_support/patches_data.py b/testing_support/patches_data.py deleted file mode 100644 index 1acd83265..000000000 --- a/testing_support/patches_data.py +++ /dev/null @@ -1,351 +0,0 @@ -# coding: utf-8 -# Copyright (c) 2012 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Samples patches to test patch.py.""" - - -class RAW(object): - PATCH = ( - 'Index: chrome/file.cc\n' - '===================================================================\n' - '--- chrome/file.cc\t(revision 74690)\n' - '+++ chrome/file.cc\t(working copy)\n' - '@@ -3,6 +3,7 @@ bb\n' - ' ccc\n' - ' dd\n' - ' e\n' - '+FOO!\n' - ' ff\n' - ' ggg\n' - ' hh\n') - - NEW = ( - '--- /dev/null\n' - '+++ foo\n' - '@@ -0,0 +1 @@\n' - '+bar\n') - - NEW_NOT_NULL = ( - '--- file_a\n' - '+++ file_a\n' - '@@ -0,0 +1 @@\n' - '+foo\n') - - MINIMAL_NEW = ( - '--- /dev/null\t2\n' - '+++ chrome/file.cc\tfoo\n') - - MINIMAL = ( - '--- file_a\n' - '+++ file_a\n') - - MINIMAL_RENAME = ( - '--- file_a\n' - '+++ file_b\n') - - DELETE = ( - '--- tools/clang_check/README.chromium\n' - '+++ /dev/null\n' - '@@ -1,1 +0,0 @@\n' - '-bar\n') - - MINIMAL_DELETE = ( - '--- chrome/file.cc\tbar\n' - '+++ /dev/null\tfoo\n') - - DELETE2 = ( - 'Index: browser/extensions/extension_sidebar_api.cc\n' - '===================================================================\n' - '--- browser/extensions/extension_sidebar_api.cc\t(revision 116830)\n' - '+++ browser/extensions/extension_sidebar_api.cc\t(working copy)\n' - '@@ -1,19 +0,0 @@\n' - '-// Copyright (c) 2011 The Chromium Authors. All rights reserved.\n' - '-// Use of this source code is governed by a BSD-style license that\n' - '-// found in the LICENSE file.\n' - '-\n' - '-#include "base/command_line.h"\n' - '-#include "chrome/browser/extensions/extension_apitest.h"\n' - '-#include "chrome/common/chrome_switches.h"\n' - '-\n' - '-class SidebarApiTest : public ExtensionApiTest {\n' - '- public:\n' - '- void SetUpCommandLine(CommandLine* command_line) {\n' - '- ExtensionApiTest::SetUpCommandLine(command_line);\n' - '- command_line->AppendSwitch(switches::Bleh);\n' - '- }\n' - '-};\n' - '-\n' - '-IN_PROC_BROWSER_TEST_F(SidebarApiTest, Sidebar) {\n' - '- ASSERT_TRUE(RunExtensionTest("sidebar")) << message_;\n' - '-}\n') - - # http://codereview.chromium.org/api/7530007/5001 - # http://codereview.chromium.org/download/issue7530007_5001_4011.diff - CRAP_ONLY = ( - 'Index: scripts/master/factory/skia/__init__.py\n' - '===================================================================\n') - - TWO_HUNKS = ( - 'Index: chrome/app/generated_resources.grd\n' - '===================================================================\n' - '--- chrome/app/generated_resources.grd\t(revision 116830)\n' - '+++ chrome/app/generated_resources.grd\t(working copy)\n' - '@@ -4169,9 +4169,6 @@\n' - ' \n' - ' Could not load options page \'$1\n' - '- \n' - '- Could not load sidebar page \'$1\n' - ' \n' - ' \n') - - # http://codereview.chromium.org/download/issue6287022_3001_4010.diff - RENAME = ( - 'Index: tools/run_local_server.sh\n' - 'diff --git a/tools/run_local_server.PY b/tools/run_local_server.sh\n' - 'similarity index 100%\n' - 'rename from tools/run_local_server.PY\n' - 'rename to tools/run_local_server.sh\n') - - COPY = ( - 'diff --git a/PRESUBMIT.py b/pp\n' - 'similarity index 100%\n' - 'copy from PRESUBMIT.py\n' - 'copy to pp\n') - - COPY_PARTIAL = ( - 'diff --git a/wtf b/wtf2\n' - 'similarity index 98%\n' - 'copy from wtf\n' - 'copy to wtf2\n' - 'index 79fbaf3..3560689 100755\n' - '--- a/wtf\n' - '+++ b/wtf2\n' - '@@ -1,4 +1,4 @@\n' - '-#!/usr/bin/env python\n' - '+#!/usr/bin/env python1.3\n' - ' # Copyright (c) 2010 The Chromium Authors. All rights reserved.\n' - ' # blah blah blah as\n' - ' # found in the LICENSE file.\n') - - NEW = ( - 'diff --git a/foo b/foo\n' - 'new file mode 100644\n' - 'index 0000000..5716ca5\n' - '--- /dev/null\n' - '+++ b/foo\n' - '@@ -0,0 +1 @@\n' - '+bar\n') - - NEW_EXE = ( - 'diff --git a/natsort_test.py b/natsort_test.py\n' - 'new file mode 100755\n' - '--- /dev/null\n' - '+++ b/natsort_test.py\n' - '@@ -0,0 +1,1 @@\n' - '+#!/usr/bin/env python\n') - - # To make sure the subdirectory was created as needed. - NEW_SUBDIR = ( - 'diff --git a/new_dir/subdir/new_file b/new_dir/subdir/new_file\n' - 'new file mode 100644\n' - '--- /dev/null\n' - '+++ b/new_dir/subdir/new_file\n' - '@@ -0,0 +1,2 @@\n' - '+A new file\n' - '+should exist.\n') - - NEW_MODE = ( - 'diff --git a/natsort_test.py b/natsort_test.py\n' - 'new file mode 100644\n' - '--- /dev/null\n' - '+++ b/natsort_test.py\n' - '@@ -0,0 +1,1 @@\n' - '+#!/usr/bin/env python\n') - - MODE_EXE = ( - 'diff --git a/git_cl/git-cl b/git_cl/git-cl\n' - 'old mode 100644\n' - 'new mode 100755\n') - - MODE_EXE_JUNK = ( - 'Index: Junk\n' - 'diff --git a/git_cl/git-cl b/git_cl/git-cl\n' - 'old mode 100644\n' - 'new mode 100755\n') - - NEW_NOT_EXECUTABLE = ( - 'diff --git a/build/android/ant/create.js b/build/android/ant/create.js\n' - 'new file mode 100644\n' - 'index 0000000000000000000..542a89e978feada38dd\n' - '--- /dev/null\n' - '+++ b/build/android/ant/create.js\n' - '@@ -0,0 +1,1 @@\n' - '+// Copyright (c) 2012 The Chromium Authors. All rights reserved.\n' - ) - - FOUR_HUNKS = ( - 'Index: presubmit_support.py\n' - 'diff --git a/presubmit_support.py b/presubmit_support.py\n' - 'index 52416d3f..d56512f2 100755\n' - '--- a/presubmit_support.py\n' - '+++ b/presubmit_support.py\n' - '@@ -558,6 +558,7 @@ class SvnAffectedFile(AffectedFile):\n' - ' AffectedFile.__init__(self, *args, **kwargs)\n' - ' self._server_path = None\n' - ' self._is_text_file = None\n' - '+ self._diff = None\n' - ' \n' - ' def ServerPath(self):\n' - ' if self._server_path is None:\n' - '@@ -598,8 +599,10 @@ class SvnAffectedFile(AffectedFile):\n' - ' return self._is_text_file\n' - ' \n' - ' def GenerateScmDiff(self):\n' - '- return scm.SVN.GenerateDiff(\n' - '- [self.LocalPath()], self._local_root, False, None)\n' - '+ if self._diff is None:\n' - '+ self._diff = scm.SVN.GenerateDiff(\n' - '+ [self.LocalPath()], self._local_root, False, None)\n' - '+ return self._diff\n' - ' \n' - ' \n' - ' class GitAffectedFile(AffectedFile):\n' - '@@ -611,6 +614,7 @@ class GitAffectedFile(AffectedFile):\n' - ' AffectedFile.__init__(self, *args, **kwargs)\n' - ' self._server_path = None\n' - ' self._is_text_file = None\n' - '+ self._diff = None\n' - ' \n' - ' def ServerPath(self):\n' - ' if self._server_path is None:\n' - '@@ -645,7 +649,10 @@ class GitAffectedFile(AffectedFile):\n' - ' return self._is_text_file\n' - ' \n' - ' def GenerateScmDiff(self):\n' - '- return scm.GIT.GenerateDiff(self._local_root, files=[self.Lo...\n' - '+ if self._diff is None:\n' - '+ self._diff = scm.GIT.GenerateDiff(\n' - '+ self._local_root, files=[self.LocalPath(),])\n' - '+ return self._diff\n' - ' \n' - ' \n' - ' class Change(object):\n') diff --git a/tests/checkout_test.py b/tests/checkout_test.py deleted file mode 100755 index 5aec9899c..000000000 --- a/tests/checkout_test.py +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2012 The Chromium 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 checkout.py.""" - -from __future__ import print_function - -import logging -import os -import shutil -import sys -import unittest -from xml.etree import ElementTree - -ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, os.path.dirname(ROOT_DIR)) - -from testing_support import fake_repos -from testing_support.patches_data import GIT, RAW - -import checkout -import patch -import subprocess2 - - -# pass -v to enable it. -DEBUGGING = False - -# A patch that will fail to apply. -BAD_PATCH = ''.join( - [l for l in GIT.PATCH.splitlines(True) if l.strip() != 'e']) - - -class FakeRepos(fake_repos.FakeReposBase): - TEST_GIT_REPO = 'repo_1' - - def populateGit(self): - """Creates a few revisions of changes files.""" - self._commit_git(self.TEST_GIT_REPO, self._git_tree()) - # Fix for the remote rejected error. For more details see: - # http://stackoverflow.com/questions/2816369/git-push-error-remote - subprocess2.check_output( - ['git', '--git-dir', - os.path.join(self.git_root, self.TEST_GIT_REPO, '.git'), - 'config', '--bool', 'core.bare', 'true']) - - assert os.path.isdir( - os.path.join(self.git_root, self.TEST_GIT_REPO, '.git')) - - @staticmethod - def _git_tree(): - fs = {} - fs['origin'] = 'git@1' - fs['extra'] = 'dummy\n' # new - fs['codereview.settings'] = ( - '# Test data\n' - 'bar: pouet\n') - fs['chrome/file.cc'] = ( - 'a\n' - 'bb\n' - 'ccc\n' - 'dd\n' - 'e\n' - 'ff\n' - 'ggg\n' - 'hh\n' - 'i\n' - 'jj\n' - 'kkk\n' - 'll\n' - 'm\n' - 'nn\n' - 'ooo\n' - 'pp\n' - 'q\n') - fs['chromeos/views/DOMui_menu_widget.h'] = ( - '// Copyright (c) 2010\n' - '// Use of this source code\n' - '// found in the LICENSE file.\n' - '\n' - '#ifndef DOM\n' - '#define DOM\n' - '#pragma once\n' - '\n' - '#include \n' - '#endif\n') - return fs - - -# pylint: disable=no-self-use -class BaseTest(fake_repos.FakeReposTestBase): - name = 'foo' - FAKE_REPOS_CLASS = FakeRepos - is_read_only = False - - def setUp(self): - super(BaseTest, self).setUp() - self._old_call = subprocess2.call - def redirect_call(args, **kwargs): - if not DEBUGGING: - kwargs.setdefault('stdout', subprocess2.PIPE) - kwargs.setdefault('stderr', subprocess2.STDOUT) - return self._old_call(args, **kwargs) - subprocess2.call = redirect_call - self.usr, self.pwd = self.FAKE_REPOS.USERS[0] - self.previous_log = None - - def tearDown(self): - subprocess2.call = self._old_call - super(BaseTest, self).tearDown() - - def get_patches(self): - return patch.PatchSet([ - patch.FilePatchDiff('new_dir/subdir/new_file', GIT.NEW_SUBDIR, []), - patch.FilePatchDiff('chrome/file.cc', GIT.PATCH, []), - # TODO(maruel): Test with is_new == False. - patch.FilePatchBinary('bin_file', '\x00', [], is_new=True), - patch.FilePatchDelete('extra', False), - ]) - - def get_trunk(self, modified): - raise NotImplementedError() - - def _check_base(self, co, root, expected): - raise NotImplementedError() - - def _check_exception(self, co, err_msg): - co.prepare(None) - try: - co.apply_patch([patch.FilePatchDiff('chrome/file.cc', BAD_PATCH, [])]) - self.fail() - except checkout.PatchApplicationFailed as e: - self.assertEquals(e.filename, 'chrome/file.cc') - self.assertEquals(e.status, err_msg) - - def _log(self): - raise NotImplementedError() - - def _test_process(self, co_lambda): - """Makes sure the process lambda is called correctly.""" - post_processors = [lambda *args: results.append(args)] - co = co_lambda(post_processors) - self.assertEquals(post_processors, co.post_processors) - co.prepare(None) - ps = self.get_patches() - results = [] - co.apply_patch(ps) - expected_co = getattr(co, 'checkout', co) - # Because of ReadOnlyCheckout. - expected = [(expected_co, p) for p in ps.patches] - self.assertEquals(len(expected), len(results)) - self.assertEquals(expected, results) - - def _check_move(self, co): - """Makes sure file moves are handled correctly.""" - co.prepare(None) - patchset = patch.PatchSet([ - patch.FilePatchDelete('chromeos/views/DOMui_menu_widget.h', False), - patch.FilePatchDiff( - 'chromeos/views/webui_menu_widget.h', GIT.RENAME_PARTIAL, []), - ]) - co.apply_patch(patchset) - # Make sure chromeos/views/DOMui_menu_widget.h is deleted and - # chromeos/views/webui_menu_widget.h is correctly created. - root = os.path.join(self.root_dir, self.name) - tree = self.get_trunk(False) - del tree['chromeos/views/DOMui_menu_widget.h'] - tree['chromeos/views/webui_menu_widget.h'] = ( - '// Copyright (c) 2011\n' - '// Use of this source code\n' - '// found in the LICENSE file.\n' - '\n' - '#ifndef WEB\n' - '#define WEB\n' - '#pragma once\n' - '\n' - '#include \n' - '#endif\n') - #print patchset[0].get() - #print fake_repos.read_tree(root) - self.assertTree(tree, root) - - -class GitBaseTest(BaseTest): - def setUp(self): - super(GitBaseTest, self).setUp() - self.enabled = self.FAKE_REPOS.set_up_git() - self.assertTrue(self.enabled) - self.previous_log = self._log() - - # pylint: disable=arguments-differ - def _log(self, log_from_local_repo=False): - if log_from_local_repo: - repo_root = os.path.join(self.root_dir, self.name) - else: - repo_root = os.path.join(self.FAKE_REPOS.git_root, - self.FAKE_REPOS.TEST_GIT_REPO) - out = subprocess2.check_output( - ['git', - '--git-dir', - os.path.join(repo_root, '.git'), - 'log', '--pretty=format:"%H%x09%ae%x09%ad%x09%s"', - '--max-count=1']).strip('"') - if out and len(out.split()) != 0: - revision = out.split()[0] - else: - return {'revision': 0} - - return { - 'revision': revision, - 'author': out.split()[1], - 'msg': out.split()[-1], - } - - def _check_base(self, co, root, expected): - read_only = isinstance(co, checkout.ReadOnlyCheckout) - self.assertEquals(read_only, self.is_read_only) - if not read_only: - self.FAKE_REPOS.git_dirty = True - - self.assertEquals(root, co.project_path) - git_rev = co.prepare(None) - self.assertEquals(unicode, type(git_rev)) - self.assertEquals(self.previous_log['revision'], git_rev) - self.assertEquals('pouet', co.get_settings('bar')) - self.assertTree(self.get_trunk(False), root) - patches = self.get_patches() - co.apply_patch(patches) - self.assertEquals( - ['bin_file', 'chrome/file.cc', 'new_dir/subdir/new_file', 'extra'], - patches.filenames) - - # Hackish to verify _branches() internal function. - # pylint: disable=protected-access - self.assertEquals( - (['master', 'working_branch'], 'working_branch'), - co._branches()) - - # Verify that the patch is applied even for read only checkout. - self.assertTree(self.get_trunk(True), root) - fake_author = self.FAKE_REPOS.USERS[1][0] - revision = co.commit(u'msg', fake_author) - # Nothing changed. - self.assertTree(self.get_trunk(True), root) - - if read_only: - self.assertEquals('FAKE', revision) - self.assertEquals(self.previous_log['revision'], co.prepare(None)) - # Changes should be reverted now. - self.assertTree(self.get_trunk(False), root) - expected = self.previous_log - else: - self.assertEquals(self._log()['revision'], revision) - self.assertEquals(self._log()['revision'], co.prepare(None)) - self.assertTree(self.get_trunk(True), root) - expected = self._log() - - actual = self._log(log_from_local_repo=True) - self.assertEquals(expected, actual) - - def get_trunk(self, modified): - tree = {} - for k, v in self.FAKE_REPOS.git_hashes[ - self.FAKE_REPOS.TEST_GIT_REPO][1][1].iteritems(): - assert k not in tree - tree[k] = v - - if modified: - content_lines = tree['chrome/file.cc'].splitlines(True) - tree['chrome/file.cc'] = ''.join( - content_lines[0:5] + ['FOO!\n'] + content_lines[5:]) - tree['bin_file'] = '\x00' - del tree['extra'] - tree['new_dir/subdir/new_file'] = 'A new file\nshould exist.\n' - return tree - - def _test_prepare(self, co): - print(co.prepare(None)) - - -class GitCheckout(GitBaseTest): - def _get_co(self, post_processors): - self.assertNotEqual(False, post_processors) - return checkout.GitCheckout( - root_dir=self.root_dir, - project_name=self.name, - remote_branch='master', - git_url=os.path.join(self.FAKE_REPOS.git_root, - self.FAKE_REPOS.TEST_GIT_REPO), - commit_user=self.usr, - post_processors=post_processors) - - def testAll(self): - root = os.path.join(self.root_dir, self.name) - self._check_base(self._get_co(None), root, None) - - @unittest.skip('flaky') - def testException(self): - self._check_exception( - self._get_co(None), - 'While running git apply --index -3 -p1;\n fatal: corrupt patch at ' - 'line 12\n') - - def testProcess(self): - self._test_process(self._get_co) - - def _testPrepare(self): - self._test_prepare(self._get_co(None)) - - def testMove(self): - co = self._get_co(None) - self._check_move(co) - out = subprocess2.check_output( - ['git', 'diff', '--staged', '--name-status', '--no-renames'], - cwd=co.project_path) - out = sorted(out.splitlines()) - expected = sorted( - [ - 'A\tchromeos/views/webui_menu_widget.h', - 'D\tchromeos/views/DOMui_menu_widget.h', - ]) - self.assertEquals(expected, out) - - -if __name__ == '__main__': - if '-v' in sys.argv: - DEBUGGING = True - logging.basicConfig( - level=logging.DEBUG, - format='%(levelname)5s %(filename)15s(%(lineno)3d): %(message)s') - else: - logging.basicConfig( - level=logging.ERROR, - format='%(levelname)5s %(filename)15s(%(lineno)3d): %(message)s') - unittest.main() diff --git a/tests/git_cl_test.py b/tests/git_cl_test.py index 84da76321..e892d801a 100755 --- a/tests/git_cl_test.py +++ b/tests/git_cl_test.py @@ -83,19 +83,6 @@ class PresubmitMock(object): return True -class GitCheckoutMock(object): - def __init__(self, *args, **kwargs): - pass - - @staticmethod - def reset(): - GitCheckoutMock.conflict = False - - def apply_patch(self, p): - if GitCheckoutMock.conflict: - raise Exception('failed') - - class WatchlistsMock(object): def __init__(self, _): pass @@ -673,8 +660,6 @@ class TestGitCl(TestCase): self.mock(git_cl, 'write_json', lambda path, contents: self._mocked_call('write_json', path, contents)) self.mock(git_cl.presubmit_support, 'DoPresubmitChecks', PresubmitMock) - self.mock(git_cl.checkout, 'GitCheckout', GitCheckoutMock) - GitCheckoutMock.reset() self.mock(git_cl.watchlists, 'Watchlists', WatchlistsMock) self.mock(git_cl.auth, 'get_authenticator_for_host', AuthenticatorMock) self.mock(git_cl.gerrit_util, 'GetChangeDetail', diff --git a/tests/patch_test.py b/tests/patch_test.py deleted file mode 100755 index 2414a90ce..000000000 --- a/tests/patch_test.py +++ /dev/null @@ -1,556 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 -# Copyright (c) 2012 The Chromium 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 patch.py.""" - -import logging -import os -import posixpath -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from testing_support.patches_data import GIT, RAW - -import patch - - -class PatchTest(unittest.TestCase): - def _check_patch(self, - p, - filename, - diff, - source_filename=None, - is_binary=False, - is_delete=False, - is_git_diff=False, - is_new=False, - patchlevel=0, - svn_properties=None, - nb_hunks=None): - self.assertEquals(p.filename, filename) - self.assertEquals(p.source_filename, source_filename) - self.assertEquals(p.is_binary, is_binary) - self.assertEquals(p.is_delete, is_delete) - if hasattr(p, 'is_git_diff'): - self.assertEquals(p.is_git_diff, is_git_diff) - self.assertEquals(p.is_new, is_new) - if hasattr(p, 'patchlevel'): - self.assertEquals(p.patchlevel, patchlevel) - if diff: - if is_binary: - self.assertEquals(p.get(), diff) - else: - self.assertEquals(p.get(True), diff) - if hasattr(p, 'hunks'): - self.assertEquals(len(p.hunks), nb_hunks) - else: - self.assertEquals(None, nb_hunks) - if hasattr(p, 'svn_properties'): - self.assertEquals(p.svn_properties, svn_properties or []) - - def testFilePatchDelete(self): - p = patch.FilePatchDelete('foo', False) - self._check_patch(p, 'foo', None, is_delete=True) - - def testFilePatchDeleteBin(self): - p = patch.FilePatchDelete('foo', True) - self._check_patch(p, 'foo', None, is_delete=True, is_binary=True) - - def testFilePatchBinary(self): - p = patch.FilePatchBinary('foo', 'data', [], is_new=False) - self._check_patch(p, 'foo', 'data', is_binary=True) - - def testFilePatchBinaryNew(self): - p = patch.FilePatchBinary('foo', 'data', [], is_new=True) - self._check_patch(p, 'foo', 'data', is_binary=True, is_new=True) - - def testFilePatchDiff(self): - p = patch.FilePatchDiff('chrome/file.cc', RAW.PATCH, []) - self._check_patch(p, 'chrome/file.cc', RAW.PATCH, nb_hunks=1) - - def testDifferent(self): - name = 'master/unittests/data/processes-summary.dat' - p = patch.FilePatchDiff(name, RAW.DIFFERENT, []) - self._check_patch(p, name, RAW.DIFFERENT, nb_hunks=1) - - def testFilePatchDiffHeaderMode(self): - p = patch.FilePatchDiff('git_cl/git-cl', GIT.MODE_EXE, []) - self._check_patch( - p, 'git_cl/git-cl', GIT.MODE_EXE, is_git_diff=True, patchlevel=1, - svn_properties=[('svn:executable', '.')], nb_hunks=0) - - def testFilePatchDiffHeaderModeIndex(self): - p = patch.FilePatchDiff('git_cl/git-cl', GIT.MODE_EXE_JUNK, []) - self._check_patch( - p, 'git_cl/git-cl', GIT.MODE_EXE_JUNK, is_git_diff=True, patchlevel=1, - svn_properties=[('svn:executable', '.')], nb_hunks=0) - - def testFilePatchDiffHeaderNotExecutable(self): - p = patch.FilePatchDiff( - 'build/android/ant/create.js', GIT.NEW_NOT_EXECUTABLE, []) - self._check_patch( - p, 'build/android/ant/create.js', GIT.NEW_NOT_EXECUTABLE, - is_git_diff=True, patchlevel=1, is_new=True, - nb_hunks=1) - - def testFilePatchDiffSvnNew(self): - # The code path is different for git and svn. - p = patch.FilePatchDiff('foo', RAW.NEW, []) - self._check_patch(p, 'foo', RAW.NEW, is_new=True, nb_hunks=1) - - def testFilePatchDiffGitNew(self): - # The code path is different for git and svn. - p = patch.FilePatchDiff('foo', GIT.NEW, []) - self._check_patch( - p, 'foo', GIT.NEW, is_new=True, is_git_diff=True, patchlevel=1, - nb_hunks=1) - - def testSvn(self): - # Should not throw. - p = patch.FilePatchDiff('chrome/file.cc', RAW.PATCH, []) - lines = RAW.PATCH.splitlines(True) - header = ''.join(lines[:4]) - hunks = ''.join(lines[4:]) - self.assertEquals(header, p.diff_header) - self.assertEquals(hunks, p.diff_hunks) - self.assertEquals(RAW.PATCH, p.get(True)) - self.assertEquals(RAW.PATCH, p.get(False)) - - def testSvnNew(self): - p = patch.FilePatchDiff('chrome/file.cc', RAW.MINIMAL_NEW, []) - self.assertEquals(RAW.MINIMAL_NEW, p.diff_header) - self.assertEquals('', p.diff_hunks) - self.assertEquals(RAW.MINIMAL_NEW, p.get(True)) - self.assertEquals(RAW.MINIMAL_NEW, p.get(False)) - - def testSvnDelete(self): - p = patch.FilePatchDiff('chrome/file.cc', RAW.MINIMAL_DELETE, []) - self.assertEquals(RAW.MINIMAL_DELETE, p.diff_header) - self.assertEquals('', p.diff_hunks) - self.assertEquals(RAW.MINIMAL_DELETE, p.get(True)) - self.assertEquals(RAW.MINIMAL_DELETE, p.get(False)) - - def testSvnRename(self): - p = patch.FilePatchDiff('file_b', RAW.MINIMAL_RENAME, []) - self.assertEquals(RAW.MINIMAL_RENAME, p.diff_header) - self.assertEquals('', p.diff_hunks) - self.assertEquals(RAW.MINIMAL_RENAME, p.get(True)) - self.assertEquals('--- file_b\n+++ file_b\n', p.get(False)) - - def testRelPath(self): - patches = patch.PatchSet([ - patch.FilePatchDiff('pp', GIT.COPY, []), - patch.FilePatchDiff( - 'chromeos\\views/webui_menu_widget.h', GIT.RENAME_PARTIAL, []), - patch.FilePatchDiff('tools/run_local_server.sh', GIT.RENAME, []), - patch.FilePatchBinary('bar', 'data', [], is_new=False), - patch.FilePatchDiff('chrome/file.cc', RAW.PATCH, []), - patch.FilePatchDiff('foo', GIT.NEW, []), - patch.FilePatchDelete('other/place/foo', True), - patch.FilePatchDiff( - 'tools\\clang_check/README.chromium', GIT.DELETE, []), - ]) - expected = [ - 'pp', - 'chromeos/views/webui_menu_widget.h', - 'tools/run_local_server.sh', - 'bar', - 'chrome/file.cc', - 'foo', - 'other/place/foo', - 'tools/clang_check/README.chromium', - ] - self.assertEquals(expected, patches.filenames) - - # Test patch #4. - orig_name = patches.patches[4].filename - orig_source_name = patches.patches[4].source_filename or orig_name - patches.set_relpath(os.path.join('a', 'bb')) - # Expect posixpath all the time. - expected = [posixpath.join('a', 'bb', x) for x in expected] - self.assertEquals(expected, patches.filenames) - # Make sure each header is updated accordingly. - header = [] - new_name = posixpath.join('a', 'bb', orig_name) - new_source_name = posixpath.join('a', 'bb', orig_source_name) - for line in RAW.PATCH.splitlines(True): - if line.startswith('@@'): - break - if line[:3] == '---': - line = line.replace(orig_source_name, new_source_name) - if line[:3] == '+++': - line = line.replace(orig_name, new_name) - header.append(line) - header = ''.join(header) - self.assertEquals(header, patches.patches[4].diff_header) - - def testRelPathEmpty(self): - patches = patch.PatchSet([ - patch.FilePatchDiff('chrome\\file.cc', RAW.PATCH, []), - patch.FilePatchDelete('other\\place\\foo', True), - ]) - patches.set_relpath('') - self.assertEquals( - ['chrome/file.cc', 'other/place/foo'], - [f.filename for f in patches]) - self.assertEquals([None, None], [f.source_filename for f in patches]) - - def testBackSlash(self): - mangled_patch = RAW.PATCH.replace('chrome/', 'chrome\\') - patches = patch.PatchSet([ - patch.FilePatchDiff('chrome\\file.cc', mangled_patch, []), - patch.FilePatchDelete('other\\place\\foo', True), - ]) - expected = ['chrome/file.cc', 'other/place/foo'] - self.assertEquals(expected, patches.filenames) - self.assertEquals(RAW.PATCH, patches.patches[0].get(True)) - self.assertEquals(RAW.PATCH, patches.patches[0].get(False)) - - def testTwoHunks(self): - name = 'chrome/app/generated_resources.grd' - p = patch.FilePatchDiff(name, RAW.TWO_HUNKS, []) - self._check_patch(p, name, RAW.TWO_HUNKS, nb_hunks=2) - - def testGitThreeHunks(self): - p = patch.FilePatchDiff('presubmit_support.py', GIT.FOUR_HUNKS, []) - self._check_patch( - p, 'presubmit_support.py', GIT.FOUR_HUNKS, is_git_diff=True, - patchlevel=1, - nb_hunks=4) - - def testDelete(self): - p = patch.FilePatchDiff('tools/clang_check/README.chromium', RAW.DELETE, []) - self._check_patch( - p, 'tools/clang_check/README.chromium', RAW.DELETE, is_delete=True, - nb_hunks=1) - - def testDelete2(self): - name = 'browser/extensions/extension_sidebar_api.cc' - p = patch.FilePatchDiff(name, RAW.DELETE2, []) - self._check_patch(p, name, RAW.DELETE2, is_delete=True, nb_hunks=1) - - def testGitDelete(self): - p = patch.FilePatchDiff('tools/clang_check/README.chromium', GIT.DELETE, []) - self._check_patch( - p, 'tools/clang_check/README.chromium', GIT.DELETE, is_delete=True, - is_git_diff=True, patchlevel=1, nb_hunks=1) - - def testGitRename(self): - p = patch.FilePatchDiff('tools/run_local_server.sh', GIT.RENAME, []) - self._check_patch( - p, - 'tools/run_local_server.sh', - GIT.RENAME, - is_git_diff=True, - patchlevel=1, - source_filename='tools/run_local_server.PY', - is_new=True, - nb_hunks=0) - - def testGitRenamePartial(self): - p = patch.FilePatchDiff( - 'chromeos/views/webui_menu_widget.h', GIT.RENAME_PARTIAL, []) - self._check_patch( - p, - 'chromeos/views/webui_menu_widget.h', - GIT.RENAME_PARTIAL, - source_filename='chromeos/views/DOMui_menu_widget.h', - is_git_diff=True, - patchlevel=1, - is_new=True, - nb_hunks=1) - - def testGitCopy(self): - p = patch.FilePatchDiff('pp', GIT.COPY, []) - self._check_patch( - p, 'pp', GIT.COPY, is_git_diff=True, patchlevel=1, - source_filename='PRESUBMIT.py', is_new=True, nb_hunks=0) - - def testOnlyHeader(self): - p = patch.FilePatchDiff('file_a', RAW.MINIMAL, []) - self._check_patch(p, 'file_a', RAW.MINIMAL, nb_hunks=0) - - def testSmallest(self): - p = patch.FilePatchDiff('file_a', RAW.NEW_NOT_NULL, []) - self._check_patch(p, 'file_a', RAW.NEW_NOT_NULL, is_new=True, nb_hunks=1) - - def testRenameOnlyHeader(self): - p = patch.FilePatchDiff('file_b', RAW.MINIMAL_RENAME, []) - self._check_patch( - p, 'file_b', RAW.MINIMAL_RENAME, source_filename='file_a', is_new=True, - nb_hunks=0) - - def testUnicodeFilenameGet(self): - p = patch.FilePatchDiff(u'filé_b', RAW.RENAME_UTF8, []) - self._check_patch( - p, u'filé_b', RAW.RENAME_UTF8, source_filename=u'file_à', is_new=True, - nb_hunks=1) - self.assertTrue(isinstance(p.get(False), str)) - p.set_relpath('foo') - self.assertTrue(isinstance(p.get(False), str)) - self.assertEquals(u'foo/file_à'.encode('utf-8'), p.source_filename_utf8) - self.assertEquals(u'foo/file_à', p.source_filename) - self.assertEquals(u'foo/filé_b'.encode('utf-8'), p.filename_utf8) - self.assertEquals(u'foo/filé_b', p.filename) - - def testGitCopyPartial(self): - p = patch.FilePatchDiff('wtf2', GIT.COPY_PARTIAL, []) - self._check_patch( - p, 'wtf2', GIT.COPY_PARTIAL, source_filename='wtf', is_git_diff=True, - patchlevel=1, is_new=True, nb_hunks=1) - - def testGitCopyPartialAsSvn(self): - p = patch.FilePatchDiff('wtf2', GIT.COPY_PARTIAL, []) - # TODO(maruel): Improve processing. - diff = ( - 'diff --git a/wtf2 b/wtf22\n' - 'similarity index 98%\n' - 'copy from wtf2\n' - 'copy to wtf22\n' - 'index 79fbaf3..3560689 100755\n' - '--- a/wtf2\n' - '+++ b/wtf22\n' - '@@ -1,4 +1,4 @@\n' - '-#!/usr/bin/env python\n' - '+#!/usr/bin/env python1.3\n' - ' # Copyright (c) 2010 The Chromium Authors. All rights reserved.\n' - ' # blah blah blah as\n' - ' # found in the LICENSE file.\n') - self.assertEquals(diff, p.get(False)) - - def testGitNewExe(self): - p = patch.FilePatchDiff('natsort_test.py', GIT.NEW_EXE, []) - self._check_patch( - p, - 'natsort_test.py', - GIT.NEW_EXE, - is_new=True, - is_git_diff=True, - patchlevel=1, - svn_properties=[('svn:executable', '.')], - nb_hunks=1) - - def testGitNewMode(self): - p = patch.FilePatchDiff('natsort_test.py', GIT.NEW_MODE, []) - self._check_patch( - p, 'natsort_test.py', GIT.NEW_MODE, is_new=True, is_git_diff=True, - patchlevel=1, nb_hunks=1) - - def testPatchsetOrder(self): - # Deletes must be last. - # File renames/move/copy must be first. - patches = [ - patch.FilePatchDiff('chrome/file.cc', RAW.PATCH, []), - patch.FilePatchDiff( - 'tools\\clang_check/README.chromium', GIT.DELETE, []), - patch.FilePatchDiff('tools/run_local_server.sh', GIT.RENAME, []), - patch.FilePatchDiff( - 'chromeos\\views/webui_menu_widget.h', GIT.RENAME_PARTIAL, []), - patch.FilePatchDiff('pp', GIT.COPY, []), - patch.FilePatchDiff('foo', GIT.NEW, []), - patch.FilePatchDelete('other/place/foo', True), - patch.FilePatchBinary('bar', 'data', [], is_new=False), - ] - expected = [ - 'pp', - 'chromeos/views/webui_menu_widget.h', - 'tools/run_local_server.sh', - 'bar', - 'chrome/file.cc', - 'foo', - 'other/place/foo', - 'tools/clang_check/README.chromium', - ] - patchset = patch.PatchSet(patches) - self.assertEquals(expected, patchset.filenames) - - def testGitPatch(self): - p = patch.FilePatchDiff('chrome/file.cc', GIT.PATCH, []) - self._check_patch( - p, 'chrome/file.cc', GIT.PATCH, is_git_diff=True, patchlevel=1, - nb_hunks=1) - - def testGitPatchShortHunkHeader(self): - p = patch.FilePatchDiff( - 'chrome/browser/api/OWNERS', GIT.PATCH_SHORT_HUNK_HEADER, []) - self._check_patch( - p, 'chrome/browser/api/OWNERS', GIT.PATCH_SHORT_HUNK_HEADER, - is_git_diff=True, patchlevel=1, nb_hunks=1) - - -class PatchTestFail(unittest.TestCase): - # All patches that should throw. - def testFilePatchDelete(self): - self.assertFalse(hasattr(patch.FilePatchDelete('foo', False), 'get')) - - def testFilePatchDeleteBin(self): - self.assertFalse(hasattr(patch.FilePatchDelete('foo', True), 'get')) - - def testFilePatchDiffBad(self): - try: - patch.FilePatchDiff('foo', 'data', []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - - def testFilePatchDiffEmpty(self): - try: - patch.FilePatchDiff('foo', '', []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - - def testFilePatchDiffNone(self): - try: - patch.FilePatchDiff('foo', None, []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - - def testFilePatchBadDiffName(self): - try: - patch.FilePatchDiff('foo', RAW.PATCH, []) - self.fail() - except patch.UnsupportedPatchFormat as e: - self.assertEquals( - "Can't process patch for file foo.\nUnexpected diff: chrome/file.cc.", - str(e)) - - def testFilePatchDiffBadHeader(self): - try: - diff = ( - '+++ b/foo\n' - '@@ -0,0 +1 @@\n' - '+bar\n') - patch.FilePatchDiff('foo', diff, []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - - def testFilePatchDiffBadGitHeader(self): - try: - diff = ( - 'diff --git a/foo b/foo\n' - '+++ b/foo\n' - '@@ -0,0 +1 @@\n' - '+bar\n') - patch.FilePatchDiff('foo', diff, []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - - def testFilePatchDiffBadHeaderReversed(self): - try: - diff = ( - '+++ b/foo\n' - '--- b/foo\n' - '@@ -0,0 +1 @@\n' - '+bar\n') - patch.FilePatchDiff('foo', diff, []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - - def testFilePatchDiffGitBadHeaderReversed(self): - try: - diff = ( - 'diff --git a/foo b/foo\n' - '+++ b/foo\n' - '--- b/foo\n' - '@@ -0,0 +1 @@\n' - '+bar\n') - patch.FilePatchDiff('foo', diff, []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - - def testFilePatchDiffInvalidGit(self): - try: - patch.FilePatchDiff('svn_utils_test.txt', ( - 'diff --git a/tests/svn_utils_test_data/svn_utils_test.txt ' - 'b/tests/svn_utils_test_data/svn_utils_test.txt\n' - 'index 0e4de76..8320059 100644\n' - '--- a/svn_utils_test.txt\n' - '+++ b/svn_utils_test.txt\n' - '@@ -3,6 +3,7 @@ bb\n' - 'ccc\n' - 'dd\n' - 'e\n' - '+FOO!\n' - 'ff\n' - 'ggg\n' - 'hh\n'), - []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - try: - patch.FilePatchDiff('svn_utils_test2.txt', ( - 'diff --git a/svn_utils_test_data/svn_utils_test.txt ' - 'b/svn_utils_test.txt\n' - 'index 0e4de76..8320059 100644\n' - '--- a/svn_utils_test.txt\n' - '+++ b/svn_utils_test.txt\n' - '@@ -3,6 +3,7 @@ bb\n' - 'ccc\n' - 'dd\n' - 'e\n' - '+FOO!\n' - 'ff\n' - 'ggg\n' - 'hh\n'), - []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - - def testRelPathBad(self): - patches = patch.PatchSet([ - patch.FilePatchDiff('chrome\\file.cc', RAW.PATCH, []), - patch.FilePatchDelete('other\\place\\foo', True), - ]) - try: - patches.set_relpath('..') - self.fail() - except patch.UnsupportedPatchFormat: - pass - - def testInverted(self): - try: - patch.FilePatchDiff( - 'file_a', '+++ file_a\n--- file_a\n@@ -0,0 +1 @@\n+foo\n', []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - - def testInvertedOnlyHeader(self): - try: - patch.FilePatchDiff('file_a', '+++ file_a\n--- file_a\n', []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - - def testBadHunkCommas(self): - try: - patch.FilePatchDiff( - 'file_a', - '--- file_a\n' - '+++ file_a\n' - '@@ -0,,0 +1 @@\n' - '+foo\n', - []) - self.fail() - except patch.UnsupportedPatchFormat: - pass - - -if __name__ == '__main__': - logging.basicConfig(level= - [logging.WARNING, logging.INFO, logging.DEBUG][ - min(2, sys.argv.count('-v'))]) - unittest.main()