depot_tools: Remove unused files.
- appengine_mapper.py - checkout.py - patch.py - testing_support/gerrit_test_case.py - testing_support/patches_data.py - tests/checkout_test.py - tests/patch_test.py Bug: 984182 Change-Id: I2d1ccb1dc41d7034f63043aa87bca3bca4e18294 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1727401 Reviewed-by: Robbie Iannucci <iannucci@chromium.org> Commit-Queue: Edward Lesmes <ehmaldonado@chromium.org>changes/01/1727401/6
parent
c0758331ea
commit
364640e249
@ -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)
|
@ -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 <ref> without a colon is equivalent to <ref>: when
|
||||
# pulling/fetching, so it merges <ref> 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 <email>" 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
|
@ -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:
|
||||
|
||||
<garbage>
|
||||
diff --git (|a/)<filename> (|b/)<filename>
|
||||
<similarity>
|
||||
<filemode changes>
|
||||
<index>
|
||||
<copy|rename from>
|
||||
<copy|rename to>
|
||||
--- <filename>
|
||||
+++ <filename>
|
||||
|
||||
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]
|
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<manifest>
|
||||
<remote name="remote1"
|
||||
fetch="%(gerrit_url)s"
|
||||
review="%(gerrit_host)s" />
|
||||
<remote name="remote2"
|
||||
fetch="%(gerrit_url)s"
|
||||
review="%(gerrit_host)s" />
|
||||
<default revision="refs/heads/master" remote="remote1" sync-j="1" />
|
||||
<project remote="remote1" path="localpath/testproj1" name="remotepath/testproj1" />
|
||||
<project remote="remote1" path="localpath/testproj2" name="remotepath/testproj2" />
|
||||
<project remote="remote2" path="localpath/testproj3" name="remotepath/testproj3" />
|
||||
<project remote="remote2" path="localpath/testproj4" name="remotepath/testproj4" />
|
||||
</manifest>
|
||||
"""
|
||||
|
||||
@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))
|
@ -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'
|
||||
' <message name="IDS_EXTENSION_LOAD_OPTIONS_PAGE_FAILED" desc="">\n'
|
||||
' Could not load options page \'<ph name="OPTIONS_PAGE">$1<ex....\n'
|
||||
' </message>\n'
|
||||
'- <message name="IDS_EXTENSION_LOAD_SIDEBAR_PAGE_FAILED" desc="">\n'
|
||||
'- Could not load sidebar page \'<ph name="SIDEBAR_PAGE">$1<e...\n'
|
||||
'- </message>\n'
|
||||
' <if expr="is_win">\n'
|
||||
' <message name="IDS_EXTENSION_UNPACK_FAILED" desc="On wind...\n'
|
||||
' Can not unpack extension. To safely unpack an extensio...\n'
|
||||
'@@ -5593,9 +5590,6 @@\n'
|
||||
' <message name="IDS_ACCNAME_WEB_CONTENTS" desc="The acces...\n'
|
||||
' Web Contents\n'
|
||||
' </message>\n'
|
||||
'- <message name="IDS_ACCNAME_SIDE_BAR" desc="The acces...\n'
|
||||
'- Sidebar\n'
|
||||
'- </message>\n'
|
||||
' \n'
|
||||
' <!-- Browser Hung Plugin Detector -->\n'
|
||||
' <message name="IDS_UNKNOWN_PLUGIN_NAME" ...\n')
|
||||
|
||||
# http://codereview.chromium.org/download/issue9091003_9005_8009.diff
|
||||
DIFFERENT = (
|
||||
'Index: master/unittests/data/processes-summary.dat\n'
|
||||
'===================================================================\n'
|
||||
'--- master/unittests/data/processes-summary.dat\t(revision 116240)\n'
|
||||
'+++ master/unittests/data/processes-summary.dat\t(working copy)\n'
|
||||
'@@ -1 +1 @@\n'
|
||||
'-{"traces": {"1t_proc": ["2.0", "0.0"], "1t_proc_ref": ["1.0", ...\n'
|
||||
'+{"traces": {"1t_proc": ["2.0", "0.0"], "1t_proc_ref": ["1.0", ...\n')
|
||||
|
||||
RENAME_UTF8 = (
|
||||
u'--- file_à\n'
|
||||
u'+++ filé_b\n'
|
||||
u'@@ -3,6 +3,7 @@ bb\n'
|
||||
u' ccc\n'
|
||||
u' ddé\n'
|
||||
u' e\n'
|
||||
u'+FÔÒ!\n'
|
||||
u' ff\n'
|
||||
u' ggg\n'
|
||||
u' hh\n').encode('utf-8')
|
||||
|
||||
|
||||
class GIT(object):
|
||||
"""Sample patches generated by git diff."""
|
||||
PATCH = (
|
||||
'diff --git a/chrome/file.cc b/chrome/file.cc\n'
|
||||
'index 0e4de76..8320059 100644\n'
|
||||
'--- a/chrome/file.cc\n'
|
||||
'+++ b/chrome/file.cc\n'
|
||||
'@@ -3,6 +3,7 @@ bb\n'
|
||||
' ccc\n'
|
||||
' dd\n'
|
||||
' e\n'
|
||||
'+FOO!\n'
|
||||
' ff\n'
|
||||
' ggg\n'
|
||||
' hh\n')
|
||||
|
||||
# http://codereview.chromium.org/download/issue10868039_12001_10003.diff
|
||||
PATCH_SHORT_HUNK_HEADER = (
|
||||
'Index: chrome/browser/api/OWNERS\n'
|
||||
'diff --git a/chrome/browser/api/OWNERS b/chrome/browser/api/OWNERS\n'
|
||||
'--- a/chrome/browser/api/OWNERS\n'
|
||||
'+++ b/chrome/browser/api/OWNERS\n'
|
||||
'@@ -1 +1,2 @@\n'
|
||||
'+erikwright@chromium.org\n'
|
||||
' joi@chromium.org\n')
|
||||
|
||||
# http://codereview.chromium.org/download/issue6368055_22_29.diff
|
||||
DELETE = (
|
||||
'Index: tools/clang_check/README.chromium\n'
|
||||
'diff --git a/tools/clang_check/README.chromium '
|
||||
'b/tools/clang_check/README.chromium\n'
|
||||
'deleted file mode 100644\n'
|
||||
'index fcaa7e0e94bb604a026c4f478fecb1c5796f5413..'
|
||||
'0000000000000000000000000000000000000000\n'
|
||||
'--- a/tools/clang_check/README.chromium\n'
|
||||
'+++ /dev/null\n'
|
||||
'@@ -1,9 +0,0 @@\n'
|
||||
'-These are terrible, terrible hacks.\n'
|
||||
'-\n'
|
||||
'-They are meant \n'
|
||||
'-AND doing the normal \n'
|
||||
'-run during normal \n'
|
||||
'-build system to do a syntax check.\n'
|
||||
'-\n'
|
||||
'-Also see\n'
|
||||
'\n')
|
||||
|
||||
# http://codereview.chromium.org/download/issue8508015_6001_7001.diff
|
||||
DELETE_EMPTY = (
|
||||
'Index: tests/__init__.py\n'
|
||||
'diff --git a/tests/__init__.py b/tests/__init__.py\n'
|
||||
'deleted file mode 100644\n'
|
||||
'index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..'
|
||||
'0000000000000000000000000000000000000000\n')
|
||||
|
||||
# http://codereview.chromium.org/download/issue6250123_3013_6010.diff
|
||||
RENAME_PARTIAL = (
|
||||
'Index: chromeos/views/webui_menu_widget.h\n'
|
||||
'diff --git a/chromeos/views/DOMui_menu_widget.h '
|
||||
'b/chromeos/views/webui_menu_widget.h\n'
|
||||
'similarity index 79%\n'
|
||||
'rename from chromeos/views/DOMui_menu_widget.h\n'
|
||||
'rename to chromeos/views/webui_menu_widget.h\n'
|
||||
'index 095d4c474fd9718f5aebfa41a1ccb2d951356d41..'
|
||||
'157925075434b590e8acaaf605a64f24978ba08b 100644\n'
|
||||
'--- a/chromeos/views/DOMui_menu_widget.h\n'
|
||||
'+++ b/chromeos/views/webui_menu_widget.h\n'
|
||||
'@@ -1,9 +1,9 @@\n'
|
||||
'-// Copyright (c) 2010\n'
|
||||
'+// Copyright (c) 2011\n'
|
||||
' // Use of this source code\n'
|
||||
' // found in the LICENSE file.\n'
|
||||
' \n'
|
||||
'-#ifndef DOM\n'
|
||||
'-#define DOM\n'
|
||||
'+#ifndef WEB\n'
|
||||
'+#define WEB\n'
|
||||
' #pragma once\n'
|
||||
' \n'
|
||||
' #include <string>\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')
|
@ -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 <string>\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 <string>\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()
|
@ -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()
|
Loading…
Reference in New Issue