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