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