From d8845f46c5613ec0fd9f982e5de9982dc7ed5f79 Mon Sep 17 00:00:00 2001 From: Salud Lemus Date: Wed, 24 Jul 2019 13:04:20 -0700 Subject: LLVM tools: Unittests for llvm_patch_management.py BUG=None TEST='./llvm_patch_management_unittest.py' passes Change-Id: I03cb727669039c9f89431ceec34c32d5d940777b Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/1717250 Reviewed-by: George Burgess Reviewed-by: Manoj Gupta Tested-by: Salud Lemus --- llvm_tools/llvm_patch_management_unittest.py | 437 +++++++++++++++++++++++++++ llvm_tools/test_helpers.py | 71 +++++ 2 files changed, 508 insertions(+) create mode 100755 llvm_tools/llvm_patch_management_unittest.py create mode 100644 llvm_tools/test_helpers.py diff --git a/llvm_tools/llvm_patch_management_unittest.py b/llvm_tools/llvm_patch_management_unittest.py new file mode 100755 index 00000000..54f34463 --- /dev/null +++ b/llvm_tools/llvm_patch_management_unittest.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +# Copyright 2019 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Unit tests when creating the arguments for the patch manager.""" + +from __future__ import print_function + +from collections import namedtuple +import mock +import os +import unittest + +from cros_utils import command_executer +from failure_modes import FailureModes +from test_helpers import CallCountsToMockFunctions +import llvm_patch_management +import patch_manager + + +class LlvmPatchManagementTest(unittest.TestCase): + """Test class when constructing the arguments for the patch manager.""" + + def testInvalidChrootPathWhenGetPathToFilesDir(self): + # Verify the exception is raised when an invalid absolute path to the chroot + # is passed in. + with self.assertRaises(ValueError) as err: + llvm_patch_management.GetPathToFilesDirectory('/some/path/to/chroot', + 'sys-devel/llvm') + + self.assertEqual(err.exception.message, + 'Invalid chroot provided: /some/path/to/chroot') + + # Simulate the behavior of 'os.path.isdir()' when a valid chroot path is + # passed in. + @mock.patch.object(os.path, 'isdir', return_value=True) + @mock.patch.object(command_executer.CommandExecuter, + 'ChrootRunCommandWOutput') + def testFailedToGetChrootPathToEbuildWhenGetPathToFilesDir( + self, mock_chroot_cmd, mock_isdir): + + # Simulate behavior of 'ChrootRunCommandWOutput()' when failed to get the + # absolute chroot path to the package's ebuild. + # + # Returns shell error code, stdout, stderr. + mock_chroot_cmd.return_value = (1, None, 'Invalid package provided.') + + # Verify the exception is raised when failed to get the absolute chroot + # path to a package's ebuild. + with self.assertRaises(ValueError) as err: + llvm_patch_management.GetPathToFilesDirectory('/some/path/to/chroot', + 'test/package') + + self.assertEqual( + err.exception.message, + 'Failed to get the absolute chroot path of the package ' + 'test/package: Invalid package provided.') + + mock_chroot_cmd.assert_called_once_with( + chromeos_root='/some/path/to/chroot', + command='equery w test/package', + print_to_console=False) + + mock_isdir.assert_called_once() + + # Simulate the behavior of 'os.path.isdir()' when a valid chroot path is + # passed in. + @mock.patch.object(os.path, 'isdir', return_value=True) + @mock.patch.object(command_executer.CommandExecuter, + 'ChrootRunCommandWOutput') + @mock.patch.object(llvm_patch_management, '_GetRelativePathOfChrootPath') + def testSuccessfullyGetPathToFilesDir( + self, mock_get_relative_path_of_chroot_path, mock_chroot_cmd, mock_isdir): + + # Simulate behavior of 'ChrootRunCommandWOutput()' when successfully + # retrieved the absolute chroot path to the package's ebuild. + # + # Returns shell error code, stdout, stderr. + mock_chroot_cmd.return_value = (0, + '/mnt/host/source/path/to/llvm/llvm.ebuild', + 0) + + # Simulate behavior of '_GetRelativePathOfChrootPath()' when successfully + # removed '/mnt/host/source' of the absolute chroot path to the package's + # ebuild. + # + # Returns relative path after '/mnt/host/source/'. + mock_get_relative_path_of_chroot_path.return_value = 'path/to/llvm' + + self.assertEqual( + llvm_patch_management.GetPathToFilesDirectory('/some/path/to/chroot', + 'sys-devel/llvm'), + '/some/path/to/chroot/path/to/llvm/files/') + + mock_isdir.assert_called_once() + + mock_chroot_cmd.assert_called_once() + + mock_get_relative_path_of_chroot_path.assert_called_once_with( + '/mnt/host/source/path/to/llvm') + + def testInvalidPrefixForChrootPath(self): + # Verify the exception is raised when the chroot path does not start with + # '/mnt/host/source/'. + with self.assertRaises(ValueError) as err: + llvm_patch_management._GetRelativePathOfChrootPath('/path/to/llvm') + + self.assertEqual(err.exception.message, + 'Invalid prefix for the chroot path: /path/to/llvm') + + def testValidPrefixForChrootPath(self): + self.assertEqual( + llvm_patch_management._GetRelativePathOfChrootPath( + '/mnt/host/source/path/to/llvm'), 'path/to/llvm') + + # Simulate behavior of 'os.path.isfile()' when the patch metadata file does + # not exist. + @mock.patch.object(os.path, 'isfile', return_value=False) + def testInvalidFileForPatchMetadataPath(self, mock_isfile): + # Verify the exception is raised when the absolute path to the patch + # metadata file does not exist. + with self.assertRaises(ValueError) as err: + llvm_patch_management._CheckPatchMetadataPath( + '/abs/path/to/files/test.json') + + self.assertEqual(err.exception.message, + 'Invalid file provided: /abs/path/to/files/test.json') + + mock_isfile.assert_called_once() + + # Simulate behavior of 'os.path.isfile()' when the absolute path to the + # patch metadata file exists. + @mock.patch.object(os.path, 'isfile', return_value=True) + def testPatchMetadataFileDoesNotEndInJson(self, mock_isfile): + # Verify the exception is raised when the patch metadata file does not end + # in '.json'. + with self.assertRaises(ValueError) as err: + llvm_patch_management._CheckPatchMetadataPath( + '/abs/path/to/files/PATCHES') + + self.assertEqual( + err.exception.message, 'File does not end in \'.json\': ' + '/abs/path/to/files/PATCHES') + + mock_isfile.assert_called_once() + + @mock.patch.object(os.path, 'isfile') + def testValidPatchMetadataFile(self, mock_isfile): + # Simulate behavior of 'os.path.isfile()' when the absolute path to the + # patch metadata file exists. + mock_isfile.return_value = True + + llvm_patch_management._CheckPatchMetadataPath( + '/abs/path/to/files/PATCHES.json') + + mock_isfile.assert_called_once() + + @mock.patch.object(command_executer.CommandExecuter, + 'ChrootRunCommandWOutput') + def testFailedToUnpackPackage(self, mock_chroot_cmd): + # Simulate the behavior of 'ChrootRunCommandWOutput()' when unpacking fails + # on a package. + @CallCountsToMockFunctions + def MultipleCallsToGetSrcPath(call_count, chromeos_root, command, + print_to_console): + + # First call to 'ChrootRunCommandWOutput()' which would successfully + # get the ebuild path of the package. + if call_count == 0: + # Returns shell error code, stdout, stderr. + return 0, '/mount/host/source/path/to/package/test-r1.ebuild', 0 + + # Second call to 'ChrootRunCommandWOutput()' which failed to unpack the + # package. + if call_count == 1: + # Returns shell error code, stdout, stderr. + return 1, None, 'Invalid package provided.' + + # 'ChrootRunCommandWOutput()' was called more times than expected (2 + # times). + assert False, ('Unexpectedly called more than 2 times.') + + # Use test function to simulate 'ChrootRunCommandWOutput()' behavior. + mock_chroot_cmd.side_effect = MultipleCallsToGetSrcPath + + # Verify the exception is raised when failed to unpack a package. + with self.assertRaises(ValueError) as err: + llvm_patch_management.UnpackLLVMPackage('/some/path/to/chroot', + 'test/package') + + self.assertEqual( + err.exception.message, 'Failed to unpack the package test/package: ' + 'Invalid package provided.') + + self.assertEqual(mock_chroot_cmd.call_count, 2) + + @mock.patch.object(command_executer.CommandExecuter, + 'ChrootRunCommandWOutput') + def testFailedToGetChrootPathToEbuild(self, mock_chroot_cmd): + # Simulate the behavior of 'ChrootRunCommandWOutput()' when failed to get + # the absolute chroot path to the package's ebuild. + mock_chroot_cmd.return_value = (1, None, 'Invalid package provided.') + + # Verify the exception is raised when failed to get the absolute chroot + # path to the package's ebuild. + with self.assertRaises(ValueError) as err: + llvm_patch_management.UnpackLLVMPackage('/some/path/to/chroot', + 'test/package') + + self.assertEqual( + err.exception.message, + 'Failed to get the absolute chroot path to the ebuild of ' + 'test/package: Invalid package provided.') + + mock_chroot_cmd.assert_called_once_with( + chromeos_root='/some/path/to/chroot', + command='equery w test/package', + print_to_console=False) + + @mock.patch.object(command_executer.CommandExecuter, + 'ChrootRunCommandWOutput') + @mock.patch.object(llvm_patch_management, '_ConstructPathToSources') + def testSuccessfullyGetSrcPath(self, mock_construct_src_path, + mock_chroot_cmd): + + # Simulate the behavior of 'ChrootRunCommandWOutput()' when successfully + # get the absolute chroot ebuild path to the package and successfully + # unpacked the package. + @CallCountsToMockFunctions + def MultipleCallsToGetSrcPath(call_count, chromeos_root, command, + print_to_console): + + # First call to 'ChrootRunCommandWOutput()' which would successfully + # get the absolute chroot path to the package's ebuild. + if call_count == 0: + # Returns shell error code, stdout, stderr. + return 0, '/mount/host/source/path/to/package/test-r1.ebuild', 0 + + # Second call to 'ChrootRunCommandWOutput()' which would successfully + # unpack the package. + if call_count == 1: + # Returns shell error code, stdout, stderr. + return 0, None, 0 + + # 'ChrootRunCommandWOutput()' was called more times than expected (2 + # times). + assert False, ('Unexpectedly called more than 2 times.') + + # Use the test function to simulate 'ChrootRunCommandWOutput()' behavior. + mock_chroot_cmd.side_effect = MultipleCallsToGetSrcPath + + # Simulate the behavior of '_ConstructPathToSources()' when the ebuild name + # has a revision number and '.ebuild' and the absolute path to the src + # directory is valid. + mock_construct_src_path.return_value = ('/some/path/to/chroot/chroot/var' + '/tmp/portage/to/test-r1/work/' + 'test') + + self.assertEqual( + llvm_patch_management.UnpackLLVMPackage('/some/path/to/chroot', + 'package/test'), + '/some/path/to/chroot/chroot/var/tmp/portage/to/test-r1/work/test') + + self.assertEqual(mock_chroot_cmd.call_count, 2) + + mock_construct_src_path.assert_called_once_with('/some/path/to/chroot', + 'test-r1.ebuild', 'to') + + def testFailedToRemoveEbuildPartFromTheEbuildName(self): + # Verify the exception is raised when the ebuild name with the revision + # number does not have '.ebuild' in the name. + # + # Ex: llvm-9.0_pre361749_p20190714-r4 + # + # Does not have a '.ebuild' in the ebuild name. + with self.assertRaises(ValueError) as err: + llvm_patch_management._ConstructPathToSources('/some/path/to/chroot', + 'test-r1', 'test-packages') + + self.assertEqual(err.exception.message, + 'Failed to remove \'.ebuild\' from test-r1.') + + def testFailedToRemoveTheRevisionNumberFromTheEbuildName(self): + # Verify the exception is raised when the ebuild name with the revision + # number does not have the revision number in the name. + # + # Ex: llvm-9.0_pre361749_p20190714.ebuild + # + # Does not have a revision number in the ebuild name. + with self.assertRaises(ValueError) as err: + llvm_patch_management._ConstructPathToSources( + '/some/path/to/chroot', 'test.ebuild', 'test-packages') + + self.assertEqual(err.exception.message, + 'Failed to remove the revision number from test.') + + # Simulate behavior of 'os.path.isdir()' when the constructed absolute path to + # the unpacked sources does not exist. + @mock.patch.object(os.path, 'isdir', return_value=False) + def testInvalidPathToUnpackedSources(self, mock_isdir): + # Verify the exception is raised when the absolute path to the unpacked + # sources is constructed, but the path is invalid. + with self.assertRaises(ValueError) as err: + llvm_patch_management._ConstructPathToSources( + '/some/path/to/chroot', 'test-r1.ebuild', 'test-packages') + + self.assertEqual( + err.exception.message, + 'Failed to construct the absolute path to the unpacked ' + 'sources of the package test: ' + '/some/path/to/chroot/chroot/var/tmp/portage/test-packages' + '/test-r1/work/test') + + mock_isdir.assert_called_once() + + # Simulate the behavior of 'os.path.isdir()' when the absolute path to the + # src directory exists. + @mock.patch.object(os.path, 'isdir', return_value=True) + def testSuccessfullyConstructedSrcPath(self, mock_isdir): + self.assertEqual( + llvm_patch_management._ConstructPathToSources( + '/some/path/to/chroot', 'test-r1.ebuild', 'test-packages'), + '/some/path/to/chroot/chroot/var/tmp/portage/test-packages/' + 'test-r1/work/test') + + mock_isdir.assert_called_once() + + @mock.patch.object(llvm_patch_management, 'GetPathToFilesDirectory') + @mock.patch.object(llvm_patch_management, '_CheckPatchMetadataPath') + def testExceptionIsRaisedWhenUpdatingAPackagesMetadataFile( + self, mock_check_patch_metadata_path, mock_get_filesdir_path): + + # Simulate the behavior of '_CheckPatchMetadataPath()' when the patch + # metadata file in $FILESDIR does not exist or does not end in '.json'. + def InvalidPatchMetadataFile(patch_metadata_path): + self.assertEqual(patch_metadata_path, + '/some/path/to/chroot/some/path/to/filesdir/PATCHES') + + raise ValueError('File does not end in \'.json\': ' + '/some/path/to/chroot/some/path/to/filesdir/PATCHES') + + # Use the test function to simulate behavior of '_CheckPatchMetadataPath()'. + mock_check_patch_metadata_path.side_effect = InvalidPatchMetadataFile + + # Simulate the behavior of 'GetPathToFilesDirectory()' when successfully + # constructed the absolute path to $FILESDIR of a package. + mock_get_filesdir_path.return_value = ('/some/path/to/chroot/some/path/' + 'to/filesdir') + + # Verify the exception is raised when a package is constructing the + # arguments for the patch manager to update its patch metadata file and an + # exception is raised in the process. + with self.assertRaises(ValueError) as err: + llvm_patch_management.UpdatePackagesPatchMetadataFile( + '/some/path/to/chroot', 1000, 'PATCHES', ['test-packages/package1'], + FailureModes.FAIL) + + self.assertEqual( + err.exception.message, 'File does not end in \'.json\': ' + '/some/path/to/chroot/some/path/to/filesdir/PATCHES') + + mock_get_filesdir_path.assert_called_once_with('/some/path/to/chroot', + 'test-packages/package1') + + mock_check_patch_metadata_path.assert_called_once() + + @mock.patch.object(llvm_patch_management, 'GetPathToFilesDirectory') + @mock.patch.object(llvm_patch_management, '_CheckPatchMetadataPath') + @mock.patch.object(llvm_patch_management, 'UnpackLLVMPackage') + @mock.patch.object(patch_manager, 'HandlePatches') + def testSuccessfullyRetrievedPatchResults( + self, mock_handle_patches, mock_unpack_package, + mock_check_patch_metadata_path, mock_get_filesdir_path): + + # Simulate the behavior of 'GetPathToFilesDirectory()' when successfully + # constructed the absolute path to $FILESDIR of a package. + mock_get_filesdir_path.return_value = ('/some/path/to/chroot/some/path/' + 'to/filesdir') + + # Simulate the behavior of 'UnpackLLVMPackage()' when successfully unpacked + # the package and constructed the absolute path to the unpacked sources. + mock_unpack_package.return_value = ('/some/path/to/chroot/chroot/var/tmp/' + 'portage/test-packages/package2-r1/work' + '/package2') + + PatchInfo = namedtuple('PatchInfo', [ + 'applied_patches', 'failed_patches', 'non_applicable_patches', + 'disabled_patches', 'removed_patches', 'modified_metadata' + ]) + + # Simulate the behavior of 'HandlePatches()' when successfully iterated + # through every patch in the patch metadata file and a dictionary is + # returned that contains information about the patches' status. + mock_handle_patches.return_value = PatchInfo( + applied_patches=['fixes_something.patch'], + failed_patches=['disables_output.patch'], + non_applicable_patches=[], + disabled_patches=[], + removed_patches=[], + modified_metadata=None) + + expected_patch_results = { + 'applied_patches': ['fixes_something.patch'], + 'failed_patches': ['disables_output.patch'], + 'non_applicable_patches': [], + 'disabled_patches': [], + 'removed_patches': [], + 'modified_metadata': None + } + + patch_info = llvm_patch_management.UpdatePackagesPatchMetadataFile( + '/some/path/to/chroot', 1000, 'PATCHES.json', + ['test-packages/package2'], FailureModes.CONTINUE) + + self.assertDictEqual(patch_info, + {'test-packages/package2': expected_patch_results}) + + mock_get_filesdir_path.assert_called_once_with('/some/path/to/chroot', + 'test-packages/package2') + + mock_check_patch_metadata_path.assert_called_once_with( + '/some/path/to/chroot/some/path/to/filesdir/PATCHES.json') + + mock_unpack_package.assert_called_once_with('/some/path/to/chroot', + 'test-packages/package2') + + mock_handle_patches.assert_called_once_with( + 1000, '/some/path/to/chroot/some/path/to/filesdir/PATCHES.json', + '/some/path/to/chroot/some/path/to/filesdir', + '/some/path/to/chroot/chroot/var/tmp/portage/test-packages/' + 'package2-r1/work/package2', FailureModes.CONTINUE) + + +if __name__ == '__main__': + unittest.main() diff --git a/llvm_tools/test_helpers.py b/llvm_tools/test_helpers.py new file mode 100644 index 00000000..7f1beafb --- /dev/null +++ b/llvm_tools/test_helpers.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Helper functions for unit testing.""" + +from __future__ import print_function + +from contextlib import contextmanager +from tempfile import mkstemp +import json +import os + + +# FIXME: Migrate modules with similar helper to use this module. +def CallCountsToMockFunctions(mock_function): + """A decorator that passes a call count to the function it decorates. + + Examples: + @CallCountsToMockFunctions + def foo(call_count): + return call_count + ... + ... + [foo(), foo(), foo()] + [0, 1, 2] + + NOTE: This decorator will not handle recursive functions properly. + """ + + counter = [0] + + def Result(*args, **kwargs): + ret_value = mock_function(counter[0], *args, **kwargs) + counter[0] += 1 + return ret_value + + return Result + + +def WritePrettyJsonFile(file_name, json_object): + """Writes the contents of the file to the json object. + + Args: + file_name: The file that has contents to be used for the json object. + json_object: The json object to write to. + """ + + json.dump(file_name, json_object, indent=4, separators=(',', ': ')) + + +@contextmanager +def CreateTemporaryJsonFile(): + """Makes a temporary .json file.""" + + # Create a temporary file to simulate a .json file. + fd, temp_file_path = mkstemp() + + temp_json_file = '%s.json' % temp_file_path + + os.close(fd) + os.remove(temp_file_path) + + try: + yield temp_json_file + + finally: + # Make sure that the file was created. + if os.path.isfile(temp_json_file): + os.remove(temp_json_file) -- cgit v1.2.3