scm_unittest.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. #!/usr/bin/env python
  2. # Copyright (c) 2012 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Unit tests for scm.py."""
  6. import logging
  7. import os
  8. import sys
  9. import unittest
  10. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  11. from testing_support import fake_repos
  12. from testing_support.super_mox import SuperMoxTestBase
  13. import scm
  14. import subprocess2
  15. # Access to a protected member XXX of a client class
  16. # pylint: disable=W0212
  17. class BaseTestCase(SuperMoxTestBase):
  18. # Like unittest's assertRaises, but checks for Gclient.Error.
  19. def assertRaisesError(self, msg, fn, *args, **kwargs):
  20. try:
  21. fn(*args, **kwargs)
  22. except scm.gclient_utils.Error, e:
  23. self.assertEquals(e.args[0], msg)
  24. else:
  25. self.fail('%s not raised' % msg)
  26. class BaseSCMTestCase(BaseTestCase):
  27. def setUp(self):
  28. BaseTestCase.setUp(self)
  29. self.mox.StubOutWithMock(scm.gclient_utils, 'CheckCallAndFilter')
  30. self.mox.StubOutWithMock(scm.gclient_utils, 'CheckCallAndFilterAndHeader')
  31. self.mox.StubOutWithMock(subprocess2, 'Popen')
  32. self.mox.StubOutWithMock(subprocess2, 'communicate')
  33. class RootTestCase(BaseSCMTestCase):
  34. def testMembersChanged(self):
  35. self.mox.ReplayAll()
  36. members = [
  37. 'cStringIO',
  38. 'determine_scm',
  39. 'ElementTree',
  40. 'gclient_utils',
  41. 'GenFakeDiff',
  42. 'GetCasedPath',
  43. 'GIT',
  44. 'glob',
  45. 'logging',
  46. 'only_int',
  47. 'os',
  48. 're',
  49. 'subprocess2',
  50. 'SVN',
  51. 'sys',
  52. 'tempfile',
  53. 'time',
  54. 'ValidateEmail',
  55. ]
  56. # If this test fails, you should add the relevant test.
  57. self.compareMembers(scm, members)
  58. class GitWrapperTestCase(BaseSCMTestCase):
  59. def testMembersChanged(self):
  60. members = [
  61. 'AssertVersion',
  62. 'Capture',
  63. 'CaptureStatus',
  64. 'current_version',
  65. 'FetchUpstreamTuple',
  66. 'GenerateDiff',
  67. 'GetBranch',
  68. 'GetBranchRef',
  69. 'GetCheckoutRoot',
  70. 'GetDifferentFiles',
  71. 'GetEmail',
  72. 'GetGitSvnHeadRev',
  73. 'GetPatchName',
  74. 'GetSha1ForSvnRev',
  75. 'GetSVNBranch',
  76. 'GetUpstreamBranch',
  77. 'IsGitSvn',
  78. 'IsValidRevision',
  79. 'MatchSvnGlob',
  80. 'ShortBranchName',
  81. ]
  82. # If this test fails, you should add the relevant test.
  83. self.compareMembers(scm.GIT, members)
  84. def testGetEmail(self):
  85. self.mox.StubOutWithMock(scm.GIT, 'Capture')
  86. scm.GIT.Capture(['config', 'user.email'], cwd=self.root_dir
  87. ).AndReturn('mini@me.com')
  88. self.mox.ReplayAll()
  89. self.assertEqual(scm.GIT.GetEmail(self.root_dir), 'mini@me.com')
  90. def testMatchSvnGlob(self):
  91. self.assertEquals(scm.GIT.MatchSvnGlob(
  92. 'svn://svn.chromium.org/chrome/trunk/src',
  93. 'svn://svn.chromium.org/chrome',
  94. 'trunk/src:refs/remotes/origin/trunk',
  95. False), 'refs/remotes/origin/trunk')
  96. self.assertEquals(scm.GIT.MatchSvnGlob(
  97. 'https://v8.googlecode.com/svn/branches/bleeding_edge',
  98. 'https://v8.googlecode.com/svn',
  99. 'branches/*:refs/remotes/*',
  100. True), 'refs/remotes/bleeding_edge')
  101. class RealGitTest(fake_repos.FakeReposTestBase):
  102. def setUp(self):
  103. super(RealGitTest, self).setUp()
  104. self.enabled = self.FAKE_REPOS.set_up_git()
  105. if self.enabled:
  106. self.clone_dir = scm.os.path.join(self.FAKE_REPOS.git_root, 'repo_1')
  107. def testIsValidRevision(self):
  108. if not self.enabled:
  109. return
  110. # Sha1's are [0-9a-z]{32}, so starting with a 'z' or 'r' should always fail.
  111. self.assertFalse(scm.GIT.IsValidRevision(cwd=self.clone_dir, rev='zebra'))
  112. self.assertFalse(scm.GIT.IsValidRevision(cwd=self.clone_dir, rev='r123456'))
  113. # Valid cases
  114. first_rev = self.githash('repo_1', 1)
  115. self.assertTrue(scm.GIT.IsValidRevision(cwd=self.clone_dir, rev=first_rev))
  116. self.assertTrue(scm.GIT.IsValidRevision(cwd=self.clone_dir, rev='HEAD'))
  117. class RealGitSvnTest(fake_repos.FakeReposTestBase):
  118. def setUp(self):
  119. super(RealGitSvnTest, self).setUp()
  120. self.enabled = self.FAKE_REPOS.set_up_git() and self.FAKE_REPOS.set_up_svn()
  121. if self.enabled:
  122. self.tree_name = 'git-svn'
  123. self.svn_url = scm.os.path.join(self.FAKE_REPOS.svn_base, 'trunk')
  124. self.clone_dir = scm.os.path.join(self.FAKE_REPOS.git_root,
  125. self.tree_name)
  126. scm.os.makedirs(self.clone_dir)
  127. self._capture(['svn', 'clone', '-q', '-q', self.svn_url, self.clone_dir])
  128. # git rev-list gives revisions in reverse chronological order.
  129. hashes = reversed(self._capture(['rev-list', 'HEAD']).splitlines())
  130. # We insert a null value at 0 to do 1-based indexing, not 0-based, as SVN
  131. # revisions are 1-based (i.e. they start at r1, not r0).
  132. self.git_hashes = ([None] + list(hashes))
  133. def tearDown(self):
  134. scm.gclient_utils.rmtree(self.clone_dir)
  135. def _capture(self, cmd, **kwargs):
  136. kwargs.setdefault('cwd', self.clone_dir)
  137. return scm.GIT.Capture(cmd, **kwargs)
  138. def testGetGitSvnHeadRev(self):
  139. if not self.enabled:
  140. return
  141. self.assertEquals(scm.GIT.GetGitSvnHeadRev(cwd=self.clone_dir), 2)
  142. self._capture(['reset', '--hard', 'HEAD^'])
  143. self.assertEquals(scm.GIT.GetGitSvnHeadRev(cwd=self.clone_dir), 1)
  144. def testGetGetSha1ForSvnRev(self):
  145. if not self.enabled:
  146. return
  147. self.assertEquals(scm.GIT.GetSha1ForSvnRev(cwd=self.clone_dir, rev=1),
  148. self.git_hashes[1])
  149. self.assertEquals(scm.GIT.GetSha1ForSvnRev(cwd=self.clone_dir, rev=2),
  150. self.git_hashes[2])
  151. class SVNTestCase(BaseSCMTestCase):
  152. def setUp(self):
  153. BaseSCMTestCase.setUp(self)
  154. self.mox.StubOutWithMock(scm.SVN, 'Capture')
  155. self.url = self.SvnUrl()
  156. def testMembersChanged(self):
  157. self.mox.ReplayAll()
  158. members = [
  159. 'AssertVersion',
  160. 'Capture',
  161. 'CaptureLocalInfo',
  162. 'CaptureRemoteInfo',
  163. 'CaptureRevision',
  164. 'CaptureStatus',
  165. 'current_version',
  166. 'DiffItem',
  167. 'GenerateDiff',
  168. 'GetCheckoutRoot',
  169. 'GetEmail',
  170. 'GetFileProperty',
  171. 'IsMoved',
  172. 'IsMovedInfo',
  173. 'IsValidRevision',
  174. 'ReadSimpleAuth',
  175. 'Revert',
  176. 'RunAndGetFileList',
  177. ]
  178. # If this test fails, you should add the relevant test.
  179. self.compareMembers(scm.SVN, members)
  180. def testGetCheckoutRoot(self):
  181. # pylint: disable=E1103
  182. self.mox.StubOutWithMock(scm.SVN, '_CaptureInfo')
  183. self.mox.StubOutWithMock(scm, 'GetCasedPath')
  184. scm.os.path.abspath = lambda x: x
  185. scm.GetCasedPath = lambda x: x
  186. scm.SVN._CaptureInfo([], self.root_dir + '/foo/bar').AndReturn({
  187. 'Repository Root': 'svn://svn.chromium.org/chrome',
  188. 'URL': 'svn://svn.chromium.org/chrome/trunk/src',
  189. })
  190. scm.SVN._CaptureInfo([], self.root_dir + '/foo').AndReturn({
  191. 'Repository Root': 'svn://svn.chromium.org/chrome',
  192. 'URL': 'svn://svn.chromium.org/chrome/trunk',
  193. })
  194. scm.SVN._CaptureInfo([], self.root_dir).AndReturn({
  195. 'Repository Root': 'svn://svn.chromium.org/chrome',
  196. 'URL': 'svn://svn.chromium.org/chrome/trunk/tools/commit-queue/workdir',
  197. })
  198. self.mox.ReplayAll()
  199. self.assertEquals(scm.SVN.GetCheckoutRoot(self.root_dir + '/foo/bar'),
  200. self.root_dir + '/foo')
  201. def testGetFileInfo(self):
  202. xml_text = r"""<?xml version="1.0"?>
  203. <info>
  204. <entry kind="file" path="%s" revision="14628">
  205. <url>http://src.chromium.org/svn/trunk/src/chrome/app/d</url>
  206. <repository><root>http://src.chromium.org/svn</root></repository>
  207. <wc-info>
  208. <schedule>add</schedule>
  209. <depth>infinity</depth>
  210. <copy-from-url>http://src.chromium.org/svn/trunk/src/chrome/app/DEPS</copy-from-url>
  211. <copy-from-rev>14628</copy-from-rev>
  212. <checksum>369f59057ba0e6d9017e28f8bdfb1f43</checksum>
  213. </wc-info>
  214. </entry>
  215. </info>
  216. """ % self.url
  217. scm.SVN.Capture(['info', '--xml', self.url], None).AndReturn(xml_text)
  218. expected = {
  219. 'URL': 'http://src.chromium.org/svn/trunk/src/chrome/app/d',
  220. 'UUID': None,
  221. 'Repository Root': 'http://src.chromium.org/svn',
  222. 'Schedule': 'add',
  223. 'Copied From URL':
  224. 'http://src.chromium.org/svn/trunk/src/chrome/app/DEPS',
  225. 'Copied From Rev': '14628',
  226. 'Path': self.url,
  227. 'Revision': 14628,
  228. 'Node Kind': 'file',
  229. }
  230. self.mox.ReplayAll()
  231. file_info = scm.SVN._CaptureInfo([self.url], None)
  232. self.assertEquals(sorted(file_info.items()), sorted(expected.items()))
  233. def testCaptureInfo(self):
  234. xml_text = """<?xml version="1.0"?>
  235. <info>
  236. <entry
  237. kind="dir"
  238. path="."
  239. revision="35">
  240. <url>%s</url>
  241. <repository>
  242. <root>%s</root>
  243. <uuid>7b9385f5-0452-0410-af26-ad4892b7a1fb</uuid>
  244. </repository>
  245. <wc-info>
  246. <schedule>normal</schedule>
  247. <depth>infinity</depth>
  248. </wc-info>
  249. <commit
  250. revision="35">
  251. <author>maruel</author>
  252. <date>2008-12-04T20:12:19.685120Z</date>
  253. </commit>
  254. </entry>
  255. </info>
  256. """ % (self.url, self.root_dir)
  257. scm.SVN.Capture(['info', '--xml', self.url], None).AndReturn(xml_text)
  258. self.mox.ReplayAll()
  259. file_info = scm.SVN._CaptureInfo([self.url], None)
  260. expected = {
  261. 'URL': self.url,
  262. 'UUID': '7b9385f5-0452-0410-af26-ad4892b7a1fb',
  263. 'Revision': 35,
  264. 'Repository Root': self.root_dir,
  265. 'Schedule': 'normal',
  266. 'Copied From URL': None,
  267. 'Copied From Rev': None,
  268. 'Path': '.',
  269. 'Node Kind': 'directory',
  270. }
  271. self.assertEqual(file_info, expected)
  272. def testCaptureStatus(self):
  273. text = r"""<?xml version="1.0"?>
  274. <status>
  275. <target path=".">
  276. <entry path="unversionned_file.txt">
  277. <wc-status props="none" item="unversioned"></wc-status>
  278. </entry>
  279. <entry path="build\internal\essential.vsprops">
  280. <wc-status props="normal" item="modified" revision="14628">
  281. <commit revision="13818">
  282. <author>ajwong@chromium.org</author>
  283. <date>2009-04-16T00:42:06.872358Z</date>
  284. </commit>
  285. </wc-status>
  286. </entry>
  287. <entry path="chrome\app\d">
  288. <wc-status props="none" copied="true" tree-conflicted="true" item="added">
  289. </wc-status>
  290. </entry>
  291. <entry path="chrome\app\DEPS">
  292. <wc-status props="modified" item="modified" revision="14628">
  293. <commit revision="1279">
  294. <author>brettw@google.com</author>
  295. <date>2008-08-23T17:16:42.090152Z</date>
  296. </commit>
  297. </wc-status>
  298. </entry>
  299. <entry path="scripts\master\factory\gclient_factory.py">
  300. <wc-status props="normal" item="conflicted" revision="14725">
  301. <commit revision="14633">
  302. <author>nsylvain@chromium.org</author>
  303. <date>2009-04-27T19:37:17.977400Z</date>
  304. </commit>
  305. </wc-status>
  306. </entry>
  307. </target>
  308. </status>
  309. """
  310. scm.SVN.Capture(['status', '--xml'], '.').AndReturn(text)
  311. self.mox.ReplayAll()
  312. info = scm.SVN.CaptureStatus(None, '.')
  313. expected = [
  314. ('? ', 'unversionned_file.txt'),
  315. ('M ', 'build\\internal\\essential.vsprops'),
  316. ('A + ', 'chrome\\app\\d'),
  317. ('MM ', 'chrome\\app\\DEPS'),
  318. ('C ', 'scripts\\master\\factory\\gclient_factory.py'),
  319. ]
  320. self.assertEquals(sorted(info), sorted(expected))
  321. def testCaptureStatusEmpty(self):
  322. text = r"""<?xml version="1.0"?>
  323. <status>
  324. <target
  325. path="perf">
  326. </target>
  327. </status>"""
  328. scm.SVN.Capture(['status', '--xml'], None).AndReturn(text)
  329. self.mox.ReplayAll()
  330. info = scm.SVN.CaptureStatus(None, None)
  331. self.assertEquals(info, [])
  332. class RealSvnTest(fake_repos.FakeReposTestBase):
  333. # Tests that work with a checkout.
  334. def setUp(self):
  335. super(RealSvnTest, self).setUp()
  336. self.enabled = self.FAKE_REPOS.set_up_svn()
  337. if self.enabled:
  338. self.svn_root = scm.os.path.join(self.root_dir, 'base')
  339. scm.SVN.Capture(
  340. ['checkout', self.svn_base + 'trunk/third_party', 'base'],
  341. cwd=self.root_dir)
  342. self.tree = self.mangle_svn_tree(('trunk/third_party@-1', ''),)
  343. def _capture(self, cmd, **kwargs):
  344. kwargs.setdefault('cwd', self.svn_root)
  345. return scm.SVN.Capture(cmd, **kwargs)
  346. def testCheckout(self):
  347. if not self.enabled:
  348. return
  349. # Checkout and verify the tree.
  350. self.assertTree(self.tree, self.svn_root)
  351. def testIsValidRevision(self):
  352. if not self.enabled:
  353. return
  354. url_at_rev = self.svn_base + 'trunk/third_party@%s'
  355. # Invalid or non-existent.
  356. self.assertFalse(scm.SVN.IsValidRevision('url://totally_invalid/trunk/foo'))
  357. self.assertFalse(scm.SVN.IsValidRevision(url_at_rev % 0))
  358. self.assertFalse(scm.SVN.IsValidRevision(url_at_rev % 123))
  359. # Valid.
  360. self.assertTrue(scm.SVN.IsValidRevision(url_at_rev % 1))
  361. self.assertTrue(scm.SVN.IsValidRevision(url_at_rev % 2))
  362. self.assertTrue(scm.SVN.IsValidRevision(url_at_rev % 'HEAD'))
  363. def testRevert(self):
  364. if not self.enabled:
  365. return
  366. # Mess around and make sure revert works for all corner cases.
  367. # - svn add a file
  368. # - svn add a file and delete it
  369. # - Delete a file
  370. # - svn delete a file
  371. # - svn move a directory and svn rename files in it
  372. # - add a directory tree.
  373. def join(*args):
  374. return scm.os.path.join(self.svn_root, *args)
  375. self._capture(['move', 'foo', 'foo2'])
  376. self._capture(
  377. ['move',
  378. scm.os.path.join('foo2', 'origin'),
  379. scm.os.path.join('foo2', 'o')])
  380. scm.os.remove(join('origin'))
  381. self._capture(['propset', 'foo', 'bar', join('prout', 'origin')])
  382. fake_repos.gclient_utils.rmtree(join('prout'))
  383. with open(join('faa'), 'w') as f:
  384. f.write('eh')
  385. with open(join('faala'), 'w') as f:
  386. f.write('oh')
  387. self._capture(['add', join('faala')])
  388. added_and_removed = join('added_and_removed')
  389. with open(added_and_removed, 'w') as f:
  390. f.write('oh')
  391. self._capture(['add', added_and_removed])
  392. scm.os.remove(added_and_removed)
  393. # Make sure a tree of directories can be removed.
  394. scm.os.makedirs(join('new_dir', 'subdir'))
  395. with open(join('new_dir', 'subdir', 'newfile'), 'w') as f:
  396. f.write('ah!')
  397. self._capture(['add', join('new_dir')])
  398. self._capture(['add', join('new_dir', 'subdir')])
  399. self._capture(['add', join('new_dir', 'subdir', 'newfile')])
  400. # A random file in an added directory confuses svn.
  401. scm.os.makedirs(join('new_dir2', 'subdir'))
  402. with open(join('new_dir2', 'subdir', 'newfile'), 'w') as f:
  403. f.write('ah!')
  404. self._capture(['add', join('new_dir2')])
  405. self._capture(['add', join('new_dir2', 'subdir')])
  406. self._capture(['add', join('new_dir2', 'subdir', 'newfile')])
  407. with open(join('new_dir2', 'subdir', 'unversionedfile'), 'w') as f:
  408. f.write('unadded file!')
  409. scm.SVN.Revert(self.svn_root)
  410. self._capture(['update', '--revision', 'base'])
  411. self.assertTree(self.tree, self.svn_root)
  412. # Asserting the tree is not sufficient, svn status must come out clear too.
  413. self.assertEquals('', self._capture(['status']))
  414. if __name__ == '__main__':
  415. if '-v' in sys.argv:
  416. logging.basicConfig(level=logging.DEBUG)
  417. unittest.main()
  418. # vim: ts=2:sw=2:tw=80:et: