scm_unittest.py revision 65f03d4f644ce73618e5f4f50dd694b26f55ae12
1# Copyright (C) 2009 Google Inc. All rights reserved.
2# Copyright (C) 2009 Apple Inc. All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are
6# met:
7#
8#    * Redistributions of source code must retain the above copyright
9# notice, this list of conditions and the following disclaimer.
10#    * Redistributions in binary form must reproduce the above
11# copyright notice, this list of conditions and the following disclaimer
12# in the documentation and/or other materials provided with the
13# distribution.
14#    * Neither the name of Google Inc. nor the names of its
15# contributors may be used to endorse or promote products derived from
16# this software without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30from __future__ import with_statement
31
32import base64
33import codecs
34import getpass
35import os
36import os.path
37import re
38import stat
39import sys
40import subprocess
41import tempfile
42import unittest
43import urllib
44import shutil
45
46from datetime import date
47from webkitpy.common.checkout.api import Checkout
48from webkitpy.common.checkout.scm import detect_scm_system, SCM, SVN, Git, CheckoutNeedsUpdate, commit_error_handler, AuthenticationError, AmbiguousCommitError, find_checkout_root, default_scm
49from webkitpy.common.config.committers import Committer  # FIXME: This should not be needed
50from webkitpy.common.net.bugzilla import Attachment # FIXME: This should not be needed
51from webkitpy.common.system.executive import Executive, run_command, ScriptError
52from webkitpy.common.system.outputcapture import OutputCapture
53from webkitpy.tool.mocktool import MockExecutive
54
55# Eventually we will want to write tests which work for both scms. (like update_webkit, changed_files, etc.)
56# Perhaps through some SCMTest base-class which both SVNTest and GitTest inherit from.
57
58# FIXME: This should be unified into one of the executive.py commands!
59# Callers could use run_and_throw_if_fail(args, cwd=cwd, quiet=True)
60def run_silent(args, cwd=None):
61    # Note: Not thread safe: http://bugs.python.org/issue2320
62    process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
63    process.communicate() # ignore output
64    exit_code = process.wait()
65    if exit_code:
66        raise ScriptError('Failed to run "%s"  exit_code: %d  cwd: %s' % (args, exit_code, cwd))
67
68
69def write_into_file_at_path(file_path, contents, encoding="utf-8"):
70    if encoding:
71        with codecs.open(file_path, "w", encoding) as file:
72            file.write(contents)
73    else:
74        with open(file_path, "w") as file:
75            file.write(contents)
76
77
78def read_from_path(file_path, encoding="utf-8"):
79    with codecs.open(file_path, "r", encoding) as file:
80        return file.read()
81
82
83def _make_diff(command, *args):
84    # We use this wrapper to disable output decoding. diffs should be treated as
85    # binary files since they may include text files of multiple differnet encodings.
86    return run_command([command, "diff"] + list(args), decode_output=False)
87
88
89def _svn_diff(*args):
90    return _make_diff("svn", *args)
91
92
93def _git_diff(*args):
94    return _make_diff("git", *args)
95
96
97# Exists to share svn repository creation code between the git and svn tests
98class SVNTestRepository:
99    @classmethod
100    def _svn_add(cls, path):
101        run_command(["svn", "add", path])
102
103    @classmethod
104    def _svn_commit(cls, message):
105        run_command(["svn", "commit", "--quiet", "--message", message])
106
107    @classmethod
108    def _setup_test_commits(cls, test_object):
109        # Add some test commits
110        os.chdir(test_object.svn_checkout_path)
111
112        write_into_file_at_path("test_file", "test1")
113        cls._svn_add("test_file")
114        cls._svn_commit("initial commit")
115
116        write_into_file_at_path("test_file", "test1test2")
117        # This used to be the last commit, but doing so broke
118        # GitTest.test_apply_git_patch which use the inverse diff of the last commit.
119        # svn-apply fails to remove directories in Git, see:
120        # https://bugs.webkit.org/show_bug.cgi?id=34871
121        os.mkdir("test_dir")
122        # Slash should always be the right path separator since we use cygwin on Windows.
123        test_file3_path = "test_dir/test_file3"
124        write_into_file_at_path(test_file3_path, "third file")
125        cls._svn_add("test_dir")
126        cls._svn_commit("second commit")
127
128        write_into_file_at_path("test_file", "test1test2test3\n")
129        write_into_file_at_path("test_file2", "second file")
130        cls._svn_add("test_file2")
131        cls._svn_commit("third commit")
132
133        # This 4th commit is used to make sure that our patch file handling
134        # code correctly treats patches as binary and does not attempt to
135        # decode them assuming they're utf-8.
136        write_into_file_at_path("test_file", u"latin1 test: \u00A0\n", "latin1")
137        write_into_file_at_path("test_file2", u"utf-8 test: \u00A0\n", "utf-8")
138        cls._svn_commit("fourth commit")
139
140        # svn does not seem to update after commit as I would expect.
141        run_command(['svn', 'update'])
142
143    @classmethod
144    def setup(cls, test_object):
145        # Create an test SVN repository
146        test_object.svn_repo_path = tempfile.mkdtemp(suffix="svn_test_repo")
147        test_object.svn_repo_url = "file://%s" % test_object.svn_repo_path # Not sure this will work on windows
148        # git svn complains if we don't pass --pre-1.5-compatible, not sure why:
149        # Expected FS format '2'; found format '3' at /usr/local/libexec/git-core//git-svn line 1477
150        run_command(['svnadmin', 'create', '--pre-1.5-compatible', test_object.svn_repo_path])
151
152        # Create a test svn checkout
153        test_object.svn_checkout_path = tempfile.mkdtemp(suffix="svn_test_checkout")
154        run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url, test_object.svn_checkout_path])
155
156        # Create and checkout a trunk dir to match the standard svn configuration to match git-svn's expectations
157        os.chdir(test_object.svn_checkout_path)
158        os.mkdir('trunk')
159        cls._svn_add('trunk')
160        # We can add tags and branches as well if we ever need to test those.
161        cls._svn_commit('add trunk')
162
163        # Change directory out of the svn checkout so we can delete the checkout directory.
164        # _setup_test_commits will CD back to the svn checkout directory.
165        os.chdir('/')
166        run_command(['rm', '-rf', test_object.svn_checkout_path])
167        run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url + '/trunk', test_object.svn_checkout_path])
168
169        cls._setup_test_commits(test_object)
170
171    @classmethod
172    def tear_down(cls, test_object):
173        run_command(['rm', '-rf', test_object.svn_repo_path])
174        run_command(['rm', '-rf', test_object.svn_checkout_path])
175
176        # Now that we've deleted the checkout paths, cwddir may be invalid
177        # Change back to a valid directory so that later calls to os.getcwd() do not fail.
178        os.chdir(detect_scm_system(os.path.dirname(__file__)).checkout_root)
179
180
181class StandaloneFunctionsTest(unittest.TestCase):
182    """This class tests any standalone/top-level functions in the package."""
183    def setUp(self):
184        self.orig_cwd = os.path.abspath(os.getcwd())
185        self.orig_abspath = os.path.abspath
186
187        # We capture but ignore the output from stderr to reduce unwanted
188        # logging.
189        self.output = OutputCapture()
190        self.output.capture_output()
191
192    def tearDown(self):
193        os.chdir(self.orig_cwd)
194        os.path.abspath = self.orig_abspath
195        self.output.restore_output()
196
197    def test_find_checkout_root(self):
198        # Test from inside the tree.
199        os.chdir(sys.path[0])
200        dir = find_checkout_root()
201        self.assertNotEqual(dir, None)
202        self.assertTrue(os.path.exists(dir))
203
204        # Test from outside the tree.
205        os.chdir(os.path.expanduser("~"))
206        dir = find_checkout_root()
207        self.assertNotEqual(dir, None)
208        self.assertTrue(os.path.exists(dir))
209
210        # Mock out abspath() to test being not in a checkout at all.
211        os.path.abspath = lambda x: "/"
212        self.assertRaises(SystemExit, find_checkout_root)
213        os.path.abspath = self.orig_abspath
214
215    def test_default_scm(self):
216        # Test from inside the tree.
217        os.chdir(sys.path[0])
218        scm = default_scm()
219        self.assertNotEqual(scm, None)
220
221        # Test from outside the tree.
222        os.chdir(os.path.expanduser("~"))
223        dir = find_checkout_root()
224        self.assertNotEqual(dir, None)
225
226        # Mock out abspath() to test being not in a checkout at all.
227        os.path.abspath = lambda x: "/"
228        self.assertRaises(SystemExit, default_scm)
229        os.path.abspath = self.orig_abspath
230
231# For testing the SCM baseclass directly.
232class SCMClassTests(unittest.TestCase):
233    def setUp(self):
234        self.dev_null = open(os.devnull, "w") # Used to make our Popen calls quiet.
235
236    def tearDown(self):
237        self.dev_null.close()
238
239    def test_run_command_with_pipe(self):
240        input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null)
241        self.assertEqual(run_command(['grep', 'bar'], input=input_process.stdout), "bar\n")
242
243        # Test the non-pipe case too:
244        self.assertEqual(run_command(['grep', 'bar'], input="foo\nbar"), "bar\n")
245
246        command_returns_non_zero = ['/bin/sh', '--invalid-option']
247        # Test when the input pipe process fails.
248        input_process = subprocess.Popen(command_returns_non_zero, stdout=subprocess.PIPE, stderr=self.dev_null)
249        self.assertTrue(input_process.poll() != 0)
250        self.assertRaises(ScriptError, run_command, ['grep', 'bar'], input=input_process.stdout)
251
252        # Test when the run_command process fails.
253        input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null) # grep shows usage and calls exit(2) when called w/o arguments.
254        self.assertRaises(ScriptError, run_command, command_returns_non_zero, input=input_process.stdout)
255
256    def test_error_handlers(self):
257        git_failure_message="Merge conflict during commit: Your file or directory 'WebCore/ChangeLog' is probably out-of-date: resource out of date; try updating at /usr/local/libexec/git-core//git-svn line 469"
258        svn_failure_message="""svn: Commit failed (details follow):
259svn: File or directory 'ChangeLog' is out of date; try updating
260svn: resource out of date; try updating
261"""
262        command_does_not_exist = ['does_not_exist', 'invalid_option']
263        self.assertRaises(OSError, run_command, command_does_not_exist)
264        self.assertRaises(OSError, run_command, command_does_not_exist, error_handler=Executive.ignore_error)
265
266        command_returns_non_zero = ['/bin/sh', '--invalid-option']
267        self.assertRaises(ScriptError, run_command, command_returns_non_zero)
268        # Check if returns error text:
269        self.assertTrue(run_command(command_returns_non_zero, error_handler=Executive.ignore_error))
270
271        self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=git_failure_message))
272        self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=svn_failure_message))
273        self.assertRaises(ScriptError, commit_error_handler, ScriptError(output='blah blah blah'))
274
275
276# GitTest and SVNTest inherit from this so any test_ methods here will be run once for this class and then once for each subclass.
277class SCMTest(unittest.TestCase):
278    def _create_patch(self, patch_contents):
279        # FIXME: This code is brittle if the Attachment API changes.
280        attachment = Attachment({"bug_id": 12345}, None)
281        attachment.contents = lambda: patch_contents
282
283        joe_cool = Committer(name="Joe Cool", email_or_emails=None)
284        attachment.reviewer = lambda: joe_cool
285
286        return attachment
287
288    def _setup_webkittools_scripts_symlink(self, local_scm):
289        webkit_scm = detect_scm_system(os.path.dirname(os.path.abspath(__file__)))
290        webkit_scripts_directory = webkit_scm.scripts_directory()
291        local_scripts_directory = local_scm.scripts_directory()
292        os.mkdir(os.path.dirname(local_scripts_directory))
293        os.symlink(webkit_scripts_directory, local_scripts_directory)
294
295    # Tests which both GitTest and SVNTest should run.
296    # FIXME: There must be a simpler way to add these w/o adding a wrapper method to both subclasses
297
298    def _shared_test_changed_files(self):
299        write_into_file_at_path("test_file", "changed content")
300        self.assertEqual(self.scm.changed_files(), ["test_file"])
301        write_into_file_at_path("test_dir/test_file3", "new stuff")
302        self.assertEqual(self.scm.changed_files(), ["test_dir/test_file3", "test_file"])
303        old_cwd = os.getcwd()
304        os.chdir("test_dir")
305        # Validate that changed_files does not change with our cwd, see bug 37015.
306        self.assertEqual(self.scm.changed_files(), ["test_dir/test_file3", "test_file"])
307        os.chdir(old_cwd)
308
309    def _shared_test_added_files(self):
310        write_into_file_at_path("test_file", "changed content")
311        self.assertEqual(self.scm.added_files(), [])
312
313        write_into_file_at_path("added_file", "new stuff")
314        self.scm.add("added_file")
315
316        os.mkdir("added_dir")
317        write_into_file_at_path("added_dir/added_file2", "new stuff")
318        self.scm.add("added_dir")
319
320        # SVN reports directory changes, Git does not.
321        added_files = self.scm.added_files()
322        if "added_dir" in added_files:
323            added_files.remove("added_dir")
324        self.assertEqual(added_files, ["added_dir/added_file2", "added_file"])
325
326        # Test also to make sure clean_working_directory removes added files
327        self.scm.clean_working_directory()
328        self.assertEqual(self.scm.added_files(), [])
329        self.assertFalse(os.path.exists("added_file"))
330        self.assertFalse(os.path.exists("added_dir"))
331
332    def _shared_test_changed_files_for_revision(self):
333        # SVN reports directory changes, Git does not.
334        changed_files = self.scm.changed_files_for_revision(3)
335        if "test_dir" in changed_files:
336            changed_files.remove("test_dir")
337        self.assertEqual(changed_files, ["test_dir/test_file3", "test_file"])
338        self.assertEqual(sorted(self.scm.changed_files_for_revision(4)), sorted(["test_file", "test_file2"]))  # Git and SVN return different orders.
339        self.assertEqual(self.scm.changed_files_for_revision(2), ["test_file"])
340
341    def _shared_test_contents_at_revision(self):
342        self.assertEqual(self.scm.contents_at_revision("test_file", 3), "test1test2")
343        self.assertEqual(self.scm.contents_at_revision("test_file", 4), "test1test2test3\n")
344
345        # Verify that contents_at_revision returns a byte array, aka str():
346        self.assertEqual(self.scm.contents_at_revision("test_file", 5), u"latin1 test: \u00A0\n".encode("latin1"))
347        self.assertEqual(self.scm.contents_at_revision("test_file2", 5), u"utf-8 test: \u00A0\n".encode("utf-8"))
348
349        self.assertEqual(self.scm.contents_at_revision("test_file2", 4), "second file")
350        # Files which don't exist:
351        # Currently we raise instead of returning None because detecting the difference between
352        # "file not found" and any other error seems impossible with svn (git seems to expose such through the return code).
353        self.assertRaises(ScriptError, self.scm.contents_at_revision, "test_file2", 2)
354        self.assertRaises(ScriptError, self.scm.contents_at_revision, "does_not_exist", 2)
355
356    def _shared_test_revisions_changing_file(self):
357        self.assertEqual(self.scm.revisions_changing_file("test_file"), [5, 4, 3, 2])
358        self.assertRaises(ScriptError, self.scm.revisions_changing_file, "non_existent_file")
359
360    def _shared_test_committer_email_for_revision(self):
361        self.assertEqual(self.scm.committer_email_for_revision(3), getpass.getuser())  # Committer "email" will be the current user
362
363    def _shared_test_reverse_diff(self):
364        self._setup_webkittools_scripts_symlink(self.scm) # Git's apply_reverse_diff uses resolve-ChangeLogs
365        # Only test the simple case, as any other will end up with conflict markers.
366        self.scm.apply_reverse_diff('5')
367        self.assertEqual(read_from_path('test_file'), "test1test2test3\n")
368
369    def _shared_test_diff_for_revision(self):
370        # Patch formats are slightly different between svn and git, so just regexp for things we know should be there.
371        r3_patch = self.scm.diff_for_revision(4)
372        self.assertTrue(re.search('test3', r3_patch))
373        self.assertFalse(re.search('test4', r3_patch))
374        self.assertTrue(re.search('test2', r3_patch))
375        self.assertTrue(re.search('test2', self.scm.diff_for_revision(3)))
376
377    def _shared_test_svn_apply_git_patch(self):
378        self._setup_webkittools_scripts_symlink(self.scm)
379        git_binary_addition = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
380new file mode 100644
381index 0000000000000000000000000000000000000000..64a9532e7794fcd791f6f12157406d90
38260151690
383GIT binary patch
384literal 512
385zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
386zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
387zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
388zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
389zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
390zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
391zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
392z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A@16O26ud7H<QM=xl`toLKnz-3h@9c9q&wm|X
393z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
394ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
395
396literal 0
397HcmV?d00001
398
399"""
400        self.checkout.apply_patch(self._create_patch(git_binary_addition))
401        added = read_from_path('fizzbuzz7.gif', encoding=None)
402        self.assertEqual(512, len(added))
403        self.assertTrue(added.startswith('GIF89a'))
404        self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
405
406        # The file already exists.
407        self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_addition))
408
409        git_binary_modification = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
410index 64a9532e7794fcd791f6f12157406d9060151690..323fae03f4606ea9991df8befbb2fca7
411GIT binary patch
412literal 7
413OcmYex&reD$;sO8*F9L)B
414
415literal 512
416zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
417zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
418zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
419zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
420zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
421zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
422zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
423z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A@16O26ud7H<QM=xl`toLKnz-3h@9c9q&wm|X
424z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
425ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
426
427"""
428        self.checkout.apply_patch(self._create_patch(git_binary_modification))
429        modified = read_from_path('fizzbuzz7.gif', encoding=None)
430        self.assertEqual('foobar\n', modified)
431        self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
432
433        # Applying the same modification should fail.
434        self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_modification))
435
436        git_binary_deletion = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
437deleted file mode 100644
438index 323fae0..0000000
439GIT binary patch
440literal 0
441HcmV?d00001
442
443literal 7
444OcmYex&reD$;sO8*F9L)B
445
446"""
447        self.checkout.apply_patch(self._create_patch(git_binary_deletion))
448        self.assertFalse(os.path.exists('fizzbuzz7.gif'))
449        self.assertFalse('fizzbuzz7.gif' in self.scm.changed_files())
450
451        # Cannot delete again.
452        self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_deletion))
453
454    def _shared_test_add_recursively(self):
455        os.mkdir("added_dir")
456        write_into_file_at_path("added_dir/added_file", "new stuff")
457        self.scm.add("added_dir/added_file")
458        self.assertTrue("added_dir/added_file" in self.scm.added_files())
459
460
461class SVNTest(SCMTest):
462
463    @staticmethod
464    def _set_date_and_reviewer(changelog_entry):
465        # Joe Cool matches the reviewer set in SCMTest._create_patch
466        changelog_entry = changelog_entry.replace('REVIEWER_HERE', 'Joe Cool')
467        # svn-apply will update ChangeLog entries with today's date.
468        return changelog_entry.replace('DATE_HERE', date.today().isoformat())
469
470    def test_svn_apply(self):
471        first_entry = """2009-10-26  Eric Seidel  <eric@webkit.org>
472
473        Reviewed by Foo Bar.
474
475        Most awesome change ever.
476
477        * scm_unittest.py:
478"""
479        intermediate_entry = """2009-10-27  Eric Seidel  <eric@webkit.org>
480
481        Reviewed by Baz Bar.
482
483        A more awesomer change yet!
484
485        * scm_unittest.py:
486"""
487        one_line_overlap_patch = """Index: ChangeLog
488===================================================================
489--- ChangeLog	(revision 5)
490+++ ChangeLog	(working copy)
491@@ -1,5 +1,13 @@
492 2009-10-26  Eric Seidel  <eric@webkit.org>
493
494+        Reviewed by NOBODY (OOPS!).
495+
496+        Second most awesome change ever.
497+
498+        * scm_unittest.py:
499+
500+2009-10-26  Eric Seidel  <eric@webkit.org>
501+
502         Reviewed by Foo Bar.
503
504         Most awesome change ever.
505"""
506        one_line_overlap_entry = """DATE_HERE  Eric Seidel  <eric@webkit.org>
507
508        Reviewed by REVIEWER_HERE.
509
510        Second most awesome change ever.
511
512        * scm_unittest.py:
513"""
514        two_line_overlap_patch = """Index: ChangeLog
515===================================================================
516--- ChangeLog	(revision 5)
517+++ ChangeLog	(working copy)
518@@ -2,6 +2,14 @@
519
520         Reviewed by Foo Bar.
521
522+        Second most awesome change ever.
523+
524+        * scm_unittest.py:
525+
526+2009-10-26  Eric Seidel  <eric@webkit.org>
527+
528+        Reviewed by Foo Bar.
529+
530         Most awesome change ever.
531
532         * scm_unittest.py:
533"""
534        two_line_overlap_entry = """DATE_HERE  Eric Seidel  <eric@webkit.org>
535
536        Reviewed by Foo Bar.
537
538        Second most awesome change ever.
539
540        * scm_unittest.py:
541"""
542        write_into_file_at_path('ChangeLog', first_entry)
543        run_command(['svn', 'add', 'ChangeLog'])
544        run_command(['svn', 'commit', '--quiet', '--message', 'ChangeLog commit'])
545
546        # Patch files were created against just 'first_entry'.
547        # Add a second commit to make svn-apply have to apply the patches with fuzz.
548        changelog_contents = "%s\n%s" % (intermediate_entry, first_entry)
549        write_into_file_at_path('ChangeLog', changelog_contents)
550        run_command(['svn', 'commit', '--quiet', '--message', 'Intermediate commit'])
551
552        self._setup_webkittools_scripts_symlink(self.scm)
553        self.checkout.apply_patch(self._create_patch(one_line_overlap_patch))
554        expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(one_line_overlap_entry), changelog_contents)
555        self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
556
557        self.scm.revert_files(['ChangeLog'])
558        self.checkout.apply_patch(self._create_patch(two_line_overlap_patch))
559        expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(two_line_overlap_entry), changelog_contents)
560        self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
561
562    def setUp(self):
563        SVNTestRepository.setup(self)
564        os.chdir(self.svn_checkout_path)
565        self.scm = detect_scm_system(self.svn_checkout_path)
566        # For historical reasons, we test some checkout code here too.
567        self.checkout = Checkout(self.scm)
568
569    def tearDown(self):
570        SVNTestRepository.tear_down(self)
571
572    def test_detect_scm_system_relative_url(self):
573        scm = detect_scm_system(".")
574        # I wanted to assert that we got the right path, but there was some
575        # crazy magic with temp folder names that I couldn't figure out.
576        self.assertTrue(scm.checkout_root)
577
578    def test_create_patch_is_full_patch(self):
579        test_dir_path = os.path.join(self.svn_checkout_path, "test_dir2")
580        os.mkdir(test_dir_path)
581        test_file_path = os.path.join(test_dir_path, 'test_file2')
582        write_into_file_at_path(test_file_path, 'test content')
583        run_command(['svn', 'add', 'test_dir2'])
584
585        # create_patch depends on 'svn-create-patch', so make a dummy version.
586        scripts_path = os.path.join(self.svn_checkout_path, 'Tools', 'Scripts')
587        os.makedirs(scripts_path)
588        create_patch_path = os.path.join(scripts_path, 'svn-create-patch')
589        write_into_file_at_path(create_patch_path, '#!/bin/sh\necho $PWD') # We could pass -n to prevent the \n, but not all echo accept -n.
590        os.chmod(create_patch_path, stat.S_IXUSR | stat.S_IRUSR)
591
592        # Change into our test directory and run the create_patch command.
593        os.chdir(test_dir_path)
594        scm = detect_scm_system(test_dir_path)
595        self.assertEqual(scm.checkout_root, self.svn_checkout_path) # Sanity check that detection worked right.
596        patch_contents = scm.create_patch()
597        # Our fake 'svn-create-patch' returns $PWD instead of a patch, check that it was executed from the root of the repo.
598        self.assertEqual("%s\n" % os.path.realpath(scm.checkout_root), patch_contents) # Add a \n because echo adds a \n.
599
600    def test_detection(self):
601        scm = detect_scm_system(self.svn_checkout_path)
602        self.assertEqual(scm.display_name(), "svn")
603        self.assertEqual(scm.supports_local_commits(), False)
604
605    def test_apply_small_binary_patch(self):
606        patch_contents = """Index: test_file.swf
607===================================================================
608Cannot display: file marked as a binary type.
609svn:mime-type = application/octet-stream
610
611Property changes on: test_file.swf
612___________________________________________________________________
613Name: svn:mime-type
614   + application/octet-stream
615
616
617Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
618"""
619        expected_contents = base64.b64decode("Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==")
620        self._setup_webkittools_scripts_symlink(self.scm)
621        patch_file = self._create_patch(patch_contents)
622        self.checkout.apply_patch(patch_file)
623        actual_contents = read_from_path("test_file.swf", encoding=None)
624        self.assertEqual(actual_contents, expected_contents)
625
626    def test_apply_svn_patch(self):
627        scm = detect_scm_system(self.svn_checkout_path)
628        patch = self._create_patch(_svn_diff("-r5:4"))
629        self._setup_webkittools_scripts_symlink(scm)
630        Checkout(scm).apply_patch(patch)
631
632    def test_apply_svn_patch_force(self):
633        scm = detect_scm_system(self.svn_checkout_path)
634        patch = self._create_patch(_svn_diff("-r3:5"))
635        self._setup_webkittools_scripts_symlink(scm)
636        self.assertRaises(ScriptError, Checkout(scm).apply_patch, patch, force=True)
637
638    def test_commit_logs(self):
639        # Commits have dates and usernames in them, so we can't just direct compare.
640        self.assertTrue(re.search('fourth commit', self.scm.last_svn_commit_log()))
641        self.assertTrue(re.search('second commit', self.scm.svn_commit_log(3)))
642
643    def _shared_test_commit_with_message(self, username=None):
644        write_into_file_at_path('test_file', 'more test content')
645        commit_text = self.scm.commit_with_message("another test commit", username)
646        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6')
647
648        self.scm.dryrun = True
649        write_into_file_at_path('test_file', 'still more test content')
650        commit_text = self.scm.commit_with_message("yet another test commit", username)
651        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '0')
652
653    def test_commit_text_parsing(self):
654        self._shared_test_commit_with_message()
655
656    def test_commit_with_username(self):
657        self._shared_test_commit_with_message("dbates@webkit.org")
658
659    def test_commit_without_authorization(self):
660        self.scm.has_authorization_for_realm = lambda: False
661        self.assertRaises(AuthenticationError, self._shared_test_commit_with_message)
662
663    def test_has_authorization_for_realm(self):
664        scm = detect_scm_system(self.svn_checkout_path)
665        fake_home_dir = tempfile.mkdtemp(suffix="fake_home_dir")
666        svn_config_dir_path = os.path.join(fake_home_dir, ".subversion")
667        os.mkdir(svn_config_dir_path)
668        fake_webkit_auth_file = os.path.join(svn_config_dir_path, "fake_webkit_auth_file")
669        write_into_file_at_path(fake_webkit_auth_file, SVN.svn_server_realm)
670        self.assertTrue(scm.has_authorization_for_realm(home_directory=fake_home_dir))
671        os.remove(fake_webkit_auth_file)
672        os.rmdir(svn_config_dir_path)
673        os.rmdir(fake_home_dir)
674
675    def test_not_have_authorization_for_realm(self):
676        scm = detect_scm_system(self.svn_checkout_path)
677        fake_home_dir = tempfile.mkdtemp(suffix="fake_home_dir")
678        svn_config_dir_path = os.path.join(fake_home_dir, ".subversion")
679        os.mkdir(svn_config_dir_path)
680        self.assertFalse(scm.has_authorization_for_realm(home_directory=fake_home_dir))
681        os.rmdir(svn_config_dir_path)
682        os.rmdir(fake_home_dir)
683
684    def test_reverse_diff(self):
685        self._shared_test_reverse_diff()
686
687    def test_diff_for_revision(self):
688        self._shared_test_diff_for_revision()
689
690    def test_svn_apply_git_patch(self):
691        self._shared_test_svn_apply_git_patch()
692
693    def test_changed_files(self):
694        self._shared_test_changed_files()
695
696    def test_changed_files_for_revision(self):
697        self._shared_test_changed_files_for_revision()
698
699    def test_added_files(self):
700        self._shared_test_added_files()
701
702    def test_contents_at_revision(self):
703        self._shared_test_contents_at_revision()
704
705    def test_revisions_changing_file(self):
706        self._shared_test_revisions_changing_file()
707
708    def test_committer_email_for_revision(self):
709        self._shared_test_committer_email_for_revision()
710
711    def test_add_recursively(self):
712        self._shared_test_add_recursively()
713
714    def test_delete(self):
715        os.chdir(self.svn_checkout_path)
716        self.scm.delete("test_file")
717        self.assertTrue("test_file" in self.scm.deleted_files())
718
719    def test_propset_propget(self):
720        filepath = os.path.join(self.svn_checkout_path, "test_file")
721        expected_mime_type = "x-application/foo-bar"
722        self.scm.propset("svn:mime-type", expected_mime_type, filepath)
723        self.assertEqual(expected_mime_type, self.scm.propget("svn:mime-type", filepath))
724
725    def test_show_head(self):
726        write_into_file_at_path("test_file", u"Hello!", "utf-8")
727        SVNTestRepository._svn_commit("fourth commit")
728        self.assertEqual("Hello!", self.scm.show_head('test_file'))
729
730    def test_show_head_binary(self):
731        data = "\244"
732        write_into_file_at_path("binary_file", data, encoding=None)
733        self.scm.add("binary_file")
734        self.scm.commit_with_message("a test commit")
735        self.assertEqual(data, self.scm.show_head('binary_file'))
736
737    def do_test_diff_for_file(self):
738        write_into_file_at_path('test_file', 'some content')
739        self.scm.commit_with_message("a test commit")
740        diff = self.scm.diff_for_file('test_file')
741        self.assertEqual(diff, "")
742
743        write_into_file_at_path("test_file", "changed content")
744        diff = self.scm.diff_for_file('test_file')
745        self.assertTrue("-some content" in diff)
746        self.assertTrue("+changed content" in diff)
747
748    def clean_bogus_dir(self):
749        self.bogus_dir = self.scm._bogus_dir_name()
750        if os.path.exists(self.bogus_dir):
751            shutil.rmtree(self.bogus_dir)
752
753    def test_diff_for_file_with_existing_bogus_dir(self):
754        self.clean_bogus_dir()
755        os.mkdir(self.bogus_dir)
756        self.do_test_diff_for_file()
757        self.assertTrue(os.path.exists(self.bogus_dir))
758        shutil.rmtree(self.bogus_dir)
759
760    def test_diff_for_file_with_missing_bogus_dir(self):
761        self.clean_bogus_dir()
762        self.do_test_diff_for_file()
763        self.assertFalse(os.path.exists(self.bogus_dir))
764
765    def test_svn_lock(self):
766        svn_root_lock_path = ".svn/lock"
767        write_into_file_at_path(svn_root_lock_path, "", "utf-8")
768        # webkit-patch uses a Checkout object and runs update-webkit, just use svn update here.
769        self.assertRaises(ScriptError, run_command, ['svn', 'update'])
770        self.scm.clean_working_directory()
771        self.assertFalse(os.path.exists(svn_root_lock_path))
772        run_command(['svn', 'update'])  # Should succeed and not raise.
773
774
775class GitTest(SCMTest):
776
777    def setUp(self):
778        """Sets up fresh git repository with one commit. Then setups a second git
779        repo that tracks the first one."""
780        self.original_dir = os.getcwd()
781
782        self.untracking_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout2")
783        run_command(['git', 'init', self.untracking_checkout_path])
784
785        os.chdir(self.untracking_checkout_path)
786        write_into_file_at_path('foo_file', 'foo')
787        run_command(['git', 'add', 'foo_file'])
788        run_command(['git', 'commit', '-am', 'dummy commit'])
789        self.untracking_scm = detect_scm_system(self.untracking_checkout_path)
790
791        self.tracking_git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout")
792        run_command(['git', 'clone', '--quiet', self.untracking_checkout_path, self.tracking_git_checkout_path])
793        os.chdir(self.tracking_git_checkout_path)
794        self.tracking_scm = detect_scm_system(self.tracking_git_checkout_path)
795
796    def tearDown(self):
797        # Change back to a valid directory so that later calls to os.getcwd() do not fail.
798        os.chdir(self.original_dir)
799        run_command(['rm', '-rf', self.tracking_git_checkout_path])
800        run_command(['rm', '-rf', self.untracking_checkout_path])
801
802    def test_remote_branch_ref(self):
803        self.assertEqual(self.tracking_scm.remote_branch_ref(), 'refs/remotes/origin/master')
804
805        os.chdir(self.untracking_checkout_path)
806        self.assertRaises(ScriptError, self.untracking_scm.remote_branch_ref)
807
808    def test_multiple_remotes(self):
809        run_command(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote1'])
810        run_command(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote2'])
811        self.assertEqual(self.tracking_scm.remote_branch_ref(), 'remote1')
812
813class GitSVNTest(SCMTest):
814
815    def _setup_git_checkout(self):
816        self.git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout")
817        # --quiet doesn't make git svn silent, so we use run_silent to redirect output
818        run_silent(['git', 'svn', 'clone', '-T', 'trunk', self.svn_repo_url, self.git_checkout_path])
819        os.chdir(self.git_checkout_path)
820
821    def _tear_down_git_checkout(self):
822        # Change back to a valid directory so that later calls to os.getcwd() do not fail.
823        os.chdir(self.original_dir)
824        run_command(['rm', '-rf', self.git_checkout_path])
825
826    def setUp(self):
827        self.original_dir = os.getcwd()
828
829        SVNTestRepository.setup(self)
830        self._setup_git_checkout()
831        self.scm = detect_scm_system(self.git_checkout_path)
832        # For historical reasons, we test some checkout code here too.
833        self.checkout = Checkout(self.scm)
834
835    def tearDown(self):
836        SVNTestRepository.tear_down(self)
837        self._tear_down_git_checkout()
838
839    def test_detection(self):
840        scm = detect_scm_system(self.git_checkout_path)
841        self.assertEqual(scm.display_name(), "git")
842        self.assertEqual(scm.supports_local_commits(), True)
843
844    def test_read_git_config(self):
845        key = 'test.git-config'
846        value = 'git-config value'
847        run_command(['git', 'config', key, value])
848        self.assertEqual(self.scm.read_git_config(key), value)
849
850    def test_local_commits(self):
851        test_file = os.path.join(self.git_checkout_path, 'test_file')
852        write_into_file_at_path(test_file, 'foo')
853        run_command(['git', 'commit', '-a', '-m', 'local commit'])
854
855        self.assertEqual(len(self.scm.local_commits()), 1)
856
857    def test_discard_local_commits(self):
858        test_file = os.path.join(self.git_checkout_path, 'test_file')
859        write_into_file_at_path(test_file, 'foo')
860        run_command(['git', 'commit', '-a', '-m', 'local commit'])
861
862        self.assertEqual(len(self.scm.local_commits()), 1)
863        self.scm.discard_local_commits()
864        self.assertEqual(len(self.scm.local_commits()), 0)
865
866    def test_delete_branch(self):
867        new_branch = 'foo'
868
869        run_command(['git', 'checkout', '-b', new_branch])
870        self.assertEqual(run_command(['git', 'symbolic-ref', 'HEAD']).strip(), 'refs/heads/' + new_branch)
871
872        run_command(['git', 'checkout', '-b', 'bar'])
873        self.scm.delete_branch(new_branch)
874
875        self.assertFalse(re.search(r'foo', run_command(['git', 'branch'])))
876
877    def test_remote_merge_base(self):
878        # Diff to merge-base should include working-copy changes,
879        # which the diff to svn_branch.. doesn't.
880        test_file = os.path.join(self.git_checkout_path, 'test_file')
881        write_into_file_at_path(test_file, 'foo')
882
883        diff_to_common_base = _git_diff(self.scm.remote_branch_ref() + '..')
884        diff_to_merge_base = _git_diff(self.scm.remote_merge_base())
885
886        self.assertFalse(re.search(r'foo', diff_to_common_base))
887        self.assertTrue(re.search(r'foo', diff_to_merge_base))
888
889    def test_rebase_in_progress(self):
890        svn_test_file = os.path.join(self.svn_checkout_path, 'test_file')
891        write_into_file_at_path(svn_test_file, "svn_checkout")
892        run_command(['svn', 'commit', '--message', 'commit to conflict with git commit'], cwd=self.svn_checkout_path)
893
894        git_test_file = os.path.join(self.git_checkout_path, 'test_file')
895        write_into_file_at_path(git_test_file, "git_checkout")
896        run_command(['git', 'commit', '-a', '-m', 'commit to be thrown away by rebase abort'])
897
898        # --quiet doesn't make git svn silent, so use run_silent to redirect output
899        self.assertRaises(ScriptError, run_silent, ['git', 'svn', '--quiet', 'rebase']) # Will fail due to a conflict leaving us mid-rebase.
900
901        scm = detect_scm_system(self.git_checkout_path)
902        self.assertTrue(scm.rebase_in_progress())
903
904        # Make sure our cleanup works.
905        scm.clean_working_directory()
906        self.assertFalse(scm.rebase_in_progress())
907
908        # Make sure cleanup doesn't throw when no rebase is in progress.
909        scm.clean_working_directory()
910
911    def test_commitish_parsing(self):
912        scm = detect_scm_system(self.git_checkout_path)
913
914        # Multiple revisions are cherry-picked.
915        self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD~2'])), 1)
916        self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD', 'HEAD~2'])), 2)
917
918        # ... is an invalid range specifier
919        self.assertRaises(ScriptError, scm.commit_ids_from_commitish_arguments, ['trunk...HEAD'])
920
921    def test_commitish_order(self):
922        scm = detect_scm_system(self.git_checkout_path)
923
924        commit_range = 'HEAD~3..HEAD'
925
926        actual_commits = scm.commit_ids_from_commitish_arguments([commit_range])
927        expected_commits = []
928        expected_commits += reversed(run_command(['git', 'rev-list', commit_range]).splitlines())
929
930        self.assertEqual(actual_commits, expected_commits)
931
932    def test_apply_git_patch(self):
933        scm = detect_scm_system(self.git_checkout_path)
934        # We carefullly pick a diff which does not have a directory addition
935        # as currently svn-apply will error out when trying to remove directories
936        # in Git: https://bugs.webkit.org/show_bug.cgi?id=34871
937        patch = self._create_patch(_git_diff('HEAD..HEAD^'))
938        self._setup_webkittools_scripts_symlink(scm)
939        Checkout(scm).apply_patch(patch)
940
941    def test_apply_git_patch_force(self):
942        scm = detect_scm_system(self.git_checkout_path)
943        patch = self._create_patch(_git_diff('HEAD~2..HEAD'))
944        self._setup_webkittools_scripts_symlink(scm)
945        self.assertRaises(ScriptError, Checkout(scm).apply_patch, patch, force=True)
946
947    def test_commit_text_parsing(self):
948        write_into_file_at_path('test_file', 'more test content')
949        commit_text = self.scm.commit_with_message("another test commit")
950        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6')
951
952        self.scm.dryrun = True
953        write_into_file_at_path('test_file', 'still more test content')
954        commit_text = self.scm.commit_with_message("yet another test commit")
955        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '0')
956
957    def test_commit_with_message_working_copy_only(self):
958        write_into_file_at_path('test_file_commit1', 'more test content')
959        run_command(['git', 'add', 'test_file_commit1'])
960        scm = detect_scm_system(self.git_checkout_path)
961        commit_text = scm.commit_with_message("yet another test commit")
962
963        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
964        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
965        self.assertTrue(re.search(r'test_file_commit1', svn_log))
966
967    def _local_commit(self, filename, contents, message):
968        write_into_file_at_path(filename, contents)
969        run_command(['git', 'add', filename])
970        self.scm.commit_locally_with_message(message)
971
972    def _one_local_commit(self):
973        self._local_commit('test_file_commit1', 'more test content', 'another test commit')
974
975    def _one_local_commit_plus_working_copy_changes(self):
976        self._one_local_commit()
977        write_into_file_at_path('test_file_commit2', 'still more test content')
978        run_command(['git', 'add', 'test_file_commit2'])
979
980    def _two_local_commits(self):
981        self._one_local_commit()
982        self._local_commit('test_file_commit2', 'still more test content', 'yet another test commit')
983
984    def _three_local_commits(self):
985        self._local_commit('test_file_commit0', 'more test content', 'another test commit')
986        self._two_local_commits()
987
988    def test_revisions_changing_files_with_local_commit(self):
989        self._one_local_commit()
990        self.assertEquals(self.scm.revisions_changing_file('test_file_commit1'), [])
991
992    def test_commit_with_message(self):
993        self._one_local_commit_plus_working_copy_changes()
994        scm = detect_scm_system(self.git_checkout_path)
995        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "yet another test commit")
996        commit_text = scm.commit_with_message("yet another test commit", force_squash=True)
997
998        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
999        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1000        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1001        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1002
1003    def test_commit_with_message_git_commit(self):
1004        self._two_local_commits()
1005
1006        scm = detect_scm_system(self.git_checkout_path)
1007        commit_text = scm.commit_with_message("another test commit", git_commit="HEAD^")
1008        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1009
1010        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1011        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1012        self.assertFalse(re.search(r'test_file_commit2', svn_log))
1013
1014    def test_commit_with_message_git_commit_range(self):
1015        self._three_local_commits()
1016
1017        scm = detect_scm_system(self.git_checkout_path)
1018        commit_text = scm.commit_with_message("another test commit", git_commit="HEAD~2..HEAD")
1019        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1020
1021        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1022        self.assertFalse(re.search(r'test_file_commit0', svn_log))
1023        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1024        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1025
1026    def test_changed_files_working_copy_only(self):
1027        self._one_local_commit_plus_working_copy_changes()
1028        scm = detect_scm_system(self.git_checkout_path)
1029        commit_text = scm.commit_with_message("another test commit", git_commit="HEAD..")
1030        self.assertFalse(re.search(r'test_file_commit1', svn_log))
1031        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1032
1033    def test_commit_with_message_only_local_commit(self):
1034        self._one_local_commit()
1035        scm = detect_scm_system(self.git_checkout_path)
1036        commit_text = scm.commit_with_message("another test commit")
1037        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1038        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1039
1040    def test_commit_with_message_multiple_local_commits_and_working_copy(self):
1041        self._two_local_commits()
1042        write_into_file_at_path('test_file_commit1', 'working copy change')
1043        scm = detect_scm_system(self.git_checkout_path)
1044
1045        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "another test commit")
1046        commit_text = scm.commit_with_message("another test commit", force_squash=True)
1047
1048        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1049        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1050        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1051        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1052
1053    def test_commit_with_message_git_commit_and_working_copy(self):
1054        self._two_local_commits()
1055        write_into_file_at_path('test_file_commit1', 'working copy change')
1056        scm = detect_scm_system(self.git_checkout_path)
1057        self.assertRaises(ScriptError, scm.commit_with_message, "another test commit", git_commit="HEAD^")
1058
1059    def test_commit_with_message_multiple_local_commits_always_squash(self):
1060        self._two_local_commits()
1061        scm = detect_scm_system(self.git_checkout_path)
1062        scm._assert_can_squash = lambda working_directory_is_clean: True
1063        commit_text = scm.commit_with_message("yet another test commit")
1064        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1065
1066        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1067        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1068        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1069
1070    def test_commit_with_message_multiple_local_commits(self):
1071        self._two_local_commits()
1072        scm = detect_scm_system(self.git_checkout_path)
1073        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "yet another test commit")
1074        commit_text = scm.commit_with_message("yet another test commit", force_squash=True)
1075
1076        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1077
1078        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1079        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1080        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1081
1082    def test_commit_with_message_not_synced(self):
1083        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
1084        self._two_local_commits()
1085        scm = detect_scm_system(self.git_checkout_path)
1086        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "another test commit")
1087        commit_text = scm.commit_with_message("another test commit", force_squash=True)
1088
1089        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
1090
1091        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
1092        self.assertFalse(re.search(r'test_file2', svn_log))
1093        self.assertTrue(re.search(r'test_file_commit2', svn_log))
1094        self.assertTrue(re.search(r'test_file_commit1', svn_log))
1095
1096    def test_commit_with_message_not_synced_with_conflict(self):
1097        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
1098        self._local_commit('test_file2', 'asdf', 'asdf commit')
1099
1100        scm = detect_scm_system(self.git_checkout_path)
1101        # There's a conflict between trunk and the test_file2 modification.
1102        self.assertRaises(ScriptError, scm.commit_with_message, "another test commit", force_squash=True)
1103
1104    def test_remote_branch_ref(self):
1105        self.assertEqual(self.scm.remote_branch_ref(), 'refs/remotes/trunk')
1106
1107    def test_reverse_diff(self):
1108        self._shared_test_reverse_diff()
1109
1110    def test_diff_for_revision(self):
1111        self._shared_test_diff_for_revision()
1112
1113    def test_svn_apply_git_patch(self):
1114        self._shared_test_svn_apply_git_patch()
1115
1116    def test_create_patch_local_plus_working_copy(self):
1117        self._one_local_commit_plus_working_copy_changes()
1118        scm = detect_scm_system(self.git_checkout_path)
1119        patch = scm.create_patch()
1120        self.assertTrue(re.search(r'test_file_commit1', patch))
1121        self.assertTrue(re.search(r'test_file_commit2', patch))
1122
1123    def test_create_patch(self):
1124        self._one_local_commit_plus_working_copy_changes()
1125        scm = detect_scm_system(self.git_checkout_path)
1126        patch = scm.create_patch()
1127        self.assertTrue(re.search(r'test_file_commit2', patch))
1128        self.assertTrue(re.search(r'test_file_commit1', patch))
1129
1130    def test_create_patch_with_changed_files(self):
1131        self._one_local_commit_plus_working_copy_changes()
1132        scm = detect_scm_system(self.git_checkout_path)
1133        patch = scm.create_patch(changed_files=['test_file_commit2'])
1134        self.assertTrue(re.search(r'test_file_commit2', patch))
1135
1136    def test_create_patch_with_rm_and_changed_files(self):
1137        self._one_local_commit_plus_working_copy_changes()
1138        scm = detect_scm_system(self.git_checkout_path)
1139        os.remove('test_file_commit1')
1140        patch = scm.create_patch()
1141        patch_with_changed_files = scm.create_patch(changed_files=['test_file_commit1', 'test_file_commit2'])
1142        self.assertEquals(patch, patch_with_changed_files)
1143
1144    def test_create_patch_git_commit(self):
1145        self._two_local_commits()
1146        scm = detect_scm_system(self.git_checkout_path)
1147        patch = scm.create_patch(git_commit="HEAD^")
1148        self.assertTrue(re.search(r'test_file_commit1', patch))
1149        self.assertFalse(re.search(r'test_file_commit2', patch))
1150
1151    def test_create_patch_git_commit_range(self):
1152        self._three_local_commits()
1153        scm = detect_scm_system(self.git_checkout_path)
1154        patch = scm.create_patch(git_commit="HEAD~2..HEAD")
1155        self.assertFalse(re.search(r'test_file_commit0', patch))
1156        self.assertTrue(re.search(r'test_file_commit2', patch))
1157        self.assertTrue(re.search(r'test_file_commit1', patch))
1158
1159    def test_create_patch_working_copy_only(self):
1160        self._one_local_commit_plus_working_copy_changes()
1161        scm = detect_scm_system(self.git_checkout_path)
1162        patch = scm.create_patch(git_commit="HEAD..")
1163        self.assertFalse(re.search(r'test_file_commit1', patch))
1164        self.assertTrue(re.search(r'test_file_commit2', patch))
1165
1166    def test_create_patch_multiple_local_commits(self):
1167        self._two_local_commits()
1168        scm = detect_scm_system(self.git_checkout_path)
1169        patch = scm.create_patch()
1170        self.assertTrue(re.search(r'test_file_commit2', patch))
1171        self.assertTrue(re.search(r'test_file_commit1', patch))
1172
1173    def test_create_patch_not_synced(self):
1174        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
1175        self._two_local_commits()
1176        scm = detect_scm_system(self.git_checkout_path)
1177        patch = scm.create_patch()
1178        self.assertFalse(re.search(r'test_file2', patch))
1179        self.assertTrue(re.search(r'test_file_commit2', patch))
1180        self.assertTrue(re.search(r'test_file_commit1', patch))
1181
1182    def test_create_binary_patch(self):
1183        # Create a git binary patch and check the contents.
1184        scm = detect_scm_system(self.git_checkout_path)
1185        test_file_name = 'binary_file'
1186        test_file_path = os.path.join(self.git_checkout_path, test_file_name)
1187        file_contents = ''.join(map(chr, range(256)))
1188        write_into_file_at_path(test_file_path, file_contents, encoding=None)
1189        run_command(['git', 'add', test_file_name])
1190        patch = scm.create_patch()
1191        self.assertTrue(re.search(r'\nliteral 0\n', patch))
1192        self.assertTrue(re.search(r'\nliteral 256\n', patch))
1193
1194        # Check if we can apply the created patch.
1195        run_command(['git', 'rm', '-f', test_file_name])
1196        self._setup_webkittools_scripts_symlink(scm)
1197        self.checkout.apply_patch(self._create_patch(patch))
1198        self.assertEqual(file_contents, read_from_path(test_file_path, encoding=None))
1199
1200        # Check if we can create a patch from a local commit.
1201        write_into_file_at_path(test_file_path, file_contents, encoding=None)
1202        run_command(['git', 'add', test_file_name])
1203        run_command(['git', 'commit', '-m', 'binary diff'])
1204        patch_from_local_commit = scm.create_patch('HEAD')
1205        self.assertTrue(re.search(r'\nliteral 0\n', patch_from_local_commit))
1206        self.assertTrue(re.search(r'\nliteral 256\n', patch_from_local_commit))
1207
1208    def test_changed_files_local_plus_working_copy(self):
1209        self._one_local_commit_plus_working_copy_changes()
1210        scm = detect_scm_system(self.git_checkout_path)
1211        files = scm.changed_files()
1212        self.assertTrue('test_file_commit1' in files)
1213        self.assertTrue('test_file_commit2' in files)
1214
1215    def test_changed_files_git_commit(self):
1216        self._two_local_commits()
1217        scm = detect_scm_system(self.git_checkout_path)
1218        files = scm.changed_files(git_commit="HEAD^")
1219        self.assertTrue('test_file_commit1' in files)
1220        self.assertFalse('test_file_commit2' in files)
1221
1222    def test_changed_files_git_commit_range(self):
1223        self._three_local_commits()
1224        scm = detect_scm_system(self.git_checkout_path)
1225        files = scm.changed_files(git_commit="HEAD~2..HEAD")
1226        self.assertTrue('test_file_commit0' not in files)
1227        self.assertTrue('test_file_commit1' in files)
1228        self.assertTrue('test_file_commit2' in files)
1229
1230    def test_changed_files_working_copy_only(self):
1231        self._one_local_commit_plus_working_copy_changes()
1232        scm = detect_scm_system(self.git_checkout_path)
1233        files = scm.changed_files(git_commit="HEAD..")
1234        self.assertFalse('test_file_commit1' in files)
1235        self.assertTrue('test_file_commit2' in files)
1236
1237    def test_changed_files_multiple_local_commits(self):
1238        self._two_local_commits()
1239        scm = detect_scm_system(self.git_checkout_path)
1240        files = scm.changed_files()
1241        self.assertTrue('test_file_commit2' in files)
1242        self.assertTrue('test_file_commit1' in files)
1243
1244    def test_changed_files_not_synced(self):
1245        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
1246        self._two_local_commits()
1247        scm = detect_scm_system(self.git_checkout_path)
1248        files = scm.changed_files()
1249        self.assertFalse('test_file2' in files)
1250        self.assertTrue('test_file_commit2' in files)
1251        self.assertTrue('test_file_commit1' in files)
1252
1253    def test_changed_files_not_synced(self):
1254        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
1255        self._two_local_commits()
1256        scm = detect_scm_system(self.git_checkout_path)
1257        files = scm.changed_files()
1258        self.assertFalse('test_file2' in files)
1259        self.assertTrue('test_file_commit2' in files)
1260        self.assertTrue('test_file_commit1' in files)
1261
1262    def test_changed_files(self):
1263        self._shared_test_changed_files()
1264
1265    def test_changed_files_for_revision(self):
1266        self._shared_test_changed_files_for_revision()
1267
1268    def test_contents_at_revision(self):
1269        self._shared_test_contents_at_revision()
1270
1271    def test_revisions_changing_file(self):
1272        self._shared_test_revisions_changing_file()
1273
1274    def test_added_files(self):
1275        self._shared_test_added_files()
1276
1277    def test_committer_email_for_revision(self):
1278        self._shared_test_committer_email_for_revision()
1279
1280    def test_add_recursively(self):
1281        self._shared_test_add_recursively()
1282
1283    def test_delete(self):
1284        self._two_local_commits()
1285        self.scm.delete('test_file_commit1')
1286        self.assertTrue("test_file_commit1" in self.scm.deleted_files())
1287
1288    def test_to_object_name(self):
1289        relpath = 'test_file_commit1'
1290        fullpath = os.path.join(self.git_checkout_path, relpath)
1291        self._two_local_commits()
1292        self.assertEqual(relpath, self.scm.to_object_name(fullpath))
1293
1294    def test_show_head(self):
1295        self._two_local_commits()
1296        self.assertEqual("more test content", self.scm.show_head('test_file_commit1'))
1297
1298    def test_show_head_binary(self):
1299        self._two_local_commits()
1300        data = "\244"
1301        write_into_file_at_path("binary_file", data, encoding=None)
1302        self.scm.add("binary_file")
1303        self.scm.commit_locally_with_message("a test commit")
1304        self.assertEqual(data, self.scm.show_head('binary_file'))
1305
1306    def test_diff_for_file(self):
1307        self._two_local_commits()
1308        write_into_file_at_path('test_file_commit1', "Updated", encoding=None)
1309
1310        diff = self.scm.diff_for_file('test_file_commit1')
1311        cached_diff = self.scm.diff_for_file('test_file_commit1')
1312        self.assertTrue("+Updated" in diff)
1313        self.assertTrue("-more test content" in diff)
1314
1315        self.scm.add('test_file_commit1')
1316
1317        cached_diff = self.scm.diff_for_file('test_file_commit1')
1318        self.assertTrue("+Updated" in cached_diff)
1319        self.assertTrue("-more test content" in cached_diff)
1320
1321
1322# We need to split off more of these SCM tests to use mocks instead of the filesystem.
1323# This class is the first part of that.
1324class GitTestWithMock(unittest.TestCase):
1325    def setUp(self):
1326        executive = MockExecutive(should_log=False)
1327        # We do this should_log dance to avoid logging when Git.__init__ runs sysctl on mac to check for 64-bit support.
1328        self.scm = Git(None, executive=executive)
1329        executive.should_log = True
1330
1331    def test_create_patch(self):
1332        expected_stderr = "MOCK run_command: ['git', 'merge-base', u'refs/remotes/origin/master', 'HEAD']\nMOCK run_command: ['git', 'diff', '--binary', '--no-ext-diff', '--full-index', '-M', 'MOCK output of child process', '--']\n"
1333        OutputCapture().assert_outputs(self, self.scm.create_patch, kwargs={'changed_files': None}, expected_stderr=expected_stderr)
1334
1335
1336if __name__ == '__main__':
1337    unittest.main()
1338