#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright 2020 The ChromiumOS Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Tests for rust_uprev.py""" import os from pathlib import Path import shutil import subprocess import tempfile import unittest from unittest import mock from llvm_tools import git import rust_uprev from rust_uprev import RustVersion def _fail_command(cmd, *_args, **_kwargs): err = subprocess.CalledProcessError(returncode=1, cmd=cmd) err.stderr = b"mock failure" raise err class FetchDistfileTest(unittest.TestCase): """Tests rust_uprev.fetch_distfile_from_mirror()""" @mock.patch.object( rust_uprev, "get_distdir", return_value="/fake/distfiles" ) @mock.patch.object(subprocess, "call", side_effect=_fail_command) def test_fetch_difstfile_fail(self, *_args) -> None: with self.assertRaises(subprocess.CalledProcessError): rust_uprev.fetch_distfile_from_mirror("test_distfile.tar.gz") @mock.patch.object( rust_uprev, "get_command_output_unchecked", return_value="AccessDeniedException: Access denied.", ) @mock.patch.object( rust_uprev, "get_distdir", return_value="/fake/distfiles" ) @mock.patch.object(subprocess, "call", return_value=0) def test_fetch_distfile_acl_access_denied(self, *_args) -> None: rust_uprev.fetch_distfile_from_mirror("test_distfile.tar.gz") @mock.patch.object( rust_uprev, "get_command_output_unchecked", return_value='[ { "entity": "allUsers", "role": "READER" } ]', ) @mock.patch.object( rust_uprev, "get_distdir", return_value="/fake/distfiles" ) @mock.patch.object(subprocess, "call", return_value=0) def test_fetch_distfile_acl_ok(self, *_args) -> None: rust_uprev.fetch_distfile_from_mirror("test_distfile.tar.gz") @mock.patch.object( rust_uprev, "get_command_output_unchecked", return_value='[ { "entity": "___fake@google.com", "role": "OWNER" } ]', ) @mock.patch.object( rust_uprev, "get_distdir", return_value="/fake/distfiles" ) @mock.patch.object(subprocess, "call", return_value=0) def test_fetch_distfile_acl_wrong(self, *_args) -> None: with self.assertRaisesRegex(Exception, "allUsers.*READER"): with self.assertLogs(level="ERROR") as log: rust_uprev.fetch_distfile_from_mirror("test_distfile.tar.gz") self.assertIn( '[ { "entity": "___fake@google.com", "role": "OWNER" } ]', "\n".join(log.output), ) class FindEbuildPathTest(unittest.TestCase): """Tests for rust_uprev.find_ebuild_path()""" def test_exact_version(self): with tempfile.TemporaryDirectory() as t: tmpdir = Path(t) ebuild = tmpdir / "test-1.3.4.ebuild" ebuild.touch() (tmpdir / "test-1.2.3.ebuild").touch() result = rust_uprev.find_ebuild_path( tmpdir, "test", rust_uprev.RustVersion(1, 3, 4) ) self.assertEqual(result, ebuild) def test_no_version(self): with tempfile.TemporaryDirectory() as t: tmpdir = Path(t) ebuild = tmpdir / "test-1.2.3.ebuild" ebuild.touch() result = rust_uprev.find_ebuild_path(tmpdir, "test") self.assertEqual(result, ebuild) def test_patch_version(self): with tempfile.TemporaryDirectory() as t: tmpdir = Path(t) ebuild = tmpdir / "test-1.3.4-r3.ebuild" ebuild.touch() (tmpdir / "test-1.2.3.ebuild").touch() result = rust_uprev.find_ebuild_path( tmpdir, "test", rust_uprev.RustVersion(1, 3, 4) ) self.assertEqual(result, ebuild) def test_multiple_versions(self): with tempfile.TemporaryDirectory() as t: tmpdir = Path(t) (tmpdir / "test-1.3.4-r3.ebuild").touch() (tmpdir / "test-1.3.5.ebuild").touch() with self.assertRaises(AssertionError): rust_uprev.find_ebuild_path(tmpdir, "test") def test_selected_version(self): with tempfile.TemporaryDirectory() as t: tmpdir = Path(t) ebuild = tmpdir / "test-1.3.4-r3.ebuild" ebuild.touch() (tmpdir / "test-1.3.5.ebuild").touch() result = rust_uprev.find_ebuild_path( tmpdir, "test", rust_uprev.RustVersion(1, 3, 4) ) self.assertEqual(result, ebuild) def test_symlink(self): # Symlinks to ebuilds in the same directory are allowed, and the return # value is the regular file. with tempfile.TemporaryDirectory() as t: tmpdir = Path(t) ebuild = tmpdir / "test-1.3.4.ebuild" ebuild.touch() (tmpdir / "test-1.3.4-r1.ebuild").symlink_to("test-1.3.4.ebuild") result = rust_uprev.find_ebuild_path(tmpdir, "test") self.assertEqual(result, ebuild) class RemoveEbuildVersionTest(unittest.TestCase): """Tests for rust_uprev.remove_ebuild_version()""" @mock.patch.object(subprocess, "check_call") def test_single(self, check_call): with tempfile.TemporaryDirectory() as tmpdir: ebuild_dir = Path(tmpdir, "test-ebuilds") ebuild_dir.mkdir() ebuild = Path(ebuild_dir, "test-3.1.4.ebuild") ebuild.touch() Path(ebuild_dir, "unrelated-1.0.0.ebuild").touch() rust_uprev.remove_ebuild_version( ebuild_dir, "test", rust_uprev.RustVersion(3, 1, 4) ) check_call.assert_called_once_with( ["git", "rm", "test-3.1.4.ebuild"], cwd=ebuild_dir ) @mock.patch.object(subprocess, "check_call") def test_symlink(self, check_call): with tempfile.TemporaryDirectory() as tmpdir: ebuild_dir = Path(tmpdir, "test-ebuilds") ebuild_dir.mkdir() ebuild = Path(ebuild_dir, "test-3.1.4.ebuild") ebuild.touch() symlink = Path(ebuild_dir, "test-3.1.4-r5.ebuild") symlink.symlink_to(ebuild.name) Path(ebuild_dir, "unrelated-1.0.0.ebuild").touch() rust_uprev.remove_ebuild_version( ebuild_dir, "test", rust_uprev.RustVersion(3, 1, 4) ) check_call.assert_has_calls( [ mock.call( ["git", "rm", "test-3.1.4.ebuild"], cwd=ebuild_dir ), mock.call( ["git", "rm", "test-3.1.4-r5.ebuild"], cwd=ebuild_dir ), ], any_order=True, ) class RustVersionTest(unittest.TestCase): """Tests for RustVersion class""" def test_str(self): obj = rust_uprev.RustVersion(major=1, minor=2, patch=3) self.assertEqual(str(obj), "1.2.3") def test_parse_version_only(self): expected = rust_uprev.RustVersion(major=1, minor=2, patch=3) actual = rust_uprev.RustVersion.parse("1.2.3") self.assertEqual(expected, actual) def test_parse_ebuild_name(self): expected = rust_uprev.RustVersion(major=1, minor=2, patch=3) actual = rust_uprev.RustVersion.parse_from_ebuild("rust-1.2.3.ebuild") self.assertEqual(expected, actual) actual = rust_uprev.RustVersion.parse_from_ebuild( "rust-1.2.3-r1.ebuild" ) self.assertEqual(expected, actual) def test_parse_fail(self): with self.assertRaises(AssertionError) as context: rust_uprev.RustVersion.parse("invalid-rust-1.2.3") self.assertEqual( "failed to parse 'invalid-rust-1.2.3'", str(context.exception) ) class PrepareUprevTest(unittest.TestCase): """Tests for prepare_uprev step in rust_uprev""" def setUp(self): self.bootstrap_version = rust_uprev.RustVersion(1, 1, 0) self.version_old = rust_uprev.RustVersion(1, 2, 3) self.version_new = rust_uprev.RustVersion(1, 3, 5) @mock.patch.object( rust_uprev, "find_ebuild_for_rust_version", return_value="/path/to/ebuild", ) @mock.patch.object(rust_uprev, "find_ebuild_path") @mock.patch.object(rust_uprev, "get_command_output") def test_success_with_template( self, mock_command, mock_find_ebuild, _ebuild_for_version ): bootstrap_ebuild_path = Path( "/path/to/rust-bootstrap/", f"rust-bootstrap-{self.bootstrap_version}.ebuild", ) mock_find_ebuild.return_value = bootstrap_ebuild_path expected = (self.version_old, "/path/to/ebuild", self.bootstrap_version) actual = rust_uprev.prepare_uprev( rust_version=self.version_new, template=self.version_old ) self.assertEqual(expected, actual) mock_command.assert_not_called() @mock.patch.object( rust_uprev, "find_ebuild_for_rust_version", return_value="/path/to/ebuild", ) @mock.patch.object( rust_uprev, "get_rust_bootstrap_version", return_value=RustVersion(0, 41, 12), ) @mock.patch.object(rust_uprev, "get_command_output") def test_return_none_with_template_larger_than_input( self, mock_command, *_args ): ret = rust_uprev.prepare_uprev( rust_version=self.version_old, template=self.version_new ) self.assertIsNone(ret) mock_command.assert_not_called() @mock.patch.object(rust_uprev, "find_ebuild_path") @mock.patch.object(os.path, "exists") @mock.patch.object(rust_uprev, "get_command_output") def test_success_without_template( self, mock_command, mock_exists, mock_find_ebuild ): rust_ebuild_path = f"/path/to/rust/rust-{self.version_old}-r3.ebuild" mock_command.return_value = rust_ebuild_path bootstrap_ebuild_path = Path( "/path/to/rust-bootstrap", f"rust-bootstrap-{self.bootstrap_version}.ebuild", ) mock_find_ebuild.return_value = bootstrap_ebuild_path expected = (self.version_old, rust_ebuild_path, self.bootstrap_version) actual = rust_uprev.prepare_uprev( rust_version=self.version_new, template=None ) self.assertEqual(expected, actual) mock_command.assert_called_once_with(["equery", "w", "rust"]) mock_exists.assert_not_called() @mock.patch.object( rust_uprev, "get_rust_bootstrap_version", return_value=RustVersion(0, 41, 12), ) @mock.patch.object(os.path, "exists") @mock.patch.object(rust_uprev, "get_command_output") def test_return_none_with_ebuild_larger_than_input( self, mock_command, mock_exists, *_args ): mock_command.return_value = ( f"/path/to/rust/rust-{self.version_new}.ebuild" ) ret = rust_uprev.prepare_uprev( rust_version=self.version_old, template=None ) self.assertIsNone(ret) mock_exists.assert_not_called() def test_prepare_uprev_from_json(self): ebuild_path = "/path/to/the/ebuild" json_result = ( list(self.version_new), ebuild_path, list(self.bootstrap_version), ) expected = (self.version_new, ebuild_path, self.bootstrap_version) actual = rust_uprev.prepare_uprev_from_json(json_result) self.assertEqual(expected, actual) class UpdateBootstrapVersionTest(unittest.TestCase): """Tests for update_bootstrap_version step in rust_uprev""" ebuild_file_before = """ BOOTSTRAP_VERSION="1.2.0" """ ebuild_file_after = """ BOOTSTRAP_VERSION="1.3.6" """ def test_success(self): mock_open = mock.mock_open(read_data=self.ebuild_file_before) # ebuild_file and new bootstrap version are deliberately different ebuild_file = "/path/to/rust/cros-rustc.eclass" with mock.patch("builtins.open", mock_open): rust_uprev.update_bootstrap_version( ebuild_file, rust_uprev.RustVersion.parse("1.3.6") ) mock_open.return_value.__enter__().write.assert_called_once_with( self.ebuild_file_after ) def test_fail_when_ebuild_misses_a_variable(self): mock_open = mock.mock_open(read_data="") ebuild_file = "/path/to/rust/rust-1.3.5.ebuild" with mock.patch("builtins.open", mock_open): with self.assertRaises(RuntimeError) as context: rust_uprev.update_bootstrap_version( ebuild_file, rust_uprev.RustVersion.parse("1.2.0") ) self.assertEqual( "BOOTSTRAP_VERSION not found in /path/to/rust/rust-1.3.5.ebuild", str(context.exception), ) class UpdateManifestTest(unittest.TestCase): """Tests for update_manifest step in rust_uprev""" @mock.patch.object(rust_uprev, "ebuild_actions") def test_update_manifest(self, mock_run): ebuild_file = Path("/path/to/rust/rust-1.1.1.ebuild") rust_uprev.update_manifest(ebuild_file) mock_run.assert_called_once_with("rust", ["manifest"]) class UpdateBootstrapEbuildTest(unittest.TestCase): """Tests for rust_uprev.update_bootstrap_ebuild()""" def test_update_bootstrap_ebuild(self): # The update should do two things: # 1. Create a copy of rust-bootstrap's ebuild with the new version number. # 2. Add the old PV to RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE. with tempfile.TemporaryDirectory() as tmpdir_str, mock.patch.object( rust_uprev, "find_ebuild_path" ) as mock_find_ebuild: tmpdir = Path(tmpdir_str) bootstrapdir = Path.joinpath(tmpdir, "rust-bootstrap") bootstrapdir.mkdir() old_ebuild = bootstrapdir.joinpath("rust-bootstrap-1.45.2.ebuild") old_ebuild.write_text( encoding="utf-8", data=""" some text RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=( \t1.43.1 \t1.44.1 ) some more text """, ) mock_find_ebuild.return_value = old_ebuild rust_uprev.update_bootstrap_ebuild(rust_uprev.RustVersion(1, 46, 0)) new_ebuild = bootstrapdir.joinpath("rust-bootstrap-1.46.0.ebuild") self.assertTrue(new_ebuild.exists()) text = new_ebuild.read_text() self.assertEqual( text, """ some text RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=( \t1.43.1 \t1.44.1 \t1.45.2 ) some more text """, ) class UpdateRustPackagesTests(unittest.TestCase): """Tests for update_rust_packages step.""" def setUp(self): self.old_version = rust_uprev.RustVersion(1, 1, 0) self.current_version = rust_uprev.RustVersion(1, 2, 3) self.new_version = rust_uprev.RustVersion(1, 3, 5) self.ebuild_file = os.path.join( rust_uprev.RUST_PATH, "rust-{self.new_version}.ebuild" ) def test_add_new_rust_packages(self): package_before = ( f"dev-lang/rust-{self.old_version}\n" f"dev-lang/rust-{self.current_version}" ) package_after = ( f"dev-lang/rust-{self.old_version}\n" f"dev-lang/rust-{self.current_version}\n" f"dev-lang/rust-{self.new_version}" ) mock_open = mock.mock_open(read_data=package_before) with mock.patch("builtins.open", mock_open): rust_uprev.update_rust_packages( "dev-lang/rust", self.new_version, add=True ) mock_open.return_value.__enter__().write.assert_called_once_with( package_after ) def test_remove_old_rust_packages(self): package_before = ( f"dev-lang/rust-{self.old_version}\n" f"dev-lang/rust-{self.current_version}\n" f"dev-lang/rust-{self.new_version}" ) package_after = ( f"dev-lang/rust-{self.current_version}\n" f"dev-lang/rust-{self.new_version}" ) mock_open = mock.mock_open(read_data=package_before) with mock.patch("builtins.open", mock_open): rust_uprev.update_rust_packages( "dev-lang/rust", self.old_version, add=False ) mock_open.return_value.__enter__().write.assert_called_once_with( package_after ) class RustUprevOtherStagesTests(unittest.TestCase): """Tests for other steps in rust_uprev""" def setUp(self): self.old_version = rust_uprev.RustVersion(1, 1, 0) self.current_version = rust_uprev.RustVersion(1, 2, 3) self.new_version = rust_uprev.RustVersion(1, 3, 5) self.ebuild_file = os.path.join( rust_uprev.RUST_PATH, "rust-{self.new_version}.ebuild" ) @mock.patch.object(shutil, "copyfile") @mock.patch.object(os, "listdir") @mock.patch.object(subprocess, "check_call") def test_copy_patches(self, mock_call, mock_ls, mock_copy): mock_ls.return_value = [ f"rust-{self.old_version}-patch-1.patch", f"rust-{self.old_version}-patch-2-old.patch", f"rust-{self.current_version}-patch-1.patch", f"rust-{self.current_version}-patch-2-new.patch", ] rust_uprev.copy_patches( rust_uprev.RUST_PATH, self.current_version, self.new_version ) mock_copy.assert_has_calls( [ mock.call( os.path.join( rust_uprev.RUST_PATH, "files", f"rust-{self.current_version}-patch-1.patch", ), os.path.join( rust_uprev.RUST_PATH, "files", f"rust-{self.new_version}-patch-1.patch", ), ), mock.call( os.path.join( rust_uprev.RUST_PATH, "files", f"rust-{self.current_version}-patch-2-new.patch", ), os.path.join( rust_uprev.RUST_PATH, "files", f"rust-{self.new_version}-patch-2-new.patch", ), ), ] ) mock_call.assert_called_once_with( ["git", "add", f"rust-{self.new_version}-*.patch"], cwd=rust_uprev.RUST_PATH.joinpath("files"), ) @mock.patch.object(shutil, "copyfile") @mock.patch.object(subprocess, "check_call") def test_create_rust_ebuild(self, mock_call, mock_copy): template_ebuild = f"/path/to/rust-{self.current_version}-r2.ebuild" rust_uprev.create_ebuild( template_ebuild, "dev-lang/rust", self.new_version ) mock_copy.assert_called_once_with( template_ebuild, rust_uprev.RUST_PATH.joinpath(f"rust-{self.new_version}.ebuild"), ) mock_call.assert_called_once_with( ["git", "add", f"rust-{self.new_version}.ebuild"], cwd=rust_uprev.RUST_PATH, ) @mock.patch.object(shutil, "copyfile") @mock.patch.object(subprocess, "check_call") def test_create_rust_host_ebuild(self, mock_call, mock_copy): template_ebuild = f"/path/to/rust-host-{self.current_version}-r2.ebuild" rust_uprev.create_ebuild( template_ebuild, "dev-lang/rust-host", self.new_version ) mock_copy.assert_called_once_with( template_ebuild, rust_uprev.EBUILD_PREFIX.joinpath( f"dev-lang/rust-host/rust-host-{self.new_version}.ebuild" ), ) mock_call.assert_called_once_with( ["git", "add", f"rust-host-{self.new_version}.ebuild"], cwd=rust_uprev.EBUILD_PREFIX.joinpath("dev-lang/rust-host"), ) @mock.patch.object(rust_uprev, "find_ebuild_for_package") @mock.patch.object(subprocess, "check_call") def test_remove_rust_bootstrap_version(self, mock_call, *_args): bootstrap_path = os.path.join( rust_uprev.RUST_PATH, "..", "rust-bootstrap" ) rust_uprev.remove_rust_bootstrap_version( self.old_version, lambda *x: () ) mock_call.has_calls( [ [ "git", "rm", os.path.join( bootstrap_path, "files", f"rust-bootstrap-{self.old_version}-*.patch", ), ], [ "git", "rm", os.path.join( bootstrap_path, f"rust-bootstrap-{self.old_version}.ebuild", ), ], ] ) @mock.patch.object(subprocess, "check_call") def test_remove_virtual_rust(self, mock_call): with tempfile.TemporaryDirectory() as tmpdir: ebuild_path = Path( tmpdir, f"virtual/rust/rust-{self.old_version}.ebuild" ) os.makedirs(ebuild_path.parent) ebuild_path.touch() with mock.patch("rust_uprev.EBUILD_PREFIX", Path(tmpdir)): rust_uprev.remove_virtual_rust(self.old_version) mock_call.assert_called_once_with( ["git", "rm", str(ebuild_path.name)], cwd=ebuild_path.parent ) @mock.patch.object(subprocess, "check_call") def test_remove_virtual_rust_with_symlink(self, mock_call): with tempfile.TemporaryDirectory() as tmpdir: ebuild_path = Path( tmpdir, f"virtual/rust/rust-{self.old_version}.ebuild" ) symlink_path = Path( tmpdir, f"virtual/rust/rust-{self.old_version}-r14.ebuild" ) os.makedirs(ebuild_path.parent) ebuild_path.touch() symlink_path.symlink_to(ebuild_path.name) with mock.patch("rust_uprev.EBUILD_PREFIX", Path(tmpdir)): rust_uprev.remove_virtual_rust(self.old_version) mock_call.assert_has_calls( [ mock.call( ["git", "rm", ebuild_path.name], cwd=ebuild_path.parent, ), mock.call( ["git", "rm", symlink_path.name], cwd=ebuild_path.parent, ), ], any_order=True, ) @mock.patch.object(rust_uprev, "find_ebuild_path") @mock.patch.object(shutil, "copyfile") @mock.patch.object(subprocess, "check_call") def test_update_virtual_rust(self, mock_call, mock_copy, mock_find_ebuild): ebuild_path = Path( f"/some/dir/virtual/rust/rust-{self.current_version}.ebuild" ) mock_find_ebuild.return_value = Path(ebuild_path) rust_uprev.update_virtual_rust(self.current_version, self.new_version) mock_call.assert_called_once_with( ["git", "add", f"rust-{self.new_version}.ebuild"], cwd=ebuild_path.parent, ) mock_copy.assert_called_once_with( ebuild_path.parent.joinpath(f"rust-{self.current_version}.ebuild"), ebuild_path.parent.joinpath(f"rust-{self.new_version}.ebuild"), ) @mock.patch.object(os, "listdir") def test_find_oldest_rust_version_in_chroot_pass(self, mock_ls): oldest_version_name = f"rust-{self.old_version}.ebuild" mock_ls.return_value = [ oldest_version_name, f"rust-{self.current_version}.ebuild", f"rust-{self.new_version}.ebuild", ] actual = rust_uprev.find_oldest_rust_version_in_chroot() expected = self.old_version self.assertEqual(expected, actual) @mock.patch.object(os, "listdir") def test_find_oldest_rust_version_in_chroot_fail_with_only_one_ebuild( self, mock_ls ): mock_ls.return_value = [f"rust-{self.new_version}.ebuild"] with self.assertRaises(RuntimeError) as context: rust_uprev.find_oldest_rust_version_in_chroot() self.assertEqual( "Expect to find more than one Rust versions", str(context.exception) ) @mock.patch.object(rust_uprev, "get_command_output") @mock.patch.object(git, "CreateBranch") def test_create_new_repo(self, mock_branch, mock_output): mock_output.return_value = "" rust_uprev.create_new_repo(self.new_version) mock_branch.assert_called_once_with( rust_uprev.EBUILD_PREFIX, f"rust-to-{self.new_version}" ) @mock.patch.object(rust_uprev, "get_command_output") @mock.patch.object(subprocess, "check_call") def test_build_cross_compiler(self, mock_call, mock_output): mock_output.return_value = f"rust-{self.new_version}.ebuild" cros_targets = [ "x86_64-cros-linux-gnu", "armv7a-cros-linux-gnueabihf", "aarch64-cros-linux-gnu", ] all_triples = ["x86_64-pc-linux-gnu"] + cros_targets rust_ebuild = "RUSTC_TARGET_TRIPLES=(" + "\n\t".join(all_triples) + ")" mock_open = mock.mock_open(read_data=rust_ebuild) with mock.patch("builtins.open", mock_open): rust_uprev.build_cross_compiler() mock_call.assert_called_once_with( ["sudo", "emerge", "-j", "-G"] + [f"cross-{x}/gcc" for x in cros_targets + ["arm-none-eabi"]] ) if __name__ == "__main__": unittest.main()