1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright 2020 The Chromium OS Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Tests for rust_uprev.py""" 8 9import os 10import shutil 11import subprocess 12import tempfile 13import unittest 14from pathlib import Path 15from unittest import mock 16 17from llvm_tools import git 18 19import rust_uprev 20from rust_uprev import RustVersion 21 22 23def _fail_command(cmd, *_args, **_kwargs): 24 err = subprocess.CalledProcessError(returncode=1, cmd=cmd) 25 err.stderr = b'mock failure' 26 raise err 27 28 29class FetchDistfileTest(unittest.TestCase): 30 """Tests rust_uprev.fetch_distfile_from_mirror()""" 31 32 @mock.patch.object(rust_uprev, 'get_distdir', return_value='/fake/distfiles') 33 @mock.patch.object(subprocess, 'call', side_effect=_fail_command) 34 def test_fetch_difstfile_fail(self, *_args) -> None: 35 with self.assertRaises(subprocess.CalledProcessError): 36 rust_uprev.fetch_distfile_from_mirror('test_distfile.tar.gz') 37 38 @mock.patch.object(rust_uprev, 39 'get_command_output_unchecked', 40 return_value='AccessDeniedException: Access denied.') 41 @mock.patch.object(rust_uprev, 'get_distdir', return_value='/fake/distfiles') 42 @mock.patch.object(subprocess, 'call', return_value=0) 43 def test_fetch_distfile_acl_access_denied(self, *_args) -> None: 44 rust_uprev.fetch_distfile_from_mirror('test_distfile.tar.gz') 45 46 @mock.patch.object( 47 rust_uprev, 48 'get_command_output_unchecked', 49 return_value='[ { "entity": "allUsers", "role": "READER" } ]') 50 @mock.patch.object(rust_uprev, 'get_distdir', return_value='/fake/distfiles') 51 @mock.patch.object(subprocess, 'call', return_value=0) 52 def test_fetch_distfile_acl_ok(self, *_args) -> None: 53 rust_uprev.fetch_distfile_from_mirror('test_distfile.tar.gz') 54 55 @mock.patch.object( 56 rust_uprev, 57 'get_command_output_unchecked', 58 return_value='[ { "entity": "___fake@google.com", "role": "OWNER" } ]') 59 @mock.patch.object(rust_uprev, 'get_distdir', return_value='/fake/distfiles') 60 @mock.patch.object(subprocess, 'call', return_value=0) 61 def test_fetch_distfile_acl_wrong(self, *_args) -> None: 62 with self.assertRaisesRegex(Exception, 'allUsers.*READER'): 63 with self.assertLogs(level='ERROR') as log: 64 rust_uprev.fetch_distfile_from_mirror('test_distfile.tar.gz') 65 self.assertIn( 66 '[ { "entity": "___fake@google.com", "role": "OWNER" } ]', 67 '\n'.join(log.output)) 68 69 70class FindEbuildPathTest(unittest.TestCase): 71 """Tests for rust_uprev.find_ebuild_path()""" 72 73 def test_exact_version(self): 74 with tempfile.TemporaryDirectory() as tmpdir: 75 ebuild = Path(tmpdir, 'test-1.3.4.ebuild') 76 ebuild.touch() 77 Path(tmpdir, 'test-1.2.3.ebuild').touch() 78 result = rust_uprev.find_ebuild_path(tmpdir, 'test', 79 rust_uprev.RustVersion(1, 3, 4)) 80 self.assertEqual(result, ebuild) 81 82 def test_no_version(self): 83 with tempfile.TemporaryDirectory() as tmpdir: 84 ebuild = Path(tmpdir, 'test-1.2.3.ebuild') 85 ebuild.touch() 86 result = rust_uprev.find_ebuild_path(tmpdir, 'test') 87 self.assertEqual(result, ebuild) 88 89 def test_patch_version(self): 90 with tempfile.TemporaryDirectory() as tmpdir: 91 ebuild = Path(tmpdir, 'test-1.3.4-r3.ebuild') 92 ebuild.touch() 93 Path(tmpdir, 'test-1.2.3.ebuild').touch() 94 result = rust_uprev.find_ebuild_path(tmpdir, 'test', 95 rust_uprev.RustVersion(1, 3, 4)) 96 self.assertEqual(result, ebuild) 97 98 99class RustVersionTest(unittest.TestCase): 100 """Tests for RustVersion class""" 101 102 def test_str(self): 103 obj = rust_uprev.RustVersion(major=1, minor=2, patch=3) 104 self.assertEqual(str(obj), '1.2.3') 105 106 def test_parse_version_only(self): 107 expected = rust_uprev.RustVersion(major=1, minor=2, patch=3) 108 actual = rust_uprev.RustVersion.parse('1.2.3') 109 self.assertEqual(expected, actual) 110 111 def test_parse_ebuild_name(self): 112 expected = rust_uprev.RustVersion(major=1, minor=2, patch=3) 113 actual = rust_uprev.RustVersion.parse_from_ebuild('rust-1.2.3.ebuild') 114 self.assertEqual(expected, actual) 115 116 actual = rust_uprev.RustVersion.parse_from_ebuild('rust-1.2.3-r1.ebuild') 117 self.assertEqual(expected, actual) 118 119 def test_parse_fail(self): 120 with self.assertRaises(AssertionError) as context: 121 rust_uprev.RustVersion.parse('invalid-rust-1.2.3') 122 self.assertEqual("failed to parse 'invalid-rust-1.2.3'", 123 str(context.exception)) 124 125 126class PrepareUprevTest(unittest.TestCase): 127 """Tests for prepare_uprev step in rust_uprev""" 128 129 def setUp(self): 130 self.bootstrap_version = rust_uprev.RustVersion(1, 1, 0) 131 self.version_old = rust_uprev.RustVersion(1, 2, 3) 132 self.version_new = rust_uprev.RustVersion(1, 3, 5) 133 134 @mock.patch.object(rust_uprev, 135 'find_ebuild_for_rust_version', 136 return_value='/path/to/ebuild') 137 @mock.patch.object(rust_uprev, 'find_ebuild_path') 138 @mock.patch.object(rust_uprev, 'get_command_output') 139 def test_success_with_template(self, mock_command, mock_find_ebuild, 140 _ebuild_for_version): 141 bootstrap_ebuild_path = Path( 142 '/path/to/rust-bootstrap/', 143 f'rust-bootstrap-{self.bootstrap_version}.ebuild') 144 mock_find_ebuild.return_value = bootstrap_ebuild_path 145 expected = (self.version_old, '/path/to/ebuild', self.bootstrap_version) 146 actual = rust_uprev.prepare_uprev(rust_version=self.version_new, 147 template=self.version_old) 148 self.assertEqual(expected, actual) 149 mock_command.assert_not_called() 150 151 @mock.patch.object(rust_uprev, 152 'find_ebuild_for_rust_version', 153 return_value='/path/to/ebuild') 154 @mock.patch.object(rust_uprev, 155 'get_rust_bootstrap_version', 156 return_value=RustVersion(0, 41, 12)) 157 @mock.patch.object(rust_uprev, 'get_command_output') 158 def test_return_none_with_template_larger_than_input(self, mock_command, 159 *_args): 160 ret = rust_uprev.prepare_uprev(rust_version=self.version_old, 161 template=self.version_new) 162 self.assertIsNone(ret) 163 mock_command.assert_not_called() 164 165 @mock.patch.object(rust_uprev, 'find_ebuild_path') 166 @mock.patch.object(os.path, 'exists') 167 @mock.patch.object(rust_uprev, 'get_command_output') 168 def test_success_without_template(self, mock_command, mock_exists, 169 mock_find_ebuild): 170 rust_ebuild_path = f'/path/to/rust/rust-{self.version_old}-r3.ebuild' 171 mock_command.return_value = rust_ebuild_path 172 bootstrap_ebuild_path = Path( 173 '/path/to/rust-bootstrap', 174 f'rust-bootstrap-{self.bootstrap_version}.ebuild') 175 mock_find_ebuild.return_value = bootstrap_ebuild_path 176 expected = (self.version_old, rust_ebuild_path, self.bootstrap_version) 177 actual = rust_uprev.prepare_uprev(rust_version=self.version_new, 178 template=None) 179 self.assertEqual(expected, actual) 180 mock_command.assert_called_once_with(['equery', 'w', 'rust']) 181 mock_exists.assert_not_called() 182 183 @mock.patch.object(rust_uprev, 184 'get_rust_bootstrap_version', 185 return_value=RustVersion(0, 41, 12)) 186 @mock.patch.object(os.path, 'exists') 187 @mock.patch.object(rust_uprev, 'get_command_output') 188 def test_return_none_with_ebuild_larger_than_input(self, mock_command, 189 mock_exists, *_args): 190 mock_command.return_value = f'/path/to/rust/rust-{self.version_new}.ebuild' 191 ret = rust_uprev.prepare_uprev(rust_version=self.version_old, 192 template=None) 193 self.assertIsNone(ret) 194 mock_exists.assert_not_called() 195 196 def test_prepare_uprev_from_json(self): 197 ebuild_path = '/path/to/the/ebuild' 198 json_result = (list(self.version_new), ebuild_path, 199 list(self.bootstrap_version)) 200 expected = (self.version_new, ebuild_path, self.bootstrap_version) 201 actual = rust_uprev.prepare_uprev_from_json(json_result) 202 self.assertEqual(expected, actual) 203 204 205class UpdateEbuildTest(unittest.TestCase): 206 """Tests for update_ebuild step in rust_uprev""" 207 ebuild_file_before = """ 208BOOTSTRAP_VERSION="1.2.0" 209 """ 210 ebuild_file_after = """ 211BOOTSTRAP_VERSION="1.3.6" 212 """ 213 214 def test_success(self): 215 mock_open = mock.mock_open(read_data=self.ebuild_file_before) 216 # ebuild_file and new bootstrap version are deliberately different 217 ebuild_file = '/path/to/rust/rust-1.3.5.ebuild' 218 with mock.patch('builtins.open', mock_open): 219 rust_uprev.update_ebuild(ebuild_file, 220 rust_uprev.RustVersion.parse('1.3.6')) 221 mock_open.return_value.__enter__().write.assert_called_once_with( 222 self.ebuild_file_after) 223 224 def test_fail_when_ebuild_misses_a_variable(self): 225 mock_open = mock.mock_open(read_data='') 226 ebuild_file = '/path/to/rust/rust-1.3.5.ebuild' 227 with mock.patch('builtins.open', mock_open): 228 with self.assertRaises(RuntimeError) as context: 229 rust_uprev.update_ebuild(ebuild_file, 230 rust_uprev.RustVersion.parse('1.2.0')) 231 self.assertEqual('BOOTSTRAP_VERSION not found in rust ebuild', 232 str(context.exception)) 233 234 235class UpdateManifestTest(unittest.TestCase): 236 """Tests for update_manifest step in rust_uprev""" 237 238 # pylint: disable=protected-access 239 def _run_test_flip_mirror(self, before, after, add, expect_write): 240 mock_open = mock.mock_open(read_data=f'RESTRICT="{before}"') 241 with mock.patch('builtins.open', mock_open): 242 rust_uprev.flip_mirror_in_ebuild('', add=add) 243 if expect_write: 244 mock_open.return_value.__enter__().write.assert_called_once_with( 245 f'RESTRICT="{after}"') 246 247 def test_add_mirror_in_ebuild(self): 248 self._run_test_flip_mirror(before='variable1 variable2', 249 after='variable1 variable2 mirror', 250 add=True, 251 expect_write=True) 252 253 def test_remove_mirror_in_ebuild(self): 254 self._run_test_flip_mirror(before='variable1 variable2 mirror', 255 after='variable1 variable2', 256 add=False, 257 expect_write=True) 258 259 def test_add_mirror_when_exists(self): 260 self._run_test_flip_mirror(before='variable1 variable2 mirror', 261 after='variable1 variable2 mirror', 262 add=True, 263 expect_write=False) 264 265 def test_remove_mirror_when_not_exists(self): 266 self._run_test_flip_mirror(before='variable1 variable2', 267 after='variable1 variable2', 268 add=False, 269 expect_write=False) 270 271 @mock.patch.object(rust_uprev, 'flip_mirror_in_ebuild') 272 @mock.patch.object(rust_uprev, 'ebuild_actions') 273 def test_update_manifest(self, mock_run, mock_flip): 274 ebuild_file = Path('/path/to/rust/rust-1.1.1.ebuild') 275 rust_uprev.update_manifest(ebuild_file) 276 mock_run.assert_called_once_with('rust', ['manifest']) 277 mock_flip.assert_has_calls( 278 [mock.call(ebuild_file, add=True), 279 mock.call(ebuild_file, add=False)]) 280 281 282class UpdateBootstrapEbuildTest(unittest.TestCase): 283 """Tests for rust_uprev.update_bootstrap_ebuild()""" 284 285 def test_update_bootstrap_ebuild(self): 286 # The update should do two things: 287 # 1. Create a copy of rust-bootstrap's ebuild with the new version number. 288 # 2. Add the old PV to RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE. 289 with tempfile.TemporaryDirectory() as tmpdir_str, \ 290 mock.patch.object(rust_uprev, 'find_ebuild_path') as mock_find_ebuild: 291 tmpdir = Path(tmpdir_str) 292 bootstrapdir = Path.joinpath(tmpdir, 'rust-bootstrap') 293 bootstrapdir.mkdir() 294 old_ebuild = bootstrapdir.joinpath('rust-bootstrap-1.45.2.ebuild') 295 old_ebuild.write_text(encoding='utf-8', 296 data=""" 297some text 298RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=( 299\t1.43.1 300\t1.44.1 301) 302some more text 303""") 304 mock_find_ebuild.return_value = old_ebuild 305 rust_uprev.update_bootstrap_ebuild(rust_uprev.RustVersion(1, 46, 0)) 306 new_ebuild = bootstrapdir.joinpath('rust-bootstrap-1.46.0.ebuild') 307 self.assertTrue(new_ebuild.exists()) 308 text = new_ebuild.read_text() 309 self.assertEqual( 310 text, """ 311some text 312RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=( 313\t1.43.1 314\t1.44.1 315\t1.45.2 316) 317some more text 318""") 319 320 321class UpdateRustPackagesTests(unittest.TestCase): 322 """Tests for update_rust_packages step.""" 323 324 def setUp(self): 325 self.old_version = rust_uprev.RustVersion(1, 1, 0) 326 self.current_version = rust_uprev.RustVersion(1, 2, 3) 327 self.new_version = rust_uprev.RustVersion(1, 3, 5) 328 self.ebuild_file = os.path.join(rust_uprev.RUST_PATH, 329 'rust-{self.new_version}.ebuild') 330 331 def test_add_new_rust_packages(self): 332 package_before = (f'dev-lang/rust-{self.old_version}\n' 333 f'dev-lang/rust-{self.current_version}') 334 package_after = (f'dev-lang/rust-{self.old_version}\n' 335 f'dev-lang/rust-{self.current_version}\n' 336 f'dev-lang/rust-{self.new_version}') 337 mock_open = mock.mock_open(read_data=package_before) 338 with mock.patch('builtins.open', mock_open): 339 rust_uprev.update_rust_packages(self.new_version, add=True) 340 mock_open.return_value.__enter__().write.assert_called_once_with( 341 package_after) 342 343 def test_remove_old_rust_packages(self): 344 package_before = (f'dev-lang/rust-{self.old_version}\n' 345 f'dev-lang/rust-{self.current_version}\n' 346 f'dev-lang/rust-{self.new_version}') 347 package_after = (f'dev-lang/rust-{self.current_version}\n' 348 f'dev-lang/rust-{self.new_version}') 349 mock_open = mock.mock_open(read_data=package_before) 350 with mock.patch('builtins.open', mock_open): 351 rust_uprev.update_rust_packages(self.old_version, add=False) 352 mock_open.return_value.__enter__().write.assert_called_once_with( 353 package_after) 354 355 356class RustUprevOtherStagesTests(unittest.TestCase): 357 """Tests for other steps in rust_uprev""" 358 359 def setUp(self): 360 self.old_version = rust_uprev.RustVersion(1, 1, 0) 361 self.current_version = rust_uprev.RustVersion(1, 2, 3) 362 self.new_version = rust_uprev.RustVersion(1, 3, 5) 363 self.ebuild_file = os.path.join(rust_uprev.RUST_PATH, 364 'rust-{self.new_version}.ebuild') 365 366 @mock.patch.object(shutil, 'copyfile') 367 @mock.patch.object(os, 'listdir') 368 @mock.patch.object(subprocess, 'check_call') 369 def test_copy_patches(self, mock_call, mock_ls, mock_copy): 370 mock_ls.return_value = [ 371 f'rust-{self.old_version}-patch-1.patch', 372 f'rust-{self.old_version}-patch-2-old.patch', 373 f'rust-{self.current_version}-patch-1.patch', 374 f'rust-{self.current_version}-patch-2-new.patch' 375 ] 376 rust_uprev.copy_patches(rust_uprev.RUST_PATH, self.current_version, 377 self.new_version) 378 mock_copy.assert_has_calls([ 379 mock.call( 380 os.path.join(rust_uprev.RUST_PATH, 'files', 381 f'rust-{self.current_version}-patch-1.patch'), 382 os.path.join(rust_uprev.RUST_PATH, 'files', 383 f'rust-{self.new_version}-patch-1.patch'), 384 ), 385 mock.call( 386 os.path.join(rust_uprev.RUST_PATH, 'files', 387 f'rust-{self.current_version}-patch-2-new.patch'), 388 os.path.join(rust_uprev.RUST_PATH, 'files', 389 f'rust-{self.new_version}-patch-2-new.patch')) 390 ]) 391 mock_call.assert_called_once_with( 392 ['git', 'add', f'rust-{self.new_version}-*.patch'], 393 cwd=rust_uprev.RUST_PATH.joinpath('files')) 394 395 @mock.patch.object(shutil, 'copyfile') 396 @mock.patch.object(subprocess, 'check_call') 397 def test_create_ebuild(self, mock_call, mock_copy): 398 template_ebuild = f'/path/to/rust-{self.current_version}-r2.ebuild' 399 rust_uprev.create_ebuild(template_ebuild, self.new_version) 400 mock_copy.assert_called_once_with( 401 template_ebuild, 402 rust_uprev.RUST_PATH.joinpath(f'rust-{self.new_version}.ebuild')) 403 mock_call.assert_called_once_with( 404 ['git', 'add', f'rust-{self.new_version}.ebuild'], 405 cwd=rust_uprev.RUST_PATH) 406 407 @mock.patch.object(rust_uprev, 'find_ebuild_for_package') 408 @mock.patch.object(subprocess, 'check_call') 409 def test_remove_rust_bootstrap_version(self, mock_call, *_args): 410 bootstrap_path = os.path.join(rust_uprev.RUST_PATH, '..', 'rust-bootstrap') 411 rust_uprev.remove_rust_bootstrap_version(self.old_version, lambda *x: ()) 412 mock_call.has_calls([ 413 [ 414 'git', 'rm', 415 os.path.join(bootstrap_path, 'files', 416 f'rust-bootstrap-{self.old_version}-*.patch') 417 ], 418 [ 419 'git', 'rm', 420 os.path.join(bootstrap_path, 421 f'rust-bootstrap-{self.old_version}.ebuild') 422 ], 423 ]) 424 425 @mock.patch.object(rust_uprev, 'find_ebuild_path') 426 @mock.patch.object(subprocess, 'check_call') 427 def test_remove_virtual_rust(self, mock_call, mock_find_ebuild): 428 ebuild_path = Path( 429 f'/some/dir/virtual/rust/rust-{self.old_version}.ebuild') 430 mock_find_ebuild.return_value = Path(ebuild_path) 431 rust_uprev.remove_virtual_rust(self.old_version) 432 mock_call.assert_called_once_with( 433 ['git', 'rm', str(ebuild_path.name)], cwd=ebuild_path.parent) 434 435 @mock.patch.object(rust_uprev, 'find_ebuild_path') 436 @mock.patch.object(shutil, 'copyfile') 437 @mock.patch.object(subprocess, 'check_call') 438 def test_update_virtual_rust(self, mock_call, mock_copy, mock_find_ebuild): 439 ebuild_path = Path( 440 f'/some/dir/virtual/rust/rust-{self.current_version}.ebuild') 441 mock_find_ebuild.return_value = Path(ebuild_path) 442 rust_uprev.update_virtual_rust(self.current_version, self.new_version) 443 mock_call.assert_called_once_with( 444 ['git', 'add', f'rust-{self.new_version}.ebuild'], 445 cwd=ebuild_path.parent) 446 mock_copy.assert_called_once_with( 447 ebuild_path.parent.joinpath(f'rust-{self.current_version}.ebuild'), 448 ebuild_path.parent.joinpath(f'rust-{self.new_version}.ebuild')) 449 450 @mock.patch.object(os, 'listdir') 451 def test_find_oldest_rust_version_in_chroot_pass(self, mock_ls): 452 oldest_version_name = f'rust-{self.old_version}.ebuild' 453 mock_ls.return_value = [ 454 oldest_version_name, f'rust-{self.current_version}.ebuild', 455 f'rust-{self.new_version}.ebuild' 456 ] 457 actual = rust_uprev.find_oldest_rust_version_in_chroot() 458 expected = (self.old_version, 459 os.path.join(rust_uprev.RUST_PATH, oldest_version_name)) 460 self.assertEqual(expected, actual) 461 462 @mock.patch.object(os, 'listdir') 463 def test_find_oldest_rust_version_in_chroot_fail_with_only_one_ebuild( 464 self, mock_ls): 465 mock_ls.return_value = [f'rust-{self.new_version}.ebuild'] 466 with self.assertRaises(RuntimeError) as context: 467 rust_uprev.find_oldest_rust_version_in_chroot() 468 self.assertEqual('Expect to find more than one Rust versions', 469 str(context.exception)) 470 471 @mock.patch.object(rust_uprev, 'get_command_output') 472 @mock.patch.object(git, 'CreateBranch') 473 def test_create_new_repo(self, mock_branch, mock_output): 474 mock_output.return_value = '' 475 rust_uprev.create_new_repo(self.new_version) 476 mock_branch.assert_called_once_with(rust_uprev.RUST_PATH, 477 f'rust-to-{self.new_version}') 478 479 @mock.patch.object(rust_uprev, 'get_command_output') 480 @mock.patch.object(subprocess, 'check_call') 481 def test_build_cross_compiler(self, mock_call, mock_output): 482 mock_output.return_value = f'rust-{self.new_version}.ebuild' 483 cros_targets = [ 484 'x86_64-cros-linux-gnu', 485 'armv7a-cros-linux-gnueabihf', 486 'aarch64-cros-linux-gnu', 487 ] 488 all_triples = ['x86_64-pc-linux-gnu'] + cros_targets 489 rust_ebuild = 'RUSTC_TARGET_TRIPLES=(' + '\n\t'.join(all_triples) + ')' 490 mock_open = mock.mock_open(read_data=rust_ebuild) 491 with mock.patch('builtins.open', mock_open): 492 rust_uprev.build_cross_compiler() 493 494 mock_call.assert_called_once_with( 495 ['sudo', 'emerge', '-j', '-G'] + 496 [f'cross-{x}/gcc' for x in cros_targets + ['arm-none-eabi']]) 497 498 499if __name__ == '__main__': 500 unittest.main() 501