You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
depot_tools/tests/split_cl_test.py

560 lines
24 KiB
Python

#!/usr/bin/env vpython3
"""Tests for split_cl."""
import os
import sys
import unittest
from unittest import mock
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import split_cl
import gclient_utils
class SplitClTest(unittest.TestCase):
@property
def _input_dir(self):
base = os.path.splitext(os.path.abspath(__file__))[0]
# Here _testMethodName is a string like "testCmdAssemblyFound"
# If the test doesn't have its own subdirectory, it uses a common one
path = os.path.join(base + ".inputs", self._testMethodName)
if not os.path.isdir(path):
path = os.path.join(base + ".inputs", "commonFiles")
return path
def testAddUploadedByGitClSplitToDescription(self):
description = """Convert use of X to Y in $description
<add some background about this conversion for the reviewers>
"""
footers = 'Bug: 12345'
added_line = 'This CL was uploaded by git cl split.'
# Description without footers
self.assertEqual(
split_cl.AddUploadedByGitClSplitToDescription(description),
description + added_line)
# Description with footers
self.assertEqual(
split_cl.AddUploadedByGitClSplitToDescription(description +
footers),
description + added_line + '\n\n' + footers)
@mock.patch("split_cl.EmitWarning")
def testFormatDescriptionOrComment(self, mock_emit_warning):
description = "Converted use of X to Y in $description."
# One directory
self.assertEqual(
split_cl.FormatDescriptionOrComment(
description, split_cl.FormatDirectoriesForPrinting(["foo"])),
"Converted use of X to Y in foo.",
)
# Many directories
self.assertEqual(
split_cl.FormatDescriptionOrComment(
description,
split_cl.FormatDirectoriesForPrinting(["foo", "bar"])),
"Converted use of X to Y in ['foo', 'bar'].",
)
mock_emit_warning.assert_not_called()
description_deprecated = "Converted use of X to Y in $directory."
# Make sure we emit a deprecation warning if the old format is used
self.assertEqual(
split_cl.FormatDescriptionOrComment(
description_deprecated,
split_cl.FormatDirectoriesForPrinting([])),
"Converted use of X to Y in [].",
)
mock_emit_warning.assert_called_once()
def GetDirectoryBaseName(self, file_path):
return os.path.basename(os.path.dirname(file_path))
def MockSuggestOwners(self, paths, exclude=None):
if not paths:
return ["superowner"]
return self.GetDirectoryBaseName(paths[0]).split(",")
def MockIsFile(self, file_path):
if os.path.basename(file_path) == "OWNERS":
return "owner" in self.GetDirectoryBaseName(file_path)
return True
@mock.patch("os.path.isfile")
def testSelectReviewersForFiles(self, mock_is_file):
mock_is_file.side_effect = self.MockIsFile
owners_client = mock.Mock(SuggestOwners=self.MockSuggestOwners,
EVERYONE="*")
cl = mock.Mock(owners_client=owners_client)
files = [("M", os.path.join("foo", "owner1,owner2", "a.txt")),
("M", os.path.join("foo", "owner1,owner2", "b.txt")),
("M", os.path.join("bar", "owner1,owner2", "c.txt")),
("M", os.path.join("bax", "owner2", "d.txt")),
("M", os.path.join("baz", "owner3", "e.txt"))]
files_split_by_reviewers = split_cl.SelectReviewersForFiles(
cl, "author", files, 0)
self.assertEqual(3, len(files_split_by_reviewers.keys()))
info1 = files_split_by_reviewers[tuple(["owner1", "owner2"])]
self.assertEqual(info1.files,
[("M", os.path.join("foo", "owner1,owner2", "a.txt")),
("M", os.path.join("foo", "owner1,owner2", "b.txt")),
("M", os.path.join("bar", "owner1,owner2", "c.txt"))])
self.assertEqual(info1.owners_directories,
["foo/owner1,owner2", "bar/owner1,owner2"])
info2 = files_split_by_reviewers[tuple(["owner2"])]
self.assertEqual(info2.files,
[("M", os.path.join("bax", "owner2", "d.txt"))])
self.assertEqual(info2.owners_directories, ["bax/owner2"])
info3 = files_split_by_reviewers[tuple(["owner3"])]
self.assertEqual(info3.files,
[("M", os.path.join("baz", "owner3", "e.txt"))])
self.assertEqual(info3.owners_directories, ["baz/owner3"])
class UploadClTester:
"""Sets up test environment for testing split_cl.UploadCl()"""
def __init__(self, test):
self.mock_git_branches = self.StartPatcher("git_common.branches",
test)
self.mock_git_branches.return_value = []
self.mock_git_current_branch = self.StartPatcher(
"git_common.current_branch", test)
self.mock_git_current_branch.return_value = "branch_to_upload"
self.mock_git_run = self.StartPatcher("git_common.run", test)
self.mock_temporary_file = self.StartPatcher(
"gclient_utils.temporary_file", test)
self.mock_temporary_file(
).__enter__.return_value = "temporary_file0"
self.mock_file_writer = self.StartPatcher("gclient_utils.FileWrite",
test)
def StartPatcher(self, target, test):
patcher = mock.patch(target)
test.addCleanup(patcher.stop)
return patcher.start()
def DoUploadCl(self, description, files, reviewers, cmd_upload):
split_cl.UploadCl("branch_to_upload", "upstream_branch",
description, files, "description",
"splitting_file.txt", None, reviewers,
mock.Mock(), cmd_upload, True, True, "topic",
os.path.sep)
def testUploadCl(self):
"""Tests commands run by UploadCl."""
upload_cl_tester = self.UploadClTester(self)
description = split_cl.FormatDirectoriesForPrinting(["dir0"])
files = [("M", os.path.join("bar", "a.cc")),
("D", os.path.join("foo", "b.cc"))]
reviewers = {"reviewer1@gmail.com", "reviewer2@gmail.com"}
mock_cmd_upload = mock.Mock()
upload_cl_tester.DoUploadCl(description, files, reviewers,
mock_cmd_upload)
abs_repository_path = os.path.abspath(os.path.sep)
mock_git_run = upload_cl_tester.mock_git_run
self.assertEqual(mock_git_run.call_count, 4)
mock_git_run.assert_has_calls([
mock.call("checkout", "-t", "upstream_branch", "-b",
split_cl.CreateBranchName("branch_to_upload", files)),
mock.call("rm", os.path.join(abs_repository_path, "foo", "b.cc")),
mock.call("checkout", "branch_to_upload", "--",
os.path.join(abs_repository_path, "bar", "a.cc")),
mock.call("commit", "-F", "temporary_file0")
])
expected_upload_args = [
"-f", "-r", "reviewer1@gmail.com,reviewer2@gmail.com",
"--cq-dry-run", "--send-mail", "--enable-auto-submit",
"--topic=topic"
]
mock_cmd_upload.assert_called_once_with(expected_upload_args)
def testDontUploadClIfBranchAlreadyExists(self):
"""Tests that a CL is not uploaded if split branch already exists"""
upload_cl_tester = self.UploadClTester(self)
description = split_cl.FormatDirectoriesForPrinting(["dir0"])
files = [("M", os.path.join("bar", "a.cc")),
("D", os.path.join("foo", "b.cc"))]
reviewers = {"reviewer1@gmail.com"}
mock_cmd_upload = mock.Mock()
upload_cl_tester.mock_git_branches.return_value = [
"branch0",
split_cl.CreateBranchName("branch_to_upload", files)
]
upload_cl_tester.DoUploadCl(description, files, reviewers,
mock_cmd_upload)
upload_cl_tester.mock_git_run.assert_not_called()
mock_cmd_upload.assert_not_called()
@mock.patch("gclient_utils.AskForData")
def testCheckDescriptionBugLink(self, mock_ask_for_data):
# Description contains bug link.
self.assertTrue(split_cl.CheckDescriptionBugLink("Bug:1234"))
self.assertEqual(mock_ask_for_data.call_count, 0)
# Description does not contain bug link. User does not enter 'y' when
# prompted.
mock_ask_for_data.reset_mock()
mock_ask_for_data.return_value = "m"
self.assertFalse(split_cl.CheckDescriptionBugLink("Description"))
self.assertEqual(mock_ask_for_data.call_count, 1)
# Description does not contain bug link. User enters 'y' when prompted.
mock_ask_for_data.reset_mock()
mock_ask_for_data.return_value = "y"
self.assertTrue(split_cl.CheckDescriptionBugLink("Description"))
self.assertEqual(mock_ask_for_data.call_count, 1)
@mock.patch("gclient_utils.FileRead", return_value="Description")
def testLoadDescription(self, mock_file_read):
# No description provided, use the dummy:
self.assertTrue(
split_cl.LoadDescription(None, True).startswith("Dummy"))
self.assertEqual(mock_file_read.call_count, 0)
# No description provided during a real run
self.assertRaises(ValueError, split_cl.LoadDescription, None, False)
self.assertEqual(mock_file_read.call_count, 0)
# Description file provided, load it regardless of dry run
self.assertEqual(split_cl.LoadDescription("SomeFile.txt", False),
"Description")
self.assertEqual(mock_file_read.call_count, 1)
mock_file_read.reset_mock()
self.assertEqual(split_cl.LoadDescription("SomeFile.txt", True),
"Description")
self.assertEqual(mock_file_read.call_count, 1)
class SplitClTester:
"""Sets up test environment for testing split_cl.SplitCl()"""
def __init__(self, test):
self.mocks = []
self.mock_file_read = self.StartPatcher(
"gclient_utils.FileRead",
test,
return_value="Non-dummy description\nBug: 1243")
self.mock_in_git_repo = self.StartPatcher(
"split_cl.EnsureInGitRepository", test)
self.mock_git_status = self.StartPatcher("scm.GIT.CaptureStatus",
test)
self.mock_git_run = self.StartPatcher("git_common.run", test)
self.mock_git_current_branch = self.StartPatcher(
"git_common.current_branch",
test,
return_value="branch_to_upload")
self.mock_git_branches = self.StartPatcher("git_common.branches",
test)
self.mock_git_upstream = self.StartPatcher(
"git_common.upstream", test, return_value="upstream_branch")
self.mock_get_reviewers = self.StartPatcher(
"split_cl.SelectReviewersForFiles", test)
self.mock_ask_for_data = self.StartPatcher(
"gclient_utils.AskForData", test)
self.mock_print_cl_info = self.StartPatcher("split_cl.PrintClInfo",
test)
self.mock_print_summary = self.StartPatcher("split_cl.PrintSummary",
test)
self.mock_upload_cl = self.StartPatcher("split_cl.UploadCl", test)
self.mock_save_splitting = self.StartPatcher(
"split_cl.SaveSplittingToTempFile", test)
# Suppress output for cleaner tests
self.mock_emit = self.StartPatcher("split_cl.Emit", test)
def StartPatcher(self, target, test, **kwargs):
patcher = mock.patch(target, **kwargs)
test.addCleanup(patcher.stop)
m = patcher.start()
self.mocks.append(m)
return m
def ResetMocks(self):
for m in self.mocks:
m.reset_mock()
def DoSplitCl(self, description_file, dry_run, summarize,
reviewers_override, files_split_by_reviewers,
proceed_response):
all_files = [v.files for v in files_split_by_reviewers.values()]
all_files_flattened = [
file for files in all_files for file in files
]
self.mock_git_status.return_value = all_files_flattened
self.mock_get_reviewers.return_value = files_split_by_reviewers
self.mock_ask_for_data.return_value = proceed_response
split_cl.SplitCl(description_file, None, mock.Mock(), mock.Mock(),
dry_run, summarize, reviewers_override, False,
False, None, None, None, None)
# Save for re-use
files_split_by_reviewers = {
("a@example.com", ):
split_cl.FilesAndOwnersDirectory([
("M", "a/b/foo.cc"),
("M", "d/e/bar.h"),
], []),
("b@example.com", ):
split_cl.FilesAndOwnersDirectory([
("A", "f/g/baz.py"),
], [])
}
def testSplitClConfirm(self):
split_cl_tester = self.SplitClTester(self)
# Should prompt for confirmation and upload several times
split_cl_tester.DoSplitCl("SomeFile.txt", False, False, None,
self.files_split_by_reviewers, "y")
split_cl_tester.mock_ask_for_data.assert_called_once()
split_cl_tester.mock_print_cl_info.assert_not_called()
self.assertEqual(split_cl_tester.mock_upload_cl.call_count,
len(self.files_split_by_reviewers))
split_cl_tester.ResetMocks()
# Should prompt for confirmation and not upload
split_cl_tester.DoSplitCl("SomeFile.txt", False, False, None,
self.files_split_by_reviewers, "f")
split_cl_tester.mock_ask_for_data.assert_called_once()
split_cl_tester.mock_print_cl_info.assert_not_called()
split_cl_tester.mock_upload_cl.assert_not_called()
split_cl_tester.ResetMocks()
# Dry runs: Don't prompt, print info instead of uploading
split_cl_tester.DoSplitCl("SomeFile.txt", True, False, None,
self.files_split_by_reviewers, "f")
split_cl_tester.mock_ask_for_data.assert_not_called()
self.assertEqual(split_cl_tester.mock_print_cl_info.call_count,
len(self.files_split_by_reviewers))
split_cl_tester.mock_print_summary.assert_not_called()
split_cl_tester.mock_upload_cl.assert_not_called()
split_cl_tester.ResetMocks()
# Summarize is true: Don't prompt, emit a summary
split_cl_tester.DoSplitCl("SomeFile.txt", True, True, None,
self.files_split_by_reviewers, "f")
split_cl_tester.mock_ask_for_data.assert_not_called()
split_cl_tester.mock_print_cl_info.assert_not_called()
split_cl_tester.mock_print_summary.assert_called_once()
split_cl_tester.mock_upload_cl.assert_not_called()
def testReviewerOverride(self):
split_cl_tester = self.SplitClTester(self)
def testOneOverride(reviewers_lst):
split_cl_tester.DoSplitCl("SomeFile.txt", False, False,
reviewers_lst,
self.files_split_by_reviewers, "y")
for call in split_cl_tester.mock_upload_cl.call_args_list:
self.assertEqual(call.args[7], set(reviewers_lst))
split_cl_tester.ResetMocks()
# The 'None' case gets ample testing everywhere else
testOneOverride([])
testOneOverride(['a@b.com', 'c@d.com'])
def testValidateExistingBranches(self):
"""
Make sure that we skip existing branches if they match what we intend
to do, and fail if there are existing branches that don't match.
"""
split_cl_tester = self.SplitClTester(self)
# If no split branches exist, we should call upload once per CL
split_cl_tester.mock_git_branches.return_value = [
"branch0", "branch_to_upload"
]
split_cl_tester.DoSplitCl("SomeFile.txt", False, False, None,
self.files_split_by_reviewers, "y")
self.assertEqual(split_cl_tester.mock_upload_cl.call_count,
len(self.files_split_by_reviewers))
# TODO(389069356): We should also ensure that if there are existing
# branches that match our current splitting, we skip them when uploading
# Unfortunately, we're not set up to test that, so this will have to
# wait until we've refactored SplitCl and UploadCL to be less
# monolithic
# If a split branch with a bad name already exists, we should fail
split_cl_tester.mock_upload_cl.reset_mock()
split_cl_tester.mock_git_branches.return_value = [
"branch0", "branch_to_upload",
"branch_to_upload_123456789_whatever_split"
]
split_cl_tester.DoSplitCl("SomeFile.txt", False, False, None,
self.files_split_by_reviewers, "y")
split_cl_tester.mock_upload_cl.assert_not_called()
# Tests related to saving to and loading from files
# Sample CLInfos for testing
CLInfo_1 = split_cl.CLInfo(reviewers=["a@example.com"],
description="['chrome/browser']",
files=[
("M", "chrome/browser/a.cc"),
("M", "chrome/browser/b.cc"),
])
CLInfo_2 = split_cl.CLInfo(reviewers=["a@example.com", "b@example.com"],
description="['foo', 'bar/baz']",
files=[("M", "foo/browser/a.cc"),
("M", "bar/baz/b.cc"),
("D", "foo/bar/c.h")])
def testCLInfoFormat(self):
""" Make sure CLInfo printing works as expected """
def ReadAndStripPreamble(file):
""" Read the contents of a file and strip the automatically-added
preamble so we can do string comparison
"""
content = gclient_utils.FileRead(os.path.join(
self._input_dir, file))
# Strip preamble
stripped = [
line for line in content.splitlines()
if not line.startswith("#")
]
# Strip newlines in preamble
return "\n".join(stripped[2:])
# Direct string comparison
self.assertEqual(self.CLInfo_1.FormatForPrinting(),
ReadAndStripPreamble("1_cl.txt"))
self.assertEqual(
self.CLInfo_1.FormatForPrinting() +
"\n\n" + self.CLInfo_2.FormatForPrinting(),
ReadAndStripPreamble("2_cls.txt"))
@mock.patch("split_cl.EmitWarning")
def testParseCLInfo(self, mock_emit_warning):
""" Make sure we can parse valid files """
self.assertEqual([self.CLInfo_1],
split_cl.LoadSplittingFromFile(
os.path.join(self._input_dir, "1_cl.txt"),
self.CLInfo_1.files))
self.assertEqual([self.CLInfo_1, self.CLInfo_2],
split_cl.LoadSplittingFromFile(
os.path.join(self._input_dir, "2_cls.txt"),
self.CLInfo_1.files + self.CLInfo_2.files))
# Make sure everything in this file is valid to parse
split_cl.LoadSplittingFromFile(
os.path.join(self._input_dir, "odd_formatting.txt"),
self.CLInfo_1.files + self.CLInfo_2.files + [("A", "a/b/c"),
("A", "a/b/d"),
("D", "a/e")])
mock_emit_warning.assert_not_called()
def testParseBadFiles(self):
""" Make sure we don't parse invalid files """
for file in os.listdir(self._input_dir):
lines = gclient_utils.FileRead(os.path.join(self._input_dir,
file)).splitlines()
self.assertRaises(split_cl.ClSplitParseError,
split_cl.ParseSplittings, lines)
@mock.patch("split_cl.EmitWarning")
def testValidateBadFiles(self, mock_emit_warning):
""" Make sure we reject invalid CL lists """
# Warn on an empty file
split_cl.LoadSplittingFromFile(
os.path.join(self._input_dir, "warn_0_cls.txt"), [])
mock_emit_warning.assert_called_once()
mock_emit_warning.reset_mock()
# Warn if reviewers don't look like emails
split_cl.LoadSplittingFromFile(
os.path.join(self._input_dir, "warn_bad_reviewer_email.txt"),
[("M", "a.cc")])
self.assertEqual(mock_emit_warning.call_count, 2)
mock_emit_warning.reset_mock()
# Fail if a file appears in multiple CLs
self.assertRaises(
split_cl.ClSplitParseError, split_cl.LoadSplittingFromFile,
os.path.join(self._input_dir, "error_file_in_multiple_cls.txt"),
[("M", "chrome/browser/a.cc"), ("M", "chrome/browser/b.cc"),
("M", "bar/baz/b.cc"), ("D", "foo/bar/c.h")])
# Fail if a file is listed that doesn't appear on disk
self.assertRaises(
split_cl.ClSplitParseError, split_cl.LoadSplittingFromFile,
os.path.join(self._input_dir, "no_inherent_problems.txt"),
[("M", "chrome/browser/a.cc"), ("M", "chrome/browser/b.cc")])
self.assertRaises(
split_cl.ClSplitParseError,
split_cl.LoadSplittingFromFile,
os.path.join(self._input_dir, "no_inherent_problems.txt"),
[
("M", "chrome/browser/a.cc"),
("M", "chrome/browser/b.cc"),
("D", "c.h") # Wrong action, should still error
])
# Warn if not all files on disk are included
split_cl.LoadSplittingFromFile(
os.path.join(self._input_dir, "no_inherent_problems.txt"),
[("M", "chrome/browser/a.cc"), ("M", "chrome/browser/b.cc"),
("A", "c.h"), ("D", "d.h")])
mock_emit_warning.assert_called_once()
@mock.patch("split_cl.Emit")
@mock.patch("gclient_utils.FileWrite")
def testParsingRoundTrip(self, mock_file_write, _):
""" Make sure that if we parse a file and save the result,
we get the same file. Only works on test files that are
nicely formatted. """
for file in os.listdir(self._input_dir):
if file == "odd_formatting.txt":
continue
contents = gclient_utils.FileRead(
os.path.join(self._input_dir, file))
parsed_contents = split_cl.ParseSplittings(contents.splitlines())
split_cl.SaveSplittingToFile(parsed_contents, "file.txt")
written_lines = [
args[0][1] for args in mock_file_write.call_args_list
]
self.assertEqual(contents, "".join(written_lines))
mock_file_write.reset_mock()
if __name__ == '__main__':
unittest.main()