diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/fixtures/test.gitconfig | 10 | ||||
-rw-r--r-- | tests/test_git_command.py | 27 | ||||
-rw-r--r-- | tests/test_git_config.py | 19 | ||||
-rw-r--r-- | tests/test_git_superproject.py | 260 | ||||
-rw-r--r-- | tests/test_git_trace2_event_log.py | 72 | ||||
-rw-r--r-- | tests/test_manifest_xml.py | 346 | ||||
-rw-r--r-- | tests/test_platform_utils.py | 50 | ||||
-rw-r--r-- | tests/test_ssh.py | 74 | ||||
-rw-r--r-- | tests/test_subcmds.py | 30 |
9 files changed, 801 insertions, 87 deletions
diff --git a/tests/fixtures/test.gitconfig b/tests/fixtures/test.gitconfig index 9b3f2574..b178cf60 100644 --- a/tests/fixtures/test.gitconfig +++ b/tests/fixtures/test.gitconfig | |||
@@ -11,3 +11,13 @@ | |||
11 | intk = 10k | 11 | intk = 10k |
12 | intm = 10m | 12 | intm = 10m |
13 | intg = 10g | 13 | intg = 10g |
14 | [repo "syncstate.main"] | ||
15 | synctime = 2021-09-14T17:23:43.537338Z | ||
16 | version = 1 | ||
17 | [repo "syncstate.sys"] | ||
18 | argv = ['/usr/bin/pytest-3'] | ||
19 | [repo "syncstate.superproject"] | ||
20 | test = false | ||
21 | [repo "syncstate.options"] | ||
22 | verbose = true | ||
23 | mpupdate = false | ||
diff --git a/tests/test_git_command.py b/tests/test_git_command.py index 912a9dbe..93300a6f 100644 --- a/tests/test_git_command.py +++ b/tests/test_git_command.py | |||
@@ -26,33 +26,6 @@ import git_command | |||
26 | import wrapper | 26 | import wrapper |
27 | 27 | ||
28 | 28 | ||
29 | class SSHUnitTest(unittest.TestCase): | ||
30 | """Tests the ssh functions.""" | ||
31 | |||
32 | def test_ssh_version(self): | ||
33 | """Check ssh_version() handling.""" | ||
34 | ver = git_command._parse_ssh_version('Unknown\n') | ||
35 | self.assertEqual(ver, ()) | ||
36 | ver = git_command._parse_ssh_version('OpenSSH_1.0\n') | ||
37 | self.assertEqual(ver, (1, 0)) | ||
38 | ver = git_command._parse_ssh_version('OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n') | ||
39 | self.assertEqual(ver, (6, 6, 1)) | ||
40 | ver = git_command._parse_ssh_version('OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n') | ||
41 | self.assertEqual(ver, (7, 6)) | ||
42 | |||
43 | def test_ssh_sock(self): | ||
44 | """Check ssh_sock() function.""" | ||
45 | with mock.patch('tempfile.mkdtemp', return_value='/tmp/foo'): | ||
46 | # old ssh version uses port | ||
47 | with mock.patch('git_command.ssh_version', return_value=(6, 6)): | ||
48 | self.assertTrue(git_command.ssh_sock().endswith('%p')) | ||
49 | git_command._ssh_sock_path = None | ||
50 | # new ssh version uses hash | ||
51 | with mock.patch('git_command.ssh_version', return_value=(6, 7)): | ||
52 | self.assertTrue(git_command.ssh_sock().endswith('%C')) | ||
53 | git_command._ssh_sock_path = None | ||
54 | |||
55 | |||
56 | class GitCallUnitTest(unittest.TestCase): | 29 | class GitCallUnitTest(unittest.TestCase): |
57 | """Tests the _GitCall class (via git_command.git).""" | 30 | """Tests the _GitCall class (via git_command.git).""" |
58 | 31 | ||
diff --git a/tests/test_git_config.py b/tests/test_git_config.py index 3300c12f..faf12a2e 100644 --- a/tests/test_git_config.py +++ b/tests/test_git_config.py | |||
@@ -104,6 +104,25 @@ class GitConfigReadOnlyTests(unittest.TestCase): | |||
104 | for key, value in TESTS: | 104 | for key, value in TESTS: |
105 | self.assertEqual(value, self.config.GetInt('section.%s' % (key,))) | 105 | self.assertEqual(value, self.config.GetInt('section.%s' % (key,))) |
106 | 106 | ||
107 | def test_GetSyncAnalysisStateData(self): | ||
108 | """Test config entries with a sync state analysis data.""" | ||
109 | superproject_logging_data = {} | ||
110 | superproject_logging_data['test'] = False | ||
111 | options = type('options', (object,), {})() | ||
112 | options.verbose = 'true' | ||
113 | options.mp_update = 'false' | ||
114 | TESTS = ( | ||
115 | ('superproject.test', 'false'), | ||
116 | ('options.verbose', 'true'), | ||
117 | ('options.mpupdate', 'false'), | ||
118 | ('main.version', '1'), | ||
119 | ) | ||
120 | self.config.UpdateSyncAnalysisState(options, superproject_logging_data) | ||
121 | sync_data = self.config.GetSyncAnalysisStateData() | ||
122 | for key, value in TESTS: | ||
123 | self.assertEqual(sync_data[f'{git_config.SYNC_STATE_PREFIX}{key}'], value) | ||
124 | self.assertTrue(sync_data[f'{git_config.SYNC_STATE_PREFIX}main.synctime']) | ||
125 | |||
107 | 126 | ||
108 | class GitConfigReadWriteTests(unittest.TestCase): | 127 | class GitConfigReadWriteTests(unittest.TestCase): |
109 | """Read/write tests of the GitConfig class.""" | 128 | """Read/write tests of the GitConfig class.""" |
diff --git a/tests/test_git_superproject.py b/tests/test_git_superproject.py index 9550949b..a24fc7f0 100644 --- a/tests/test_git_superproject.py +++ b/tests/test_git_superproject.py | |||
@@ -14,6 +14,7 @@ | |||
14 | 14 | ||
15 | """Unittests for the git_superproject.py module.""" | 15 | """Unittests for the git_superproject.py module.""" |
16 | 16 | ||
17 | import json | ||
17 | import os | 18 | import os |
18 | import platform | 19 | import platform |
19 | import tempfile | 20 | import tempfile |
@@ -21,13 +22,20 @@ import unittest | |||
21 | from unittest import mock | 22 | from unittest import mock |
22 | 23 | ||
23 | import git_superproject | 24 | import git_superproject |
25 | import git_trace2_event_log | ||
24 | import manifest_xml | 26 | import manifest_xml |
25 | import platform_utils | 27 | import platform_utils |
28 | from test_manifest_xml import sort_attributes | ||
26 | 29 | ||
27 | 30 | ||
28 | class SuperprojectTestCase(unittest.TestCase): | 31 | class SuperprojectTestCase(unittest.TestCase): |
29 | """TestCase for the Superproject module.""" | 32 | """TestCase for the Superproject module.""" |
30 | 33 | ||
34 | PARENT_SID_KEY = 'GIT_TRACE2_PARENT_SID' | ||
35 | PARENT_SID_VALUE = 'parent_sid' | ||
36 | SELF_SID_REGEX = r'repo-\d+T\d+Z-.*' | ||
37 | FULL_SID_REGEX = r'^%s/%s' % (PARENT_SID_VALUE, SELF_SID_REGEX) | ||
38 | |||
31 | def setUp(self): | 39 | def setUp(self): |
32 | """Set up superproject every time.""" | 40 | """Set up superproject every time.""" |
33 | self.tempdir = tempfile.mkdtemp(prefix='repo_tests') | 41 | self.tempdir = tempfile.mkdtemp(prefix='repo_tests') |
@@ -37,6 +45,13 @@ class SuperprojectTestCase(unittest.TestCase): | |||
37 | os.mkdir(self.repodir) | 45 | os.mkdir(self.repodir) |
38 | self.platform = platform.system().lower() | 46 | self.platform = platform.system().lower() |
39 | 47 | ||
48 | # By default we initialize with the expected case where | ||
49 | # repo launches us (so GIT_TRACE2_PARENT_SID is set). | ||
50 | env = { | ||
51 | self.PARENT_SID_KEY: self.PARENT_SID_VALUE, | ||
52 | } | ||
53 | self.git_event_log = git_trace2_event_log.EventLog(env=env) | ||
54 | |||
40 | # The manifest parsing really wants a git repo currently. | 55 | # The manifest parsing really wants a git repo currently. |
41 | gitdir = os.path.join(self.repodir, 'manifests.git') | 56 | gitdir = os.path.join(self.repodir, 'manifests.git') |
42 | os.mkdir(gitdir) | 57 | os.mkdir(gitdir) |
@@ -53,7 +68,8 @@ class SuperprojectTestCase(unittest.TestCase): | |||
53 | <project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """ | 68 | <project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """ |
54 | " /></manifest> | 69 | " /></manifest> |
55 | """) | 70 | """) |
56 | self._superproject = git_superproject.Superproject(manifest, self.repodir) | 71 | self._superproject = git_superproject.Superproject(manifest, self.repodir, |
72 | self.git_event_log) | ||
57 | 73 | ||
58 | def tearDown(self): | 74 | def tearDown(self): |
59 | """Tear down superproject every time.""" | 75 | """Tear down superproject every time.""" |
@@ -65,14 +81,56 @@ class SuperprojectTestCase(unittest.TestCase): | |||
65 | fp.write(data) | 81 | fp.write(data) |
66 | return manifest_xml.XmlManifest(self.repodir, self.manifest_file) | 82 | return manifest_xml.XmlManifest(self.repodir, self.manifest_file) |
67 | 83 | ||
84 | def verifyCommonKeys(self, log_entry, expected_event_name, full_sid=True): | ||
85 | """Helper function to verify common event log keys.""" | ||
86 | self.assertIn('event', log_entry) | ||
87 | self.assertIn('sid', log_entry) | ||
88 | self.assertIn('thread', log_entry) | ||
89 | self.assertIn('time', log_entry) | ||
90 | |||
91 | # Do basic data format validation. | ||
92 | self.assertEqual(expected_event_name, log_entry['event']) | ||
93 | if full_sid: | ||
94 | self.assertRegex(log_entry['sid'], self.FULL_SID_REGEX) | ||
95 | else: | ||
96 | self.assertRegex(log_entry['sid'], self.SELF_SID_REGEX) | ||
97 | self.assertRegex(log_entry['time'], r'^\d+-\d+-\d+T\d+:\d+:\d+\.\d+Z$') | ||
98 | |||
99 | def readLog(self, log_path): | ||
100 | """Helper function to read log data into a list.""" | ||
101 | log_data = [] | ||
102 | with open(log_path, mode='rb') as f: | ||
103 | for line in f: | ||
104 | log_data.append(json.loads(line)) | ||
105 | return log_data | ||
106 | |||
107 | def verifyErrorEvent(self): | ||
108 | """Helper to verify that error event is written.""" | ||
109 | |||
110 | with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir: | ||
111 | log_path = self.git_event_log.Write(path=tempdir) | ||
112 | self.log_data = self.readLog(log_path) | ||
113 | |||
114 | self.assertEqual(len(self.log_data), 2) | ||
115 | error_event = self.log_data[1] | ||
116 | self.verifyCommonKeys(self.log_data[0], expected_event_name='version') | ||
117 | self.verifyCommonKeys(error_event, expected_event_name='error') | ||
118 | # Check for 'error' event specific fields. | ||
119 | self.assertIn('msg', error_event) | ||
120 | self.assertIn('fmt', error_event) | ||
121 | |||
68 | def test_superproject_get_superproject_no_superproject(self): | 122 | def test_superproject_get_superproject_no_superproject(self): |
69 | """Test with no url.""" | 123 | """Test with no url.""" |
70 | manifest = self.getXmlManifest(""" | 124 | manifest = self.getXmlManifest(""" |
71 | <manifest> | 125 | <manifest> |
72 | </manifest> | 126 | </manifest> |
73 | """) | 127 | """) |
74 | superproject = git_superproject.Superproject(manifest, self.repodir) | 128 | superproject = git_superproject.Superproject(manifest, self.repodir, self.git_event_log) |
75 | self.assertFalse(superproject.Sync()) | 129 | # Test that exit condition is false when there is no superproject tag. |
130 | sync_result = superproject.Sync() | ||
131 | self.assertFalse(sync_result.success) | ||
132 | self.assertFalse(sync_result.fatal) | ||
133 | self.verifyErrorEvent() | ||
76 | 134 | ||
77 | def test_superproject_get_superproject_invalid_url(self): | 135 | def test_superproject_get_superproject_invalid_url(self): |
78 | """Test with an invalid url.""" | 136 | """Test with an invalid url.""" |
@@ -83,8 +141,10 @@ class SuperprojectTestCase(unittest.TestCase): | |||
83 | <superproject name="superproject"/> | 141 | <superproject name="superproject"/> |
84 | </manifest> | 142 | </manifest> |
85 | """) | 143 | """) |
86 | superproject = git_superproject.Superproject(manifest, self.repodir) | 144 | superproject = git_superproject.Superproject(manifest, self.repodir, self.git_event_log) |
87 | self.assertFalse(superproject.Sync()) | 145 | sync_result = superproject.Sync() |
146 | self.assertFalse(sync_result.success) | ||
147 | self.assertTrue(sync_result.fatal) | ||
88 | 148 | ||
89 | def test_superproject_get_superproject_invalid_branch(self): | 149 | def test_superproject_get_superproject_invalid_branch(self): |
90 | """Test with an invalid branch.""" | 150 | """Test with an invalid branch.""" |
@@ -95,21 +155,28 @@ class SuperprojectTestCase(unittest.TestCase): | |||
95 | <superproject name="superproject"/> | 155 | <superproject name="superproject"/> |
96 | </manifest> | 156 | </manifest> |
97 | """) | 157 | """) |
98 | superproject = git_superproject.Superproject(manifest, self.repodir) | 158 | self._superproject = git_superproject.Superproject(manifest, self.repodir, |
99 | with mock.patch.object(self._superproject, '_GetBranch', return_value='junk'): | 159 | self.git_event_log) |
100 | self.assertFalse(superproject.Sync()) | 160 | with mock.patch.object(self._superproject, '_branch', 'junk'): |
161 | sync_result = self._superproject.Sync() | ||
162 | self.assertFalse(sync_result.success) | ||
163 | self.assertTrue(sync_result.fatal) | ||
101 | 164 | ||
102 | def test_superproject_get_superproject_mock_init(self): | 165 | def test_superproject_get_superproject_mock_init(self): |
103 | """Test with _Init failing.""" | 166 | """Test with _Init failing.""" |
104 | with mock.patch.object(self._superproject, '_Init', return_value=False): | 167 | with mock.patch.object(self._superproject, '_Init', return_value=False): |
105 | self.assertFalse(self._superproject.Sync()) | 168 | sync_result = self._superproject.Sync() |
169 | self.assertFalse(sync_result.success) | ||
170 | self.assertTrue(sync_result.fatal) | ||
106 | 171 | ||
107 | def test_superproject_get_superproject_mock_fetch(self): | 172 | def test_superproject_get_superproject_mock_fetch(self): |
108 | """Test with _Fetch failing.""" | 173 | """Test with _Fetch failing.""" |
109 | with mock.patch.object(self._superproject, '_Init', return_value=True): | 174 | with mock.patch.object(self._superproject, '_Init', return_value=True): |
110 | os.mkdir(self._superproject._superproject_path) | 175 | os.mkdir(self._superproject._superproject_path) |
111 | with mock.patch.object(self._superproject, '_Fetch', return_value=False): | 176 | with mock.patch.object(self._superproject, '_Fetch', return_value=False): |
112 | self.assertFalse(self._superproject.Sync()) | 177 | sync_result = self._superproject.Sync() |
178 | self.assertFalse(sync_result.success) | ||
179 | self.assertTrue(sync_result.fatal) | ||
113 | 180 | ||
114 | def test_superproject_get_all_project_commit_ids_mock_ls_tree(self): | 181 | def test_superproject_get_all_project_commit_ids_mock_ls_tree(self): |
115 | """Test with LsTree being a mock.""" | 182 | """Test with LsTree being a mock.""" |
@@ -121,12 +188,13 @@ class SuperprojectTestCase(unittest.TestCase): | |||
121 | with mock.patch.object(self._superproject, '_Init', return_value=True): | 188 | with mock.patch.object(self._superproject, '_Init', return_value=True): |
122 | with mock.patch.object(self._superproject, '_Fetch', return_value=True): | 189 | with mock.patch.object(self._superproject, '_Fetch', return_value=True): |
123 | with mock.patch.object(self._superproject, '_LsTree', return_value=data): | 190 | with mock.patch.object(self._superproject, '_LsTree', return_value=data): |
124 | commit_ids = self._superproject._GetAllProjectsCommitIds() | 191 | commit_ids_result = self._superproject._GetAllProjectsCommitIds() |
125 | self.assertEqual(commit_ids, { | 192 | self.assertEqual(commit_ids_result.commit_ids, { |
126 | 'art': '2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea', | 193 | 'art': '2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea', |
127 | 'bootable/recovery': 'e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06', | 194 | 'bootable/recovery': 'e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06', |
128 | 'build/bazel': 'ade9b7a0d874e25fff4bf2552488825c6f111928' | 195 | 'build/bazel': 'ade9b7a0d874e25fff4bf2552488825c6f111928' |
129 | }) | 196 | }) |
197 | self.assertFalse(commit_ids_result.fatal) | ||
130 | 198 | ||
131 | def test_superproject_write_manifest_file(self): | 199 | def test_superproject_write_manifest_file(self): |
132 | """Test with writing manifest to a file after setting revisionId.""" | 200 | """Test with writing manifest to a file after setting revisionId.""" |
@@ -135,18 +203,18 @@ class SuperprojectTestCase(unittest.TestCase): | |||
135 | project.SetRevisionId('ABCDEF') | 203 | project.SetRevisionId('ABCDEF') |
136 | # Create temporary directory so that it can write the file. | 204 | # Create temporary directory so that it can write the file. |
137 | os.mkdir(self._superproject._superproject_path) | 205 | os.mkdir(self._superproject._superproject_path) |
138 | manifest_path = self._superproject._WriteManfiestFile() | 206 | manifest_path = self._superproject._WriteManifestFile() |
139 | self.assertIsNotNone(manifest_path) | 207 | self.assertIsNotNone(manifest_path) |
140 | with open(manifest_path, 'r') as fp: | 208 | with open(manifest_path, 'r') as fp: |
141 | manifest_xml = fp.read() | 209 | manifest_xml_data = fp.read() |
142 | self.assertEqual( | 210 | self.assertEqual( |
143 | manifest_xml, | 211 | sort_attributes(manifest_xml_data), |
144 | '<?xml version="1.0" ?><manifest>' + | 212 | '<?xml version="1.0" ?><manifest>' |
145 | '<remote name="default-remote" fetch="http://localhost"/>' + | 213 | '<remote fetch="http://localhost" name="default-remote"/>' |
146 | '<default remote="default-remote" revision="refs/heads/main"/>' + | 214 | '<default remote="default-remote" revision="refs/heads/main"/>' |
147 | '<project name="platform/art" path="art" revision="ABCDEF" ' + | 215 | '<project groups="notdefault,platform-' + self.platform + '" ' |
148 | 'groups="notdefault,platform-' + self.platform + '"/>' + | 216 | 'name="platform/art" path="art" revision="ABCDEF" upstream="refs/heads/main"/>' |
149 | '<superproject name="superproject"/>' + | 217 | '<superproject name="superproject"/>' |
150 | '</manifest>') | 218 | '</manifest>') |
151 | 219 | ||
152 | def test_superproject_update_project_revision_id(self): | 220 | def test_superproject_update_project_revision_id(self): |
@@ -162,19 +230,145 @@ class SuperprojectTestCase(unittest.TestCase): | |||
162 | return_value=data): | 230 | return_value=data): |
163 | # Create temporary directory so that it can write the file. | 231 | # Create temporary directory so that it can write the file. |
164 | os.mkdir(self._superproject._superproject_path) | 232 | os.mkdir(self._superproject._superproject_path) |
165 | manifest_path = self._superproject.UpdateProjectsRevisionId(projects) | 233 | update_result = self._superproject.UpdateProjectsRevisionId(projects) |
166 | self.assertIsNotNone(manifest_path) | 234 | self.assertIsNotNone(update_result.manifest_path) |
167 | with open(manifest_path, 'r') as fp: | 235 | self.assertFalse(update_result.fatal) |
168 | manifest_xml = fp.read() | 236 | with open(update_result.manifest_path, 'r') as fp: |
237 | manifest_xml_data = fp.read() | ||
238 | self.assertEqual( | ||
239 | sort_attributes(manifest_xml_data), | ||
240 | '<?xml version="1.0" ?><manifest>' | ||
241 | '<remote fetch="http://localhost" name="default-remote"/>' | ||
242 | '<default remote="default-remote" revision="refs/heads/main"/>' | ||
243 | '<project groups="notdefault,platform-' + self.platform + '" ' | ||
244 | 'name="platform/art" path="art" ' | ||
245 | 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>' | ||
246 | '<superproject name="superproject"/>' | ||
247 | '</manifest>') | ||
248 | |||
249 | def test_superproject_update_project_revision_id_no_superproject_tag(self): | ||
250 | """Test update of commit ids of a manifest without superproject tag.""" | ||
251 | manifest = self.getXmlManifest(""" | ||
252 | <manifest> | ||
253 | <remote name="default-remote" fetch="http://localhost" /> | ||
254 | <default remote="default-remote" revision="refs/heads/main" /> | ||
255 | <project name="test-name"/> | ||
256 | </manifest> | ||
257 | """) | ||
258 | self.maxDiff = None | ||
259 | self._superproject = git_superproject.Superproject(manifest, self.repodir, | ||
260 | self.git_event_log) | ||
261 | self.assertEqual(len(self._superproject._manifest.projects), 1) | ||
262 | projects = self._superproject._manifest.projects | ||
263 | project = projects[0] | ||
264 | project.SetRevisionId('ABCDEF') | ||
265 | update_result = self._superproject.UpdateProjectsRevisionId(projects) | ||
266 | self.assertIsNone(update_result.manifest_path) | ||
267 | self.assertFalse(update_result.fatal) | ||
268 | self.verifyErrorEvent() | ||
269 | self.assertEqual( | ||
270 | sort_attributes(manifest.ToXml().toxml()), | ||
271 | '<?xml version="1.0" ?><manifest>' | ||
272 | '<remote fetch="http://localhost" name="default-remote"/>' | ||
273 | '<default remote="default-remote" revision="refs/heads/main"/>' | ||
274 | '<project name="test-name" revision="ABCDEF" upstream="refs/heads/main"/>' | ||
275 | '</manifest>') | ||
276 | |||
277 | def test_superproject_update_project_revision_id_from_local_manifest_group(self): | ||
278 | """Test update of commit ids of a manifest that have local manifest no superproject group.""" | ||
279 | local_group = manifest_xml.LOCAL_MANIFEST_GROUP_PREFIX + ':local' | ||
280 | manifest = self.getXmlManifest(""" | ||
281 | <manifest> | ||
282 | <remote name="default-remote" fetch="http://localhost" /> | ||
283 | <remote name="goog" fetch="http://localhost2" /> | ||
284 | <default remote="default-remote" revision="refs/heads/main" /> | ||
285 | <superproject name="superproject"/> | ||
286 | <project path="vendor/x" name="platform/vendor/x" remote="goog" | ||
287 | groups=\"""" + local_group + """ | ||
288 | " revision="master-with-vendor" clone-depth="1" /> | ||
289 | <project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """ | ||
290 | " /></manifest> | ||
291 | """) | ||
292 | self.maxDiff = None | ||
293 | self._superproject = git_superproject.Superproject(manifest, self.repodir, | ||
294 | self.git_event_log) | ||
295 | self.assertEqual(len(self._superproject._manifest.projects), 2) | ||
296 | projects = self._superproject._manifest.projects | ||
297 | data = ('160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00') | ||
298 | with mock.patch.object(self._superproject, '_Init', return_value=True): | ||
299 | with mock.patch.object(self._superproject, '_Fetch', return_value=True): | ||
300 | with mock.patch.object(self._superproject, | ||
301 | '_LsTree', | ||
302 | return_value=data): | ||
303 | # Create temporary directory so that it can write the file. | ||
304 | os.mkdir(self._superproject._superproject_path) | ||
305 | update_result = self._superproject.UpdateProjectsRevisionId(projects) | ||
306 | self.assertIsNotNone(update_result.manifest_path) | ||
307 | self.assertFalse(update_result.fatal) | ||
308 | with open(update_result.manifest_path, 'r') as fp: | ||
309 | manifest_xml_data = fp.read() | ||
310 | # Verify platform/vendor/x's project revision hasn't changed. | ||
311 | self.assertEqual( | ||
312 | sort_attributes(manifest_xml_data), | ||
313 | '<?xml version="1.0" ?><manifest>' | ||
314 | '<remote fetch="http://localhost" name="default-remote"/>' | ||
315 | '<remote fetch="http://localhost2" name="goog"/>' | ||
316 | '<default remote="default-remote" revision="refs/heads/main"/>' | ||
317 | '<project groups="notdefault,platform-' + self.platform + '" ' | ||
318 | 'name="platform/art" path="art" ' | ||
319 | 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>' | ||
320 | '<project clone-depth="1" groups="' + local_group + '" ' | ||
321 | 'name="platform/vendor/x" path="vendor/x" remote="goog" ' | ||
322 | 'revision="master-with-vendor"/>' | ||
323 | '<superproject name="superproject"/>' | ||
324 | '</manifest>') | ||
325 | |||
326 | def test_superproject_update_project_revision_id_with_pinned_manifest(self): | ||
327 | """Test update of commit ids of a pinned manifest.""" | ||
328 | manifest = self.getXmlManifest(""" | ||
329 | <manifest> | ||
330 | <remote name="default-remote" fetch="http://localhost" /> | ||
331 | <default remote="default-remote" revision="refs/heads/main" /> | ||
332 | <superproject name="superproject"/> | ||
333 | <project path="vendor/x" name="platform/vendor/x" revision="" /> | ||
334 | <project path="vendor/y" name="platform/vendor/y" | ||
335 | revision="52d3c9f7c107839ece2319d077de0cd922aa9d8f" /> | ||
336 | <project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """ | ||
337 | " /></manifest> | ||
338 | """) | ||
339 | self.maxDiff = None | ||
340 | self._superproject = git_superproject.Superproject(manifest, self.repodir, | ||
341 | self.git_event_log) | ||
342 | self.assertEqual(len(self._superproject._manifest.projects), 3) | ||
343 | projects = self._superproject._manifest.projects | ||
344 | data = ('160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00' | ||
345 | '160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tvendor/x\x00') | ||
346 | with mock.patch.object(self._superproject, '_Init', return_value=True): | ||
347 | with mock.patch.object(self._superproject, '_Fetch', return_value=True): | ||
348 | with mock.patch.object(self._superproject, | ||
349 | '_LsTree', | ||
350 | return_value=data): | ||
351 | # Create temporary directory so that it can write the file. | ||
352 | os.mkdir(self._superproject._superproject_path) | ||
353 | update_result = self._superproject.UpdateProjectsRevisionId(projects) | ||
354 | self.assertIsNotNone(update_result.manifest_path) | ||
355 | self.assertFalse(update_result.fatal) | ||
356 | with open(update_result.manifest_path, 'r') as fp: | ||
357 | manifest_xml_data = fp.read() | ||
358 | # Verify platform/vendor/x's project revision hasn't changed. | ||
169 | self.assertEqual( | 359 | self.assertEqual( |
170 | manifest_xml, | 360 | sort_attributes(manifest_xml_data), |
171 | '<?xml version="1.0" ?><manifest>' + | 361 | '<?xml version="1.0" ?><manifest>' |
172 | '<remote name="default-remote" fetch="http://localhost"/>' + | 362 | '<remote fetch="http://localhost" name="default-remote"/>' |
173 | '<default remote="default-remote" revision="refs/heads/main"/>' + | 363 | '<default remote="default-remote" revision="refs/heads/main"/>' |
174 | '<project name="platform/art" path="art" ' + | 364 | '<project groups="notdefault,platform-' + self.platform + '" ' |
175 | 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" ' + | 365 | 'name="platform/art" path="art" ' |
176 | 'groups="notdefault,platform-' + self.platform + '"/>' + | 366 | 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>' |
177 | '<superproject name="superproject"/>' + | 367 | '<project name="platform/vendor/x" path="vendor/x" ' |
368 | 'revision="e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06" upstream="refs/heads/main"/>' | ||
369 | '<project name="platform/vendor/y" path="vendor/y" ' | ||
370 | 'revision="52d3c9f7c107839ece2319d077de0cd922aa9d8f"/>' | ||
371 | '<superproject name="superproject"/>' | ||
178 | '</manifest>') | 372 | '</manifest>') |
179 | 373 | ||
180 | 374 | ||
diff --git a/tests/test_git_trace2_event_log.py b/tests/test_git_trace2_event_log.py index 4a3a4c48..89dcfb92 100644 --- a/tests/test_git_trace2_event_log.py +++ b/tests/test_git_trace2_event_log.py | |||
@@ -42,7 +42,7 @@ class EventLogTestCase(unittest.TestCase): | |||
42 | self._event_log_module = git_trace2_event_log.EventLog(env=env) | 42 | self._event_log_module = git_trace2_event_log.EventLog(env=env) |
43 | self._log_data = None | 43 | self._log_data = None |
44 | 44 | ||
45 | def verifyCommonKeys(self, log_entry, expected_event_name, full_sid=True): | 45 | def verifyCommonKeys(self, log_entry, expected_event_name=None, full_sid=True): |
46 | """Helper function to verify common event log keys.""" | 46 | """Helper function to verify common event log keys.""" |
47 | self.assertIn('event', log_entry) | 47 | self.assertIn('event', log_entry) |
48 | self.assertIn('sid', log_entry) | 48 | self.assertIn('sid', log_entry) |
@@ -50,7 +50,8 @@ class EventLogTestCase(unittest.TestCase): | |||
50 | self.assertIn('time', log_entry) | 50 | self.assertIn('time', log_entry) |
51 | 51 | ||
52 | # Do basic data format validation. | 52 | # Do basic data format validation. |
53 | self.assertEqual(expected_event_name, log_entry['event']) | 53 | if expected_event_name: |
54 | self.assertEqual(expected_event_name, log_entry['event']) | ||
54 | if full_sid: | 55 | if full_sid: |
55 | self.assertRegex(log_entry['sid'], self.FULL_SID_REGEX) | 56 | self.assertRegex(log_entry['sid'], self.FULL_SID_REGEX) |
56 | else: | 57 | else: |
@@ -65,6 +66,13 @@ class EventLogTestCase(unittest.TestCase): | |||
65 | log_data.append(json.loads(line)) | 66 | log_data.append(json.loads(line)) |
66 | return log_data | 67 | return log_data |
67 | 68 | ||
69 | def remove_prefix(self, s, prefix): | ||
70 | """Return a copy string after removing |prefix| from |s|, if present or the original string.""" | ||
71 | if s.startswith(prefix): | ||
72 | return s[len(prefix):] | ||
73 | else: | ||
74 | return s | ||
75 | |||
68 | def test_initial_state_with_parent_sid(self): | 76 | def test_initial_state_with_parent_sid(self): |
69 | """Test initial state when 'GIT_TRACE2_PARENT_SID' is set by parent.""" | 77 | """Test initial state when 'GIT_TRACE2_PARENT_SID' is set by parent.""" |
70 | self.assertRegex(self._event_log_module.full_sid, self.FULL_SID_REGEX) | 78 | self.assertRegex(self._event_log_module.full_sid, self.FULL_SID_REGEX) |
@@ -234,6 +242,66 @@ class EventLogTestCase(unittest.TestCase): | |||
234 | self.assertEqual(len(self._log_data), 1) | 242 | self.assertEqual(len(self._log_data), 1) |
235 | self.verifyCommonKeys(self._log_data[0], expected_event_name='version') | 243 | self.verifyCommonKeys(self._log_data[0], expected_event_name='version') |
236 | 244 | ||
245 | def test_data_event_config(self): | ||
246 | """Test 'data' event data outputs all config keys. | ||
247 | |||
248 | Expected event log: | ||
249 | <version event> | ||
250 | <data event> | ||
251 | <data event> | ||
252 | """ | ||
253 | config = { | ||
254 | 'git.foo': 'bar', | ||
255 | 'repo.partialclone': 'false', | ||
256 | 'repo.syncstate.superproject.hassuperprojecttag': 'true', | ||
257 | 'repo.syncstate.superproject.sys.argv': ['--', 'sync', 'protobuf'], | ||
258 | } | ||
259 | prefix_value = 'prefix' | ||
260 | self._event_log_module.LogDataConfigEvents(config, prefix_value) | ||
261 | |||
262 | with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir: | ||
263 | log_path = self._event_log_module.Write(path=tempdir) | ||
264 | self._log_data = self.readLog(log_path) | ||
265 | |||
266 | self.assertEqual(len(self._log_data), 5) | ||
267 | data_events = self._log_data[1:] | ||
268 | self.verifyCommonKeys(self._log_data[0], expected_event_name='version') | ||
269 | |||
270 | for event in data_events: | ||
271 | self.verifyCommonKeys(event) | ||
272 | # Check for 'data' event specific fields. | ||
273 | self.assertIn('key', event) | ||
274 | self.assertIn('value', event) | ||
275 | key = event['key'] | ||
276 | key = self.remove_prefix(key, f'{prefix_value}/') | ||
277 | value = event['value'] | ||
278 | self.assertEqual(self._event_log_module.GetDataEventName(value), event['event']) | ||
279 | self.assertTrue(key in config and value == config[key]) | ||
280 | |||
281 | def test_error_event(self): | ||
282 | """Test and validate 'error' event data is valid. | ||
283 | |||
284 | Expected event log: | ||
285 | <version event> | ||
286 | <error event> | ||
287 | """ | ||
288 | msg = 'invalid option: --cahced' | ||
289 | fmt = 'invalid option: %s' | ||
290 | self._event_log_module.ErrorEvent(msg, fmt) | ||
291 | with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir: | ||
292 | log_path = self._event_log_module.Write(path=tempdir) | ||
293 | self._log_data = self.readLog(log_path) | ||
294 | |||
295 | self.assertEqual(len(self._log_data), 2) | ||
296 | error_event = self._log_data[1] | ||
297 | self.verifyCommonKeys(self._log_data[0], expected_event_name='version') | ||
298 | self.verifyCommonKeys(error_event, expected_event_name='error') | ||
299 | # Check for 'error' event specific fields. | ||
300 | self.assertIn('msg', error_event) | ||
301 | self.assertIn('fmt', error_event) | ||
302 | self.assertEqual(error_event['msg'], msg) | ||
303 | self.assertEqual(error_event['fmt'], fmt) | ||
304 | |||
237 | def test_write_with_filename(self): | 305 | def test_write_with_filename(self): |
238 | """Test Write() with a path to a file exits with None.""" | 306 | """Test Write() with a path to a file exits with None.""" |
239 | self.assertIsNone(self._event_log_module.Write(path='path/to/file')) | 307 | self.assertIsNone(self._event_log_module.Write(path='path/to/file')) |
diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py index eda06968..cb3eb855 100644 --- a/tests/test_manifest_xml.py +++ b/tests/test_manifest_xml.py | |||
@@ -16,6 +16,7 @@ | |||
16 | 16 | ||
17 | import os | 17 | import os |
18 | import platform | 18 | import platform |
19 | import re | ||
19 | import shutil | 20 | import shutil |
20 | import tempfile | 21 | import tempfile |
21 | import unittest | 22 | import unittest |
@@ -52,6 +53,9 @@ INVALID_FS_PATHS = ( | |||
52 | 'blah/foo~', | 53 | 'blah/foo~', |
53 | # Block Unicode characters that get normalized out by filesystems. | 54 | # Block Unicode characters that get normalized out by filesystems. |
54 | u'foo\u200Cbar', | 55 | u'foo\u200Cbar', |
56 | # Block newlines. | ||
57 | 'f\n/bar', | ||
58 | 'f\r/bar', | ||
55 | ) | 59 | ) |
56 | 60 | ||
57 | # Make sure platforms that use path separators (e.g. Windows) are also | 61 | # Make sure platforms that use path separators (e.g. Windows) are also |
@@ -60,6 +64,30 @@ if os.path.sep != '/': | |||
60 | INVALID_FS_PATHS += tuple(x.replace('/', os.path.sep) for x in INVALID_FS_PATHS) | 64 | INVALID_FS_PATHS += tuple(x.replace('/', os.path.sep) for x in INVALID_FS_PATHS) |
61 | 65 | ||
62 | 66 | ||
67 | def sort_attributes(manifest): | ||
68 | """Sort the attributes of all elements alphabetically. | ||
69 | |||
70 | This is needed because different versions of the toxml() function from | ||
71 | xml.dom.minidom outputs the attributes of elements in different orders. | ||
72 | Before Python 3.8 they were output alphabetically, later versions preserve | ||
73 | the order specified by the user. | ||
74 | |||
75 | Args: | ||
76 | manifest: String containing an XML manifest. | ||
77 | |||
78 | Returns: | ||
79 | The XML manifest with the attributes of all elements sorted alphabetically. | ||
80 | """ | ||
81 | new_manifest = '' | ||
82 | # This will find every element in the XML manifest, whether they have | ||
83 | # attributes or not. This simplifies recreating the manifest below. | ||
84 | matches = re.findall(r'(<[/?]?[a-z-]+\s*)((?:\S+?="[^"]+"\s*?)*)(\s*[/?]?>)', manifest) | ||
85 | for head, attrs, tail in matches: | ||
86 | m = re.findall(r'\S+?="[^"]+"', attrs) | ||
87 | new_manifest += head + ' '.join(sorted(m)) + tail | ||
88 | return new_manifest | ||
89 | |||
90 | |||
63 | class ManifestParseTestCase(unittest.TestCase): | 91 | class ManifestParseTestCase(unittest.TestCase): |
64 | """TestCase for parsing manifests.""" | 92 | """TestCase for parsing manifests.""" |
65 | 93 | ||
@@ -91,6 +119,11 @@ class ManifestParseTestCase(unittest.TestCase): | |||
91 | fp.write(data) | 119 | fp.write(data) |
92 | return manifest_xml.XmlManifest(self.repodir, self.manifest_file) | 120 | return manifest_xml.XmlManifest(self.repodir, self.manifest_file) |
93 | 121 | ||
122 | @staticmethod | ||
123 | def encodeXmlAttr(attr): | ||
124 | """Encode |attr| using XML escape rules.""" | ||
125 | return attr.replace('\r', '
').replace('\n', '
') | ||
126 | |||
94 | 127 | ||
95 | class ManifestValidateFilePaths(unittest.TestCase): | 128 | class ManifestValidateFilePaths(unittest.TestCase): |
96 | """Check _ValidateFilePaths helper. | 129 | """Check _ValidateFilePaths helper. |
@@ -232,6 +265,19 @@ class XmlManifestTests(ManifestParseTestCase): | |||
232 | self.assertEqual(manifest.repo_hooks_project.name, 'repohooks') | 265 | self.assertEqual(manifest.repo_hooks_project.name, 'repohooks') |
233 | self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b']) | 266 | self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b']) |
234 | 267 | ||
268 | def test_repo_hooks_unordered(self): | ||
269 | """Check repo-hooks settings work even if the project def comes second.""" | ||
270 | manifest = self.getXmlManifest(""" | ||
271 | <manifest> | ||
272 | <remote name="test-remote" fetch="http://localhost" /> | ||
273 | <default remote="test-remote" revision="refs/heads/main" /> | ||
274 | <repo-hooks in-project="repohooks" enabled-list="a, b"/> | ||
275 | <project name="repohooks" path="src/repohooks"/> | ||
276 | </manifest> | ||
277 | """) | ||
278 | self.assertEqual(manifest.repo_hooks_project.name, 'repohooks') | ||
279 | self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b']) | ||
280 | |||
235 | def test_unknown_tags(self): | 281 | def test_unknown_tags(self): |
236 | """Check superproject settings.""" | 282 | """Check superproject settings.""" |
237 | manifest = self.getXmlManifest(""" | 283 | manifest = self.getXmlManifest(""" |
@@ -246,11 +292,30 @@ class XmlManifestTests(ManifestParseTestCase): | |||
246 | self.assertEqual(manifest.superproject['name'], 'superproject') | 292 | self.assertEqual(manifest.superproject['name'], 'superproject') |
247 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') | 293 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') |
248 | self.assertEqual( | 294 | self.assertEqual( |
249 | manifest.ToXml().toxml(), | 295 | sort_attributes(manifest.ToXml().toxml()), |
250 | '<?xml version="1.0" ?><manifest>' + | 296 | '<?xml version="1.0" ?><manifest>' |
251 | '<remote name="test-remote" fetch="http://localhost"/>' + | 297 | '<remote fetch="http://localhost" name="test-remote"/>' |
252 | '<default remote="test-remote" revision="refs/heads/main"/>' + | 298 | '<default remote="test-remote" revision="refs/heads/main"/>' |
253 | '<superproject name="superproject"/>' + | 299 | '<superproject name="superproject"/>' |
300 | '</manifest>') | ||
301 | |||
302 | def test_remote_annotations(self): | ||
303 | """Check remote settings.""" | ||
304 | manifest = self.getXmlManifest(""" | ||
305 | <manifest> | ||
306 | <remote name="test-remote" fetch="http://localhost"> | ||
307 | <annotation name="foo" value="bar"/> | ||
308 | </remote> | ||
309 | </manifest> | ||
310 | """) | ||
311 | self.assertEqual(manifest.remotes['test-remote'].annotations[0].name, 'foo') | ||
312 | self.assertEqual(manifest.remotes['test-remote'].annotations[0].value, 'bar') | ||
313 | self.assertEqual( | ||
314 | sort_attributes(manifest.ToXml().toxml()), | ||
315 | '<?xml version="1.0" ?><manifest>' | ||
316 | '<remote fetch="http://localhost" name="test-remote">' | ||
317 | '<annotation name="foo" value="bar"/>' | ||
318 | '</remote>' | ||
254 | '</manifest>') | 319 | '</manifest>') |
255 | 320 | ||
256 | 321 | ||
@@ -303,6 +368,7 @@ class IncludeElementTests(ManifestParseTestCase): | |||
303 | def test_allow_bad_name_from_user(self): | 368 | def test_allow_bad_name_from_user(self): |
304 | """Check handling of bad name attribute from the user's input.""" | 369 | """Check handling of bad name attribute from the user's input.""" |
305 | def parse(name): | 370 | def parse(name): |
371 | name = self.encodeXmlAttr(name) | ||
306 | manifest = self.getXmlManifest(f""" | 372 | manifest = self.getXmlManifest(f""" |
307 | <manifest> | 373 | <manifest> |
308 | <remote name="default-remote" fetch="http://localhost" /> | 374 | <remote name="default-remote" fetch="http://localhost" /> |
@@ -327,6 +393,7 @@ class IncludeElementTests(ManifestParseTestCase): | |||
327 | def test_bad_name_checks(self): | 393 | def test_bad_name_checks(self): |
328 | """Check handling of bad name attribute.""" | 394 | """Check handling of bad name attribute.""" |
329 | def parse(name): | 395 | def parse(name): |
396 | name = self.encodeXmlAttr(name) | ||
330 | # Setup target of the include. | 397 | # Setup target of the include. |
331 | with open(os.path.join(self.manifest_dir, 'target.xml'), 'w') as fp: | 398 | with open(os.path.join(self.manifest_dir, 'target.xml'), 'w') as fp: |
332 | fp.write(f'<manifest><include name="{name}"/></manifest>') | 399 | fp.write(f'<manifest><include name="{name}"/></manifest>') |
@@ -398,16 +465,18 @@ class ProjectElementTests(ManifestParseTestCase): | |||
398 | project = manifest.projects[0] | 465 | project = manifest.projects[0] |
399 | project.SetRevisionId('ABCDEF') | 466 | project.SetRevisionId('ABCDEF') |
400 | self.assertEqual( | 467 | self.assertEqual( |
401 | manifest.ToXml().toxml(), | 468 | sort_attributes(manifest.ToXml().toxml()), |
402 | '<?xml version="1.0" ?><manifest>' + | 469 | '<?xml version="1.0" ?><manifest>' |
403 | '<remote name="default-remote" fetch="http://localhost"/>' + | 470 | '<remote fetch="http://localhost" name="default-remote"/>' |
404 | '<default remote="default-remote" revision="refs/heads/main"/>' + | 471 | '<default remote="default-remote" revision="refs/heads/main"/>' |
405 | '<project name="test-name" revision="ABCDEF"/>' + | 472 | '<project name="test-name" revision="ABCDEF" upstream="refs/heads/main"/>' |
406 | '</manifest>') | 473 | '</manifest>') |
407 | 474 | ||
408 | def test_trailing_slash(self): | 475 | def test_trailing_slash(self): |
409 | """Check handling of trailing slashes in attributes.""" | 476 | """Check handling of trailing slashes in attributes.""" |
410 | def parse(name, path): | 477 | def parse(name, path): |
478 | name = self.encodeXmlAttr(name) | ||
479 | path = self.encodeXmlAttr(path) | ||
411 | return self.getXmlManifest(f""" | 480 | return self.getXmlManifest(f""" |
412 | <manifest> | 481 | <manifest> |
413 | <remote name="default-remote" fetch="http://localhost" /> | 482 | <remote name="default-remote" fetch="http://localhost" /> |
@@ -437,6 +506,8 @@ class ProjectElementTests(ManifestParseTestCase): | |||
437 | def test_toplevel_path(self): | 506 | def test_toplevel_path(self): |
438 | """Check handling of path=. specially.""" | 507 | """Check handling of path=. specially.""" |
439 | def parse(name, path): | 508 | def parse(name, path): |
509 | name = self.encodeXmlAttr(name) | ||
510 | path = self.encodeXmlAttr(path) | ||
440 | return self.getXmlManifest(f""" | 511 | return self.getXmlManifest(f""" |
441 | <manifest> | 512 | <manifest> |
442 | <remote name="default-remote" fetch="http://localhost" /> | 513 | <remote name="default-remote" fetch="http://localhost" /> |
@@ -453,6 +524,8 @@ class ProjectElementTests(ManifestParseTestCase): | |||
453 | def test_bad_path_name_checks(self): | 524 | def test_bad_path_name_checks(self): |
454 | """Check handling of bad path & name attributes.""" | 525 | """Check handling of bad path & name attributes.""" |
455 | def parse(name, path): | 526 | def parse(name, path): |
527 | name = self.encodeXmlAttr(name) | ||
528 | path = self.encodeXmlAttr(path) | ||
456 | manifest = self.getXmlManifest(f""" | 529 | manifest = self.getXmlManifest(f""" |
457 | <manifest> | 530 | <manifest> |
458 | <remote name="default-remote" fetch="http://localhost" /> | 531 | <remote name="default-remote" fetch="http://localhost" /> |
@@ -499,12 +572,79 @@ class SuperProjectElementTests(ManifestParseTestCase): | |||
499 | self.assertEqual(manifest.superproject['name'], 'superproject') | 572 | self.assertEqual(manifest.superproject['name'], 'superproject') |
500 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') | 573 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') |
501 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject') | 574 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject') |
575 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/main') | ||
502 | self.assertEqual( | 576 | self.assertEqual( |
503 | manifest.ToXml().toxml(), | 577 | sort_attributes(manifest.ToXml().toxml()), |
504 | '<?xml version="1.0" ?><manifest>' + | 578 | '<?xml version="1.0" ?><manifest>' |
505 | '<remote name="test-remote" fetch="http://localhost"/>' + | 579 | '<remote fetch="http://localhost" name="test-remote"/>' |
506 | '<default remote="test-remote" revision="refs/heads/main"/>' + | 580 | '<default remote="test-remote" revision="refs/heads/main"/>' |
507 | '<superproject name="superproject"/>' + | 581 | '<superproject name="superproject"/>' |
582 | '</manifest>') | ||
583 | |||
584 | def test_superproject_revision(self): | ||
585 | """Check superproject settings with a different revision attribute""" | ||
586 | self.maxDiff = None | ||
587 | manifest = self.getXmlManifest(""" | ||
588 | <manifest> | ||
589 | <remote name="test-remote" fetch="http://localhost" /> | ||
590 | <default remote="test-remote" revision="refs/heads/main" /> | ||
591 | <superproject name="superproject" revision="refs/heads/stable" /> | ||
592 | </manifest> | ||
593 | """) | ||
594 | self.assertEqual(manifest.superproject['name'], 'superproject') | ||
595 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') | ||
596 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject') | ||
597 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable') | ||
598 | self.assertEqual( | ||
599 | sort_attributes(manifest.ToXml().toxml()), | ||
600 | '<?xml version="1.0" ?><manifest>' | ||
601 | '<remote fetch="http://localhost" name="test-remote"/>' | ||
602 | '<default remote="test-remote" revision="refs/heads/main"/>' | ||
603 | '<superproject name="superproject" revision="refs/heads/stable"/>' | ||
604 | '</manifest>') | ||
605 | |||
606 | def test_superproject_revision_default_negative(self): | ||
607 | """Check superproject settings with a same revision attribute""" | ||
608 | self.maxDiff = None | ||
609 | manifest = self.getXmlManifest(""" | ||
610 | <manifest> | ||
611 | <remote name="test-remote" fetch="http://localhost" /> | ||
612 | <default remote="test-remote" revision="refs/heads/stable" /> | ||
613 | <superproject name="superproject" revision="refs/heads/stable" /> | ||
614 | </manifest> | ||
615 | """) | ||
616 | self.assertEqual(manifest.superproject['name'], 'superproject') | ||
617 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') | ||
618 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject') | ||
619 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable') | ||
620 | self.assertEqual( | ||
621 | sort_attributes(manifest.ToXml().toxml()), | ||
622 | '<?xml version="1.0" ?><manifest>' | ||
623 | '<remote fetch="http://localhost" name="test-remote"/>' | ||
624 | '<default remote="test-remote" revision="refs/heads/stable"/>' | ||
625 | '<superproject name="superproject"/>' | ||
626 | '</manifest>') | ||
627 | |||
628 | def test_superproject_revision_remote(self): | ||
629 | """Check superproject settings with a same revision attribute""" | ||
630 | self.maxDiff = None | ||
631 | manifest = self.getXmlManifest(""" | ||
632 | <manifest> | ||
633 | <remote name="test-remote" fetch="http://localhost" revision="refs/heads/main" /> | ||
634 | <default remote="test-remote" /> | ||
635 | <superproject name="superproject" revision="refs/heads/stable" /> | ||
636 | </manifest> | ||
637 | """) | ||
638 | self.assertEqual(manifest.superproject['name'], 'superproject') | ||
639 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') | ||
640 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject') | ||
641 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable') | ||
642 | self.assertEqual( | ||
643 | sort_attributes(manifest.ToXml().toxml()), | ||
644 | '<?xml version="1.0" ?><manifest>' | ||
645 | '<remote fetch="http://localhost" name="test-remote" revision="refs/heads/main"/>' | ||
646 | '<default remote="test-remote"/>' | ||
647 | '<superproject name="superproject" revision="refs/heads/stable"/>' | ||
508 | '</manifest>') | 648 | '</manifest>') |
509 | 649 | ||
510 | def test_remote(self): | 650 | def test_remote(self): |
@@ -520,13 +660,14 @@ class SuperProjectElementTests(ManifestParseTestCase): | |||
520 | self.assertEqual(manifest.superproject['name'], 'platform/superproject') | 660 | self.assertEqual(manifest.superproject['name'], 'platform/superproject') |
521 | self.assertEqual(manifest.superproject['remote'].name, 'superproject-remote') | 661 | self.assertEqual(manifest.superproject['remote'].name, 'superproject-remote') |
522 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/platform/superproject') | 662 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/platform/superproject') |
663 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/main') | ||
523 | self.assertEqual( | 664 | self.assertEqual( |
524 | manifest.ToXml().toxml(), | 665 | sort_attributes(manifest.ToXml().toxml()), |
525 | '<?xml version="1.0" ?><manifest>' + | 666 | '<?xml version="1.0" ?><manifest>' |
526 | '<remote name="default-remote" fetch="http://localhost"/>' + | 667 | '<remote fetch="http://localhost" name="default-remote"/>' |
527 | '<remote name="superproject-remote" fetch="http://localhost"/>' + | 668 | '<remote fetch="http://localhost" name="superproject-remote"/>' |
528 | '<default remote="default-remote" revision="refs/heads/main"/>' + | 669 | '<default remote="default-remote" revision="refs/heads/main"/>' |
529 | '<superproject name="platform/superproject" remote="superproject-remote"/>' + | 670 | '<superproject name="platform/superproject" remote="superproject-remote"/>' |
530 | '</manifest>') | 671 | '</manifest>') |
531 | 672 | ||
532 | def test_defalut_remote(self): | 673 | def test_defalut_remote(self): |
@@ -540,10 +681,165 @@ class SuperProjectElementTests(ManifestParseTestCase): | |||
540 | """) | 681 | """) |
541 | self.assertEqual(manifest.superproject['name'], 'superproject') | 682 | self.assertEqual(manifest.superproject['name'], 'superproject') |
542 | self.assertEqual(manifest.superproject['remote'].name, 'default-remote') | 683 | self.assertEqual(manifest.superproject['remote'].name, 'default-remote') |
684 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/main') | ||
685 | self.assertEqual( | ||
686 | sort_attributes(manifest.ToXml().toxml()), | ||
687 | '<?xml version="1.0" ?><manifest>' | ||
688 | '<remote fetch="http://localhost" name="default-remote"/>' | ||
689 | '<default remote="default-remote" revision="refs/heads/main"/>' | ||
690 | '<superproject name="superproject"/>' | ||
691 | '</manifest>') | ||
692 | |||
693 | |||
694 | class ContactinfoElementTests(ManifestParseTestCase): | ||
695 | """Tests for <contactinfo>.""" | ||
696 | |||
697 | def test_contactinfo(self): | ||
698 | """Check contactinfo settings.""" | ||
699 | bugurl = 'http://localhost/contactinfo' | ||
700 | manifest = self.getXmlManifest(f""" | ||
701 | <manifest> | ||
702 | <contactinfo bugurl="{bugurl}"/> | ||
703 | </manifest> | ||
704 | """) | ||
705 | self.assertEqual(manifest.contactinfo.bugurl, bugurl) | ||
543 | self.assertEqual( | 706 | self.assertEqual( |
544 | manifest.ToXml().toxml(), | 707 | manifest.ToXml().toxml(), |
545 | '<?xml version="1.0" ?><manifest>' + | 708 | '<?xml version="1.0" ?><manifest>' |
546 | '<remote name="default-remote" fetch="http://localhost"/>' + | 709 | f'<contactinfo bugurl="{bugurl}"/>' |
547 | '<default remote="default-remote" revision="refs/heads/main"/>' + | ||
548 | '<superproject name="superproject"/>' + | ||
549 | '</manifest>') | 710 | '</manifest>') |
711 | |||
712 | |||
713 | class DefaultElementTests(ManifestParseTestCase): | ||
714 | """Tests for <default>.""" | ||
715 | |||
716 | def test_default(self): | ||
717 | """Check default settings.""" | ||
718 | a = manifest_xml._Default() | ||
719 | a.revisionExpr = 'foo' | ||
720 | a.remote = manifest_xml._XmlRemote(name='remote') | ||
721 | b = manifest_xml._Default() | ||
722 | b.revisionExpr = 'bar' | ||
723 | self.assertEqual(a, a) | ||
724 | self.assertNotEqual(a, b) | ||
725 | self.assertNotEqual(b, a.remote) | ||
726 | self.assertNotEqual(a, 123) | ||
727 | self.assertNotEqual(a, None) | ||
728 | |||
729 | |||
730 | class RemoteElementTests(ManifestParseTestCase): | ||
731 | """Tests for <remote>.""" | ||
732 | |||
733 | def test_remote(self): | ||
734 | """Check remote settings.""" | ||
735 | a = manifest_xml._XmlRemote(name='foo') | ||
736 | a.AddAnnotation('key1', 'value1', 'true') | ||
737 | b = manifest_xml._XmlRemote(name='foo') | ||
738 | b.AddAnnotation('key2', 'value1', 'true') | ||
739 | c = manifest_xml._XmlRemote(name='foo') | ||
740 | c.AddAnnotation('key1', 'value2', 'true') | ||
741 | d = manifest_xml._XmlRemote(name='foo') | ||
742 | d.AddAnnotation('key1', 'value1', 'false') | ||
743 | self.assertEqual(a, a) | ||
744 | self.assertNotEqual(a, b) | ||
745 | self.assertNotEqual(a, c) | ||
746 | self.assertNotEqual(a, d) | ||
747 | self.assertNotEqual(a, manifest_xml._Default()) | ||
748 | self.assertNotEqual(a, 123) | ||
749 | self.assertNotEqual(a, None) | ||
750 | |||
751 | |||
752 | class RemoveProjectElementTests(ManifestParseTestCase): | ||
753 | """Tests for <remove-project>.""" | ||
754 | |||
755 | def test_remove_one_project(self): | ||
756 | manifest = self.getXmlManifest(""" | ||
757 | <manifest> | ||
758 | <remote name="default-remote" fetch="http://localhost" /> | ||
759 | <default remote="default-remote" revision="refs/heads/main" /> | ||
760 | <project name="myproject" /> | ||
761 | <remove-project name="myproject" /> | ||
762 | </manifest> | ||
763 | """) | ||
764 | self.assertEqual(manifest.projects, []) | ||
765 | |||
766 | def test_remove_one_project_one_remains(self): | ||
767 | manifest = self.getXmlManifest(""" | ||
768 | <manifest> | ||
769 | <remote name="default-remote" fetch="http://localhost" /> | ||
770 | <default remote="default-remote" revision="refs/heads/main" /> | ||
771 | <project name="myproject" /> | ||
772 | <project name="yourproject" /> | ||
773 | <remove-project name="myproject" /> | ||
774 | </manifest> | ||
775 | """) | ||
776 | |||
777 | self.assertEqual(len(manifest.projects), 1) | ||
778 | self.assertEqual(manifest.projects[0].name, 'yourproject') | ||
779 | |||
780 | def test_remove_one_project_doesnt_exist(self): | ||
781 | with self.assertRaises(manifest_xml.ManifestParseError): | ||
782 | manifest = self.getXmlManifest(""" | ||
783 | <manifest> | ||
784 | <remote name="default-remote" fetch="http://localhost" /> | ||
785 | <default remote="default-remote" revision="refs/heads/main" /> | ||
786 | <remove-project name="myproject" /> | ||
787 | </manifest> | ||
788 | """) | ||
789 | manifest.projects | ||
790 | |||
791 | def test_remove_one_optional_project_doesnt_exist(self): | ||
792 | manifest = self.getXmlManifest(""" | ||
793 | <manifest> | ||
794 | <remote name="default-remote" fetch="http://localhost" /> | ||
795 | <default remote="default-remote" revision="refs/heads/main" /> | ||
796 | <remove-project name="myproject" optional="true" /> | ||
797 | </manifest> | ||
798 | """) | ||
799 | self.assertEqual(manifest.projects, []) | ||
800 | |||
801 | |||
802 | class ExtendProjectElementTests(ManifestParseTestCase): | ||
803 | """Tests for <extend-project>.""" | ||
804 | |||
805 | def test_extend_project_dest_path_single_match(self): | ||
806 | manifest = self.getXmlManifest(""" | ||
807 | <manifest> | ||
808 | <remote name="default-remote" fetch="http://localhost" /> | ||
809 | <default remote="default-remote" revision="refs/heads/main" /> | ||
810 | <project name="myproject" /> | ||
811 | <extend-project name="myproject" dest-path="bar" /> | ||
812 | </manifest> | ||
813 | """) | ||
814 | self.assertEqual(len(manifest.projects), 1) | ||
815 | self.assertEqual(manifest.projects[0].relpath, 'bar') | ||
816 | |||
817 | def test_extend_project_dest_path_multi_match(self): | ||
818 | with self.assertRaises(manifest_xml.ManifestParseError): | ||
819 | manifest = self.getXmlManifest(""" | ||
820 | <manifest> | ||
821 | <remote name="default-remote" fetch="http://localhost" /> | ||
822 | <default remote="default-remote" revision="refs/heads/main" /> | ||
823 | <project name="myproject" path="x" /> | ||
824 | <project name="myproject" path="y" /> | ||
825 | <extend-project name="myproject" dest-path="bar" /> | ||
826 | </manifest> | ||
827 | """) | ||
828 | manifest.projects | ||
829 | |||
830 | def test_extend_project_dest_path_multi_match_path_specified(self): | ||
831 | manifest = self.getXmlManifest(""" | ||
832 | <manifest> | ||
833 | <remote name="default-remote" fetch="http://localhost" /> | ||
834 | <default remote="default-remote" revision="refs/heads/main" /> | ||
835 | <project name="myproject" path="x" /> | ||
836 | <project name="myproject" path="y" /> | ||
837 | <extend-project name="myproject" path="x" dest-path="bar" /> | ||
838 | </manifest> | ||
839 | """) | ||
840 | self.assertEqual(len(manifest.projects), 2) | ||
841 | if manifest.projects[0].relpath == 'y': | ||
842 | self.assertEqual(manifest.projects[1].relpath, 'bar') | ||
843 | else: | ||
844 | self.assertEqual(manifest.projects[0].relpath, 'bar') | ||
845 | self.assertEqual(manifest.projects[1].relpath, 'y') | ||
diff --git a/tests/test_platform_utils.py b/tests/test_platform_utils.py new file mode 100644 index 00000000..55b7805c --- /dev/null +++ b/tests/test_platform_utils.py | |||
@@ -0,0 +1,50 @@ | |||
1 | # Copyright 2021 The Android Open Source Project | ||
2 | # | ||
3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
4 | # you may not use this file except in compliance with the License. | ||
5 | # You may obtain a copy of the License at | ||
6 | # | ||
7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
8 | # | ||
9 | # Unless required by applicable law or agreed to in writing, software | ||
10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
12 | # See the License for the specific language governing permissions and | ||
13 | # limitations under the License. | ||
14 | |||
15 | """Unittests for the platform_utils.py module.""" | ||
16 | |||
17 | import os | ||
18 | import tempfile | ||
19 | import unittest | ||
20 | |||
21 | import platform_utils | ||
22 | |||
23 | |||
24 | class RemoveTests(unittest.TestCase): | ||
25 | """Check remove() helper.""" | ||
26 | |||
27 | def testMissingOk(self): | ||
28 | """Check missing_ok handling.""" | ||
29 | with tempfile.TemporaryDirectory() as tmpdir: | ||
30 | path = os.path.join(tmpdir, 'test') | ||
31 | |||
32 | # Should not fail. | ||
33 | platform_utils.remove(path, missing_ok=True) | ||
34 | |||
35 | # Should fail. | ||
36 | self.assertRaises(OSError, platform_utils.remove, path) | ||
37 | self.assertRaises(OSError, platform_utils.remove, path, missing_ok=False) | ||
38 | |||
39 | # Should not fail if it exists. | ||
40 | open(path, 'w').close() | ||
41 | platform_utils.remove(path, missing_ok=True) | ||
42 | self.assertFalse(os.path.exists(path)) | ||
43 | |||
44 | open(path, 'w').close() | ||
45 | platform_utils.remove(path) | ||
46 | self.assertFalse(os.path.exists(path)) | ||
47 | |||
48 | open(path, 'w').close() | ||
49 | platform_utils.remove(path, missing_ok=False) | ||
50 | self.assertFalse(os.path.exists(path)) | ||
diff --git a/tests/test_ssh.py b/tests/test_ssh.py new file mode 100644 index 00000000..ffb5cb94 --- /dev/null +++ b/tests/test_ssh.py | |||
@@ -0,0 +1,74 @@ | |||
1 | # Copyright 2019 The Android Open Source Project | ||
2 | # | ||
3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
4 | # you may not use this file except in compliance with the License. | ||
5 | # You may obtain a copy of the License at | ||
6 | # | ||
7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
8 | # | ||
9 | # Unless required by applicable law or agreed to in writing, software | ||
10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
12 | # See the License for the specific language governing permissions and | ||
13 | # limitations under the License. | ||
14 | |||
15 | """Unittests for the ssh.py module.""" | ||
16 | |||
17 | import multiprocessing | ||
18 | import subprocess | ||
19 | import unittest | ||
20 | from unittest import mock | ||
21 | |||
22 | import ssh | ||
23 | |||
24 | |||
25 | class SshTests(unittest.TestCase): | ||
26 | """Tests the ssh functions.""" | ||
27 | |||
28 | def test_parse_ssh_version(self): | ||
29 | """Check _parse_ssh_version() handling.""" | ||
30 | ver = ssh._parse_ssh_version('Unknown\n') | ||
31 | self.assertEqual(ver, ()) | ||
32 | ver = ssh._parse_ssh_version('OpenSSH_1.0\n') | ||
33 | self.assertEqual(ver, (1, 0)) | ||
34 | ver = ssh._parse_ssh_version('OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n') | ||
35 | self.assertEqual(ver, (6, 6, 1)) | ||
36 | ver = ssh._parse_ssh_version('OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n') | ||
37 | self.assertEqual(ver, (7, 6)) | ||
38 | |||
39 | def test_version(self): | ||
40 | """Check version() handling.""" | ||
41 | with mock.patch('ssh._run_ssh_version', return_value='OpenSSH_1.2\n'): | ||
42 | self.assertEqual(ssh.version(), (1, 2)) | ||
43 | |||
44 | def test_context_manager_empty(self): | ||
45 | """Verify context manager with no clients works correctly.""" | ||
46 | with multiprocessing.Manager() as manager: | ||
47 | with ssh.ProxyManager(manager): | ||
48 | pass | ||
49 | |||
50 | def test_context_manager_child_cleanup(self): | ||
51 | """Verify orphaned clients & masters get cleaned up.""" | ||
52 | with multiprocessing.Manager() as manager: | ||
53 | with ssh.ProxyManager(manager) as ssh_proxy: | ||
54 | client = subprocess.Popen(['sleep', '964853320']) | ||
55 | ssh_proxy.add_client(client) | ||
56 | master = subprocess.Popen(['sleep', '964853321']) | ||
57 | ssh_proxy.add_master(master) | ||
58 | # If the process still exists, these will throw timeout errors. | ||
59 | client.wait(0) | ||
60 | master.wait(0) | ||
61 | |||
62 | def test_ssh_sock(self): | ||
63 | """Check sock() function.""" | ||
64 | manager = multiprocessing.Manager() | ||
65 | proxy = ssh.ProxyManager(manager) | ||
66 | with mock.patch('tempfile.mkdtemp', return_value='/tmp/foo'): | ||
67 | # old ssh version uses port | ||
68 | with mock.patch('ssh.version', return_value=(6, 6)): | ||
69 | self.assertTrue(proxy.sock().endswith('%p')) | ||
70 | |||
71 | proxy._sock_path = None | ||
72 | # new ssh version uses hash | ||
73 | with mock.patch('ssh.version', return_value=(6, 7)): | ||
74 | self.assertTrue(proxy.sock().endswith('%C')) | ||
diff --git a/tests/test_subcmds.py b/tests/test_subcmds.py index 2234e646..bc53051a 100644 --- a/tests/test_subcmds.py +++ b/tests/test_subcmds.py | |||
@@ -14,6 +14,7 @@ | |||
14 | 14 | ||
15 | """Unittests for the subcmds module (mostly __init__.py than subcommands).""" | 15 | """Unittests for the subcmds module (mostly __init__.py than subcommands).""" |
16 | 16 | ||
17 | import optparse | ||
17 | import unittest | 18 | import unittest |
18 | 19 | ||
19 | import subcmds | 20 | import subcmds |
@@ -41,3 +42,32 @@ class AllCommands(unittest.TestCase): | |||
41 | 42 | ||
42 | # Reject internal python paths like "__init__". | 43 | # Reject internal python paths like "__init__". |
43 | self.assertFalse(cmd.startswith('__')) | 44 | self.assertFalse(cmd.startswith('__')) |
45 | |||
46 | def test_help_desc_style(self): | ||
47 | """Force some consistency in option descriptions. | ||
48 | |||
49 | Python's optparse & argparse has a few default options like --help. Their | ||
50 | option description text uses lowercase sentence fragments, so enforce our | ||
51 | options follow the same style so UI is consistent. | ||
52 | |||
53 | We enforce: | ||
54 | * Text starts with lowercase. | ||
55 | * Text doesn't end with period. | ||
56 | """ | ||
57 | for name, cls in subcmds.all_commands.items(): | ||
58 | cmd = cls() | ||
59 | parser = cmd.OptionParser | ||
60 | for option in parser.option_list: | ||
61 | if option.help == optparse.SUPPRESS_HELP: | ||
62 | continue | ||
63 | |||
64 | c = option.help[0] | ||
65 | self.assertEqual( | ||
66 | c.lower(), c, | ||
67 | msg=f'subcmds/{name}.py: {option.get_opt_string()}: help text ' | ||
68 | f'should start with lowercase: "{option.help}"') | ||
69 | |||
70 | self.assertNotEqual( | ||
71 | option.help[-1], '.', | ||
72 | msg=f'subcmds/{name}.py: {option.get_opt_string()}: help text ' | ||
73 | f'should not end in a period: "{option.help}"') | ||