summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/fixtures/test.gitconfig10
-rw-r--r--tests/test_git_command.py27
-rw-r--r--tests/test_git_config.py19
-rw-r--r--tests/test_git_superproject.py260
-rw-r--r--tests/test_git_trace2_event_log.py72
-rw-r--r--tests/test_manifest_xml.py346
-rw-r--r--tests/test_platform_utils.py50
-rw-r--r--tests/test_ssh.py74
-rw-r--r--tests/test_subcmds.py30
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
26import wrapper 26import wrapper
27 27
28 28
29class 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
56class GitCallUnitTest(unittest.TestCase): 29class 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
108class GitConfigReadWriteTests(unittest.TestCase): 127class 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
17import json
17import os 18import os
18import platform 19import platform
19import tempfile 20import tempfile
@@ -21,13 +22,20 @@ import unittest
21from unittest import mock 22from unittest import mock
22 23
23import git_superproject 24import git_superproject
25import git_trace2_event_log
24import manifest_xml 26import manifest_xml
25import platform_utils 27import platform_utils
28from test_manifest_xml import sort_attributes
26 29
27 30
28class SuperprojectTestCase(unittest.TestCase): 31class 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
17import os 17import os
18import platform 18import platform
19import re
19import shutil 20import shutil
20import tempfile 21import tempfile
21import unittest 22import 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
67def 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
63class ManifestParseTestCase(unittest.TestCase): 91class 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', '&#x000d;').replace('\n', '&#x000a;')
126
94 127
95class ManifestValidateFilePaths(unittest.TestCase): 128class 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
694class 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
713class 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
730class 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
752class 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
802class 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
17import os
18import tempfile
19import unittest
20
21import platform_utils
22
23
24class 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
17import multiprocessing
18import subprocess
19import unittest
20from unittest import mock
21
22import ssh
23
24
25class 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
17import optparse
17import unittest 18import unittest
18 19
19import subcmds 20import 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}"')