split_cl_test.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. #!/usr/bin/env vpython3
  2. """Tests for split_cl."""
  3. import os
  4. import sys
  5. import unittest
  6. from unittest import mock
  7. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  8. import split_cl
  9. import gclient_utils
  10. class SplitClTest(unittest.TestCase):
  11. @property
  12. def _input_dir(self):
  13. base = os.path.splitext(os.path.abspath(__file__))[0]
  14. # Here _testMethodName is a string like "testCmdAssemblyFound"
  15. # If the test doesn't have its own subdirectory, it uses a common one
  16. path = os.path.join(base + ".inputs", self._testMethodName)
  17. if not os.path.isdir(path):
  18. path = os.path.join(base + ".inputs", "commonFiles")
  19. return path
  20. def testAddUploadedByGitClSplitToDescription(self):
  21. description = """Convert use of X to Y in $description
  22. <add some background about this conversion for the reviewers>
  23. """
  24. footers = 'Bug: 12345'
  25. added_line = 'This CL was uploaded by git cl split.'
  26. experimental_lines = ("This CL was uploaded by an experimental version "
  27. "of git cl split\n"
  28. "(https://crbug.com/389069356).")
  29. # Description without footers
  30. self.assertEqual(
  31. split_cl.AddUploadedByGitClSplitToDescription(description),
  32. description + added_line)
  33. # Description with footers
  34. self.assertEqual(
  35. split_cl.AddUploadedByGitClSplitToDescription(description +
  36. footers),
  37. description + added_line + '\n\n' + footers)
  38. # Description with footers and experimental flag
  39. self.assertEqual(
  40. split_cl.AddUploadedByGitClSplitToDescription(
  41. description + footers, True),
  42. description + experimental_lines + '\n\n' + footers)
  43. @mock.patch("split_cl.EmitWarning")
  44. def testFormatDescriptionOrComment(self, mock_emit_warning):
  45. description = "Converted use of X to Y in $description."
  46. # One directory
  47. self.assertEqual(
  48. split_cl.FormatDescriptionOrComment(
  49. description, split_cl.FormatDirectoriesForPrinting(["foo"])),
  50. "Converted use of X to Y in foo.",
  51. )
  52. # Many directories
  53. self.assertEqual(
  54. split_cl.FormatDescriptionOrComment(
  55. description,
  56. split_cl.FormatDirectoriesForPrinting(["foo", "bar"])),
  57. "Converted use of X to Y in ['foo', 'bar'].",
  58. )
  59. mock_emit_warning.assert_not_called()
  60. description_deprecated = "Converted use of X to Y in $directory."
  61. # Make sure we emit a deprecation warning if the old format is used
  62. self.assertEqual(
  63. split_cl.FormatDescriptionOrComment(
  64. description_deprecated,
  65. split_cl.FormatDirectoriesForPrinting([])),
  66. "Converted use of X to Y in [].",
  67. )
  68. mock_emit_warning.assert_called_once()
  69. def GetDirectoryBaseName(self, file_path):
  70. return os.path.basename(os.path.dirname(file_path))
  71. def MockSuggestOwners(self, paths, exclude=None):
  72. if not paths:
  73. return ["superowner"]
  74. return self.GetDirectoryBaseName(paths[0]).split(",")
  75. def MockIsFile(self, file_path):
  76. if os.path.basename(file_path) == "OWNERS":
  77. return "owner" in self.GetDirectoryBaseName(file_path)
  78. return True
  79. @mock.patch("os.path.isfile")
  80. def testSelectReviewersForFiles(self, mock_is_file):
  81. mock_is_file.side_effect = self.MockIsFile
  82. owners_client = mock.Mock(SuggestOwners=self.MockSuggestOwners,
  83. EVERYONE="*")
  84. cl = mock.Mock(owners_client=owners_client)
  85. files = [("M", os.path.join("foo", "owner1,owner2", "a.txt")),
  86. ("M", os.path.join("foo", "owner1,owner2", "b.txt")),
  87. ("M", os.path.join("bar", "owner1,owner2", "c.txt")),
  88. ("M", os.path.join("bax", "owner2", "d.txt")),
  89. ("M", os.path.join("baz", "owner3", "e.txt"))]
  90. files_split_by_reviewers = split_cl.SelectReviewersForFiles(
  91. cl, "author", files, 0, "")
  92. self.assertEqual(3, len(files_split_by_reviewers.keys()))
  93. info1 = files_split_by_reviewers[tuple(["owner1", "owner2"])]
  94. self.assertEqual(info1.files,
  95. [("M", os.path.join("foo", "owner1,owner2", "a.txt")),
  96. ("M", os.path.join("foo", "owner1,owner2", "b.txt")),
  97. ("M", os.path.join("bar", "owner1,owner2", "c.txt"))])
  98. self.assertEqual(info1.owners_directories,
  99. ["foo/owner1,owner2", "bar/owner1,owner2"])
  100. info2 = files_split_by_reviewers[tuple(["owner2"])]
  101. self.assertEqual(info2.files,
  102. [("M", os.path.join("bax", "owner2", "d.txt"))])
  103. self.assertEqual(info2.owners_directories, ["bax/owner2"])
  104. info3 = files_split_by_reviewers[tuple(["owner3"])]
  105. self.assertEqual(info3.files,
  106. [("M", os.path.join("baz", "owner3", "e.txt"))])
  107. self.assertEqual(info3.owners_directories, ["baz/owner3"])
  108. class UploadClTester:
  109. """Sets up test environment for testing split_cl.UploadCl()"""
  110. def __init__(self, test):
  111. self.mock_git_branches = self.StartPatcher("git_common.branches",
  112. test)
  113. self.mock_git_branches.return_value = []
  114. self.mock_git_current_branch = self.StartPatcher(
  115. "git_common.current_branch", test)
  116. self.mock_git_current_branch.return_value = "branch_to_upload"
  117. self.mock_git_run = self.StartPatcher("git_common.run", test)
  118. self.mock_temporary_file = self.StartPatcher(
  119. "gclient_utils.temporary_file", test)
  120. self.mock_temporary_file(
  121. ).__enter__.return_value = "temporary_file0"
  122. self.mock_file_writer = self.StartPatcher("gclient_utils.FileWrite",
  123. test)
  124. def StartPatcher(self, target, test):
  125. patcher = mock.patch(target)
  126. test.addCleanup(patcher.stop)
  127. return patcher.start()
  128. def DoUploadCl(self, description, files, reviewers, cmd_upload):
  129. split_cl.UploadCl("branch_to_upload", "upstream_branch",
  130. description, files, "description",
  131. "splitting_file.txt", None, reviewers,
  132. mock.Mock(), cmd_upload, True, True, "topic",
  133. os.path.sep)
  134. def testUploadCl(self):
  135. """Tests commands run by UploadCl."""
  136. upload_cl_tester = self.UploadClTester(self)
  137. description = split_cl.FormatDirectoriesForPrinting(["dir0"])
  138. files = [("M", os.path.join("bar", "a.cc")),
  139. ("D", os.path.join("foo", "b.cc"))]
  140. reviewers = {"reviewer1@gmail.com", "reviewer2@gmail.com"}
  141. mock_cmd_upload = mock.Mock()
  142. upload_cl_tester.DoUploadCl(description, files, reviewers,
  143. mock_cmd_upload)
  144. abs_repository_path = os.path.abspath(os.path.sep)
  145. mock_git_run = upload_cl_tester.mock_git_run
  146. self.assertEqual(mock_git_run.call_count, 4)
  147. mock_git_run.assert_has_calls([
  148. mock.call("checkout", "-t", "upstream_branch", "-b",
  149. split_cl.CreateBranchName("branch_to_upload", files)),
  150. mock.call("rm", os.path.join(abs_repository_path, "foo", "b.cc")),
  151. mock.call("checkout", "branch_to_upload", "--",
  152. os.path.join(abs_repository_path, "bar", "a.cc")),
  153. mock.call("commit", "-F", "temporary_file0")
  154. ])
  155. expected_upload_args = [
  156. "-f", "-r", "reviewer1@gmail.com,reviewer2@gmail.com",
  157. "--cq-dry-run", "--send-mail", "--enable-auto-submit",
  158. "--topic=topic"
  159. ]
  160. mock_cmd_upload.assert_called_once_with(expected_upload_args)
  161. def testDontUploadClIfBranchAlreadyExists(self):
  162. """Tests that a CL is not uploaded if split branch already exists"""
  163. upload_cl_tester = self.UploadClTester(self)
  164. description = split_cl.FormatDirectoriesForPrinting(["dir0"])
  165. files = [("M", os.path.join("bar", "a.cc")),
  166. ("D", os.path.join("foo", "b.cc"))]
  167. reviewers = {"reviewer1@gmail.com"}
  168. mock_cmd_upload = mock.Mock()
  169. upload_cl_tester.mock_git_branches.return_value = [
  170. "branch0",
  171. split_cl.CreateBranchName("branch_to_upload", files)
  172. ]
  173. upload_cl_tester.DoUploadCl(description, files, reviewers,
  174. mock_cmd_upload)
  175. upload_cl_tester.mock_git_run.assert_not_called()
  176. mock_cmd_upload.assert_not_called()
  177. @mock.patch("gclient_utils.AskForData")
  178. def testCheckDescriptionBugLink(self, mock_ask_for_data):
  179. # Description contains bug link.
  180. self.assertTrue(split_cl.CheckDescriptionBugLink("Bug:1234"))
  181. self.assertEqual(mock_ask_for_data.call_count, 0)
  182. # Description does not contain bug link. User does not enter 'y' when
  183. # prompted.
  184. mock_ask_for_data.reset_mock()
  185. mock_ask_for_data.return_value = "m"
  186. self.assertFalse(split_cl.CheckDescriptionBugLink("Description"))
  187. self.assertEqual(mock_ask_for_data.call_count, 1)
  188. # Description does not contain bug link. User enters 'y' when prompted.
  189. mock_ask_for_data.reset_mock()
  190. mock_ask_for_data.return_value = "y"
  191. self.assertTrue(split_cl.CheckDescriptionBugLink("Description"))
  192. self.assertEqual(mock_ask_for_data.call_count, 1)
  193. @mock.patch("gclient_utils.FileRead", return_value="Description")
  194. def testLoadDescription(self, mock_file_read):
  195. # No description provided, use the dummy:
  196. self.assertTrue(
  197. split_cl.LoadDescription(None, True).startswith("Dummy"))
  198. self.assertEqual(mock_file_read.call_count, 0)
  199. # No description provided during a real run
  200. self.assertRaises(ValueError, split_cl.LoadDescription, None, False)
  201. self.assertEqual(mock_file_read.call_count, 0)
  202. # Description file provided, load it regardless of dry run
  203. self.assertEqual(split_cl.LoadDescription("SomeFile.txt", False),
  204. "Description")
  205. self.assertEqual(mock_file_read.call_count, 1)
  206. mock_file_read.reset_mock()
  207. self.assertEqual(split_cl.LoadDescription("SomeFile.txt", True),
  208. "Description")
  209. self.assertEqual(mock_file_read.call_count, 1)
  210. class SplitClTester:
  211. """Sets up test environment for testing split_cl.SplitCl()"""
  212. def __init__(self, test):
  213. self.mocks = []
  214. self.mock_file_read = self.StartPatcher(
  215. "gclient_utils.FileRead",
  216. test,
  217. return_value="Non-dummy description\nBug: 1243")
  218. self.mock_in_git_repo = self.StartPatcher(
  219. "split_cl.EnsureInGitRepository", test)
  220. self.mock_git_status = self.StartPatcher("scm.GIT.CaptureStatus",
  221. test)
  222. self.mock_git_run = self.StartPatcher("git_common.run", test)
  223. self.mock_git_current_branch = self.StartPatcher(
  224. "git_common.current_branch",
  225. test,
  226. return_value="branch_to_upload")
  227. self.mock_git_branches = self.StartPatcher("git_common.branches",
  228. test)
  229. self.mock_git_upstream = self.StartPatcher(
  230. "git_common.upstream", test, return_value="upstream_branch")
  231. self.mock_get_reviewers = self.StartPatcher(
  232. "split_cl.SelectReviewersForFiles", test)
  233. self.mock_ask_for_data = self.StartPatcher(
  234. "gclient_utils.AskForData", test)
  235. self.mock_print_cl_info = self.StartPatcher("split_cl.PrintClInfo",
  236. test)
  237. self.mock_print_summary = self.StartPatcher("split_cl.PrintSummary",
  238. test)
  239. self.mock_upload_cl = self.StartPatcher("split_cl.UploadCl", test)
  240. self.mock_save_splitting = self.StartPatcher(
  241. "split_cl.SaveSplittingToTempFile", test)
  242. # Suppress output for cleaner tests
  243. self.mock_emit = self.StartPatcher("split_cl.Emit", test)
  244. def StartPatcher(self, target, test, **kwargs):
  245. patcher = mock.patch(target, **kwargs)
  246. test.addCleanup(patcher.stop)
  247. m = patcher.start()
  248. self.mocks.append(m)
  249. return m
  250. def ResetMocks(self):
  251. for m in self.mocks:
  252. m.reset_mock()
  253. def DoSplitCl(self, description_file, dry_run, summarize,
  254. reviewers_override, files_split_by_reviewers,
  255. proceed_response):
  256. all_files = [v.files for v in files_split_by_reviewers.values()]
  257. all_files_flattened = [
  258. file for files in all_files for file in files
  259. ]
  260. self.mock_git_status.return_value = all_files_flattened
  261. self.mock_get_reviewers.return_value = files_split_by_reviewers
  262. self.mock_ask_for_data.return_value = proceed_response
  263. split_cl.SplitCl(description_file, None, mock.Mock(), mock.Mock(),
  264. dry_run, summarize, reviewers_override, False,
  265. False, None, None, None, None, None, None)
  266. # Save for re-use
  267. files_split_by_reviewers = {
  268. ("a@example.com", ):
  269. split_cl.FilesAndOwnersDirectory([
  270. ("M", "a/b/foo.cc"),
  271. ("M", "d/e/bar.h"),
  272. ], []),
  273. ("b@example.com", ):
  274. split_cl.FilesAndOwnersDirectory([
  275. ("A", "f/g/baz.py"),
  276. ], [])
  277. }
  278. def testSplitClConfirm(self):
  279. split_cl_tester = self.SplitClTester(self)
  280. # Should prompt for confirmation and upload several times
  281. split_cl_tester.DoSplitCl("SomeFile.txt", False, False, None,
  282. self.files_split_by_reviewers, "y")
  283. split_cl_tester.mock_ask_for_data.assert_called_once()
  284. split_cl_tester.mock_print_cl_info.assert_not_called()
  285. self.assertEqual(split_cl_tester.mock_upload_cl.call_count,
  286. len(self.files_split_by_reviewers))
  287. split_cl_tester.ResetMocks()
  288. # Should prompt for confirmation and not upload
  289. split_cl_tester.DoSplitCl("SomeFile.txt", False, False, None,
  290. self.files_split_by_reviewers, "f")
  291. split_cl_tester.mock_ask_for_data.assert_called_once()
  292. split_cl_tester.mock_print_cl_info.assert_not_called()
  293. split_cl_tester.mock_upload_cl.assert_not_called()
  294. split_cl_tester.ResetMocks()
  295. # Dry runs: Don't prompt, print info instead of uploading
  296. split_cl_tester.DoSplitCl("SomeFile.txt", True, False, None,
  297. self.files_split_by_reviewers, "f")
  298. split_cl_tester.mock_ask_for_data.assert_not_called()
  299. self.assertEqual(split_cl_tester.mock_print_cl_info.call_count,
  300. len(self.files_split_by_reviewers))
  301. split_cl_tester.mock_print_summary.assert_not_called()
  302. split_cl_tester.mock_upload_cl.assert_not_called()
  303. split_cl_tester.ResetMocks()
  304. # Summarize is true: Don't prompt, emit a summary
  305. split_cl_tester.DoSplitCl("SomeFile.txt", True, True, None,
  306. self.files_split_by_reviewers, "f")
  307. split_cl_tester.mock_ask_for_data.assert_not_called()
  308. split_cl_tester.mock_print_cl_info.assert_not_called()
  309. split_cl_tester.mock_print_summary.assert_called_once()
  310. split_cl_tester.mock_upload_cl.assert_not_called()
  311. def testReviewerOverride(self):
  312. split_cl_tester = self.SplitClTester(self)
  313. def testOneOverride(reviewers_lst):
  314. split_cl_tester.DoSplitCl("SomeFile.txt", False, False,
  315. reviewers_lst,
  316. self.files_split_by_reviewers, "y")
  317. for call in split_cl_tester.mock_upload_cl.call_args_list:
  318. self.assertEqual(call.args[7], set(reviewers_lst))
  319. split_cl_tester.ResetMocks()
  320. # The 'None' case gets ample testing everywhere else
  321. testOneOverride([])
  322. testOneOverride(['a@b.com', 'c@d.com'])
  323. def testValidateExistingBranches(self):
  324. """
  325. Make sure that we skip existing branches if they match what we intend
  326. to do, and fail if there are existing branches that don't match.
  327. """
  328. split_cl_tester = self.SplitClTester(self)
  329. # If no split branches exist, we should call upload once per CL
  330. split_cl_tester.mock_git_branches.return_value = [
  331. "branch0", "branch_to_upload"
  332. ]
  333. split_cl_tester.DoSplitCl("SomeFile.txt", False, False, None,
  334. self.files_split_by_reviewers, "y")
  335. self.assertEqual(split_cl_tester.mock_upload_cl.call_count,
  336. len(self.files_split_by_reviewers))
  337. # TODO(389069356): We should also ensure that if there are existing
  338. # branches that match our current splitting, we skip them when uploading
  339. # Unfortunately, we're not set up to test that, so this will have to
  340. # wait until we've refactored SplitCl and UploadCL to be less
  341. # monolithic
  342. # If a split branch with a bad name already exists, we should fail
  343. split_cl_tester.mock_upload_cl.reset_mock()
  344. split_cl_tester.mock_git_branches.return_value = [
  345. "branch0", "branch_to_upload",
  346. "branch_to_upload_123456789_whatever_split"
  347. ]
  348. split_cl_tester.DoSplitCl("SomeFile.txt", False, False, None,
  349. self.files_split_by_reviewers, "y")
  350. split_cl_tester.mock_upload_cl.assert_not_called()
  351. # Tests related to saving to and loading from files
  352. # Sample CLInfos for testing
  353. CLInfo_1 = split_cl.CLInfo(reviewers=["a@example.com"],
  354. description="['chrome/browser']",
  355. files=[
  356. ("M", "chrome/browser/a.cc"),
  357. ("M", "chrome/browser/b.cc"),
  358. ])
  359. CLInfo_2 = split_cl.CLInfo(reviewers=["a@example.com", "b@example.com"],
  360. description="['foo', 'bar/baz']",
  361. files=[("M", "foo/browser/a.cc"),
  362. ("M", "bar/baz/b.cc"),
  363. ("D", "foo/bar/c.h")])
  364. def testCLInfoFormat(self):
  365. """ Make sure CLInfo printing works as expected """
  366. def ReadAndStripPreamble(file):
  367. """ Read the contents of a file and strip the automatically-added
  368. preamble so we can do string comparison
  369. """
  370. content = gclient_utils.FileRead(os.path.join(
  371. self._input_dir, file))
  372. # Strip preamble
  373. stripped = [
  374. line for line in content.splitlines()
  375. if not line.startswith("#")
  376. ]
  377. # Strip newlines in preamble
  378. return "\n".join(stripped[2:])
  379. # Direct string comparison
  380. self.assertEqual(self.CLInfo_1.FormatForPrinting(),
  381. ReadAndStripPreamble("1_cl.txt"))
  382. self.assertEqual(
  383. self.CLInfo_1.FormatForPrinting() +
  384. "\n\n" + self.CLInfo_2.FormatForPrinting(),
  385. ReadAndStripPreamble("2_cls.txt"))
  386. @mock.patch("split_cl.EmitWarning")
  387. def testParseCLInfo(self, mock_emit_warning):
  388. """ Make sure we can parse valid files """
  389. self.assertEqual([self.CLInfo_1],
  390. split_cl.LoadSplittingFromFile(
  391. os.path.join(self._input_dir, "1_cl.txt"),
  392. self.CLInfo_1.files))
  393. self.assertEqual([self.CLInfo_1, self.CLInfo_2],
  394. split_cl.LoadSplittingFromFile(
  395. os.path.join(self._input_dir, "2_cls.txt"),
  396. self.CLInfo_1.files + self.CLInfo_2.files))
  397. # Make sure everything in this file is valid to parse
  398. split_cl.LoadSplittingFromFile(
  399. os.path.join(self._input_dir, "odd_formatting.txt"),
  400. self.CLInfo_1.files + self.CLInfo_2.files + [("A", "a/b/c"),
  401. ("A", "a/b/d"),
  402. ("D", "a/e")])
  403. mock_emit_warning.assert_not_called()
  404. def testParseBadFiles(self):
  405. """ Make sure we don't parse invalid files """
  406. for file in os.listdir(self._input_dir):
  407. lines = gclient_utils.FileRead(os.path.join(self._input_dir,
  408. file)).splitlines()
  409. self.assertRaises(split_cl.ClSplitParseError,
  410. split_cl.ParseSplittings, lines)
  411. @mock.patch("split_cl.EmitWarning")
  412. @mock.patch("split_cl.Emit")
  413. def testValidateBadFiles(self, _, mock_emit_warning):
  414. """ Make sure we reject invalid CL lists """
  415. # Warn on an empty file
  416. split_cl.LoadSplittingFromFile(
  417. os.path.join(self._input_dir, "warn_0_cls.txt"), [])
  418. mock_emit_warning.assert_called_once()
  419. mock_emit_warning.reset_mock()
  420. # Warn if reviewers don't look like emails
  421. split_cl.LoadSplittingFromFile(
  422. os.path.join(self._input_dir, "warn_bad_reviewer_email.txt"),
  423. [("M", "a.cc")])
  424. self.assertEqual(mock_emit_warning.call_count, 2)
  425. mock_emit_warning.reset_mock()
  426. # Fail if a file appears in multiple CLs
  427. self.assertRaises(
  428. split_cl.ClSplitParseError, split_cl.LoadSplittingFromFile,
  429. os.path.join(self._input_dir, "error_file_in_multiple_cls.txt"),
  430. [("M", "chrome/browser/a.cc"), ("M", "chrome/browser/b.cc"),
  431. ("M", "bar/baz/b.cc"), ("D", "foo/bar/c.h")])
  432. # Fail if a file is listed that doesn't appear on disk
  433. self.assertRaises(
  434. split_cl.ClSplitParseError, split_cl.LoadSplittingFromFile,
  435. os.path.join(self._input_dir, "no_inherent_problems.txt"),
  436. [("M", "chrome/browser/a.cc"), ("M", "chrome/browser/b.cc")])
  437. self.assertRaises(
  438. split_cl.ClSplitParseError,
  439. split_cl.LoadSplittingFromFile,
  440. os.path.join(self._input_dir, "no_inherent_problems.txt"),
  441. [
  442. ("M", "chrome/browser/a.cc"),
  443. ("M", "chrome/browser/b.cc"),
  444. ("D", "c.h") # Wrong action, should still error
  445. ])
  446. # Warn if not all files on disk are included
  447. split_cl.LoadSplittingFromFile(
  448. os.path.join(self._input_dir, "no_inherent_problems.txt"),
  449. [("M", "chrome/browser/a.cc"), ("M", "chrome/browser/b.cc"),
  450. ("A", "c.h"), ("D", "d.h")])
  451. mock_emit_warning.assert_called_once()
  452. @mock.patch("split_cl.Emit")
  453. @mock.patch("gclient_utils.FileWrite")
  454. def testParsingRoundTrip(self, mock_file_write, _):
  455. """ Make sure that if we parse a file and save the result,
  456. we get the same file. Only works on test files that are
  457. nicely formatted. """
  458. for file in os.listdir(self._input_dir):
  459. if file == "odd_formatting.txt":
  460. continue
  461. contents = gclient_utils.FileRead(
  462. os.path.join(self._input_dir, file))
  463. parsed_contents = split_cl.ParseSplittings(contents.splitlines())
  464. split_cl.SaveSplittingToFile(parsed_contents, "file.txt")
  465. written_lines = [
  466. args[0][1] for args in mock_file_write.call_args_list
  467. ]
  468. self.assertEqual(contents, "".join(written_lines))
  469. mock_file_write.reset_mock()
  470. @mock.patch("os.path.isfile", return_value=False)
  471. def testDirectoryTrie(self, _):
  472. """
  473. Simple unit tests for creating and reading from a DirectoryTrie.
  474. """
  475. # The trie code uses OS paths so we need to do the same here
  476. path_abc = os.path.join("a", "b", "c.cc")
  477. path_abd = os.path.join("a", "b", "d.h")
  478. path_aefgh = os.path.join("a", "e", "f", "g", "h.hpp")
  479. path_ijk = os.path.join("i", "j", "k.cc")
  480. path_al = os.path.join("a", "l.cpp")
  481. path_top = os.path.join("top.gn")
  482. files = [path_abc, path_abd, path_aefgh, path_ijk, path_al, path_top]
  483. split_files = [file.split(os.path.sep) for file in files]
  484. trie = split_cl.DirectoryTrie(False)
  485. trie.AddFiles(split_files)
  486. self.assertEqual(trie.files, [path_top])
  487. self.assertEqual(trie.subdirectories["a"].files, [path_al])
  488. self.assertEqual(trie.subdirectories["a"].subdirectories["b"].files,
  489. [path_abc, path_abd])
  490. self.assertEqual(sorted(trie.ToList()), sorted(files))
  491. self.assertFalse(trie.has_parent)
  492. self.assertFalse(trie.subdirectories["a"].has_parent)
  493. self.assertTrue(trie.subdirectories["a"].subdirectories["b"].has_parent)
  494. self.assertEqual(trie.prefix, "")
  495. self.assertEqual(trie.subdirectories["a"].prefix, "a")
  496. self.assertEqual(trie.subdirectories["a"].subdirectories["b"].prefix,
  497. os.path.join("a", "b"))
  498. @mock.patch("os.path.isfile", return_value=False)
  499. def testClusterFiles(self, _):
  500. """
  501. Make sure ClusterFiles returns sensible results for some sample inputs.
  502. """
  503. def compareClusterOutput(clusters: list[split_cl.Bin],
  504. file_groups: list[list[str]]):
  505. """
  506. Ensure that ClusterFiles grouped files the way we expected it to.
  507. """
  508. clustered_files = sorted([sorted(bin.files) for bin in clusters])
  509. file_groups = sorted([sorted(grp) for grp in file_groups])
  510. self.assertEqual(clustered_files, file_groups)
  511. # The clustering code uses OS paths so we need to do the same here
  512. path_abc = os.path.join("a", "b", "c.cc")
  513. path_abd = os.path.join("a", "b", "d.h")
  514. path_aefgh = os.path.join("a", "e", "f", "g", "h.hpp")
  515. path_ijk = os.path.join("i", "j", "k.cc")
  516. path_ilm = os.path.join("i", "l", "m.cc")
  517. path_an = os.path.join("a", "n.cpp")
  518. path_top = os.path.join("top.gn")
  519. files = [
  520. path_abc, path_abd, path_aefgh, path_ijk, path_ilm, path_an,
  521. path_top
  522. ]
  523. def checkClustering(min_files, max_files, expected):
  524. clusters = split_cl.ClusterFiles(False, files, min_files, max_files)
  525. compareClusterOutput(clusters, expected)
  526. # Each file gets its own cluster
  527. individual_files = [[file] for file in files]
  528. checkClustering(1, 1, individual_files)
  529. # Put both entries of a/b in the same cluster, everything else alone
  530. ab_together = [[path_abc, path_abd], [path_aefgh], [path_ijk],
  531. [path_ilm], [path_an], [path_top]]
  532. checkClustering(1, 2, ab_together)
  533. checkClustering(1, 100, ab_together)
  534. # Groups of 2: a/b, rest of a/, all of i/.
  535. a_two_groups = [[path_abc, path_abd], [path_aefgh, path_an],
  536. [path_ijk, path_ilm], [path_top]]
  537. checkClustering(2, 2, a_two_groups)
  538. checkClustering(3, 3, a_two_groups)
  539. # Put all of a/ together and all of i/ together.
  540. # Don't combine top-level directories with things at the root
  541. by_top_level_dir = [[path_abc, path_abd, path_aefgh, path_an],
  542. [path_ijk, path_ilm], [path_top]]
  543. checkClustering(3, 5, by_top_level_dir)
  544. checkClustering(100, 200, by_top_level_dir)
  545. if __name__ == '__main__':
  546. unittest.main()