354 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			354 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
| # SPDX-License-Identifier: GPL-2.0+
 | |
| # Copyright 2022 Google LLC
 | |
| # Written by Simon Glass <sjg@chromium.org>
 | |
| #
 | |
| 
 | |
| """Tests for the Bintool class"""
 | |
| 
 | |
| import collections
 | |
| import os
 | |
| import shutil
 | |
| import tempfile
 | |
| import unittest
 | |
| import unittest.mock
 | |
| import urllib.error
 | |
| 
 | |
| from binman import bintool
 | |
| from binman.bintool import Bintool
 | |
| 
 | |
| from patman import command
 | |
| from patman import terminal
 | |
| from patman import test_util
 | |
| from patman import tools
 | |
| 
 | |
| # pylint: disable=R0904
 | |
| class TestBintool(unittest.TestCase):
 | |
|     """Tests for the Bintool class"""
 | |
|     def setUp(self):
 | |
|         # Create a temporary directory for test files
 | |
|         self._indir = tempfile.mkdtemp(prefix='bintool.')
 | |
|         self.seq = None
 | |
|         self.count = None
 | |
|         self.fname = None
 | |
|         self.btools = None
 | |
| 
 | |
|     def tearDown(self):
 | |
|         """Remove the temporary input directory and its contents"""
 | |
|         if self._indir:
 | |
|             shutil.rmtree(self._indir)
 | |
|         self._indir = None
 | |
| 
 | |
|     def test_missing_btype(self):
 | |
|         """Test that unknown bintool types are detected"""
 | |
|         with self.assertRaises(ValueError) as exc:
 | |
|             Bintool.create('missing')
 | |
|         self.assertIn("No module named 'binman.btool.missing'",
 | |
|                       str(exc.exception))
 | |
| 
 | |
|     def test_fresh_bintool(self):
 | |
|         """Check that the _testing bintool is not cached"""
 | |
|         btest = Bintool.create('_testing')
 | |
|         btest.present = True
 | |
|         btest2 = Bintool.create('_testing')
 | |
|         self.assertFalse(btest2.present)
 | |
| 
 | |
|     def test_version(self):
 | |
|         """Check handling of a tool being present or absent"""
 | |
|         btest = Bintool.create('_testing')
 | |
|         with test_util.capture_sys_output() as (stdout, _):
 | |
|             btest.show()
 | |
|         self.assertFalse(btest.is_present())
 | |
|         self.assertIn('-', stdout.getvalue())
 | |
|         btest.present = True
 | |
|         self.assertTrue(btest.is_present())
 | |
|         self.assertEqual('123', btest.version())
 | |
|         with test_util.capture_sys_output() as (stdout, _):
 | |
|             btest.show()
 | |
|         self.assertIn('123', stdout.getvalue())
 | |
| 
 | |
|     def test_fetch_present(self):
 | |
|         """Test fetching of a tool"""
 | |
|         btest = Bintool.create('_testing')
 | |
|         btest.present = True
 | |
|         col = terminal.Color()
 | |
|         self.assertEqual(bintool.PRESENT,
 | |
|                          btest.fetch_tool(bintool.FETCH_ANY, col, True))
 | |
| 
 | |
|     @classmethod
 | |
|     def check_fetch_url(cls, fake_download, method):
 | |
|         """Check the output from fetching a tool
 | |
| 
 | |
|         Args:
 | |
|             fake_download (function): Function to call instead of
 | |
|                 tools.download()
 | |
|             method (bintool.FETCH_...: Fetch method to use
 | |
| 
 | |
|         Returns:
 | |
|             str: Contents of stdout
 | |
|         """
 | |
|         btest = Bintool.create('_testing')
 | |
|         col = terminal.Color()
 | |
|         with unittest.mock.patch.object(tools, 'download',
 | |
|                                         side_effect=fake_download):
 | |
|             with test_util.capture_sys_output() as (stdout, _):
 | |
|                 btest.fetch_tool(method, col, False)
 | |
|         return stdout.getvalue()
 | |
| 
 | |
|     def test_fetch_url_err(self):
 | |
|         """Test an error while fetching a tool from a URL"""
 | |
|         def fail_download(url):
 | |
|             """Take the tools.download() function by raising an exception"""
 | |
|             raise urllib.error.URLError('my error')
 | |
| 
 | |
|         stdout = self.check_fetch_url(fail_download, bintool.FETCH_ANY)
 | |
|         self.assertIn('my error', stdout)
 | |
| 
 | |
|     def test_fetch_url_exception(self):
 | |
|         """Test an exception while fetching a tool from a URL"""
 | |
|         def cause_exc(url):
 | |
|             raise ValueError('exc error')
 | |
| 
 | |
|         stdout = self.check_fetch_url(cause_exc, bintool.FETCH_ANY)
 | |
|         self.assertIn('exc error', stdout)
 | |
| 
 | |
|     def test_fetch_method(self):
 | |
|         """Test fetching using a particular method"""
 | |
|         def fail_download(url):
 | |
|             """Take the tools.download() function by raising an exception"""
 | |
|             raise urllib.error.URLError('my error')
 | |
| 
 | |
|         stdout = self.check_fetch_url(fail_download, bintool.FETCH_BIN)
 | |
|         self.assertIn('my error', stdout)
 | |
| 
 | |
|     def test_fetch_pass_fail(self):
 | |
|         """Test fetching multiple tools with some passing and some failing"""
 | |
|         def handle_download(_):
 | |
|             """Take the tools.download() function by writing a file"""
 | |
|             if self.seq:
 | |
|                 raise urllib.error.URLError('not found')
 | |
|             self.seq += 1
 | |
|             tools.write_file(fname, expected)
 | |
|             return fname, dirname
 | |
| 
 | |
|         expected = b'this is a test'
 | |
|         dirname = os.path.join(self._indir, 'download_dir')
 | |
|         os.mkdir(dirname)
 | |
|         fname = os.path.join(dirname, 'downloaded')
 | |
|         destdir = os.path.join(self._indir, 'dest_dir')
 | |
|         os.mkdir(destdir)
 | |
|         dest_fname = os.path.join(destdir, '_testing')
 | |
|         self.seq = 0
 | |
| 
 | |
|         with unittest.mock.patch.object(bintool, 'DOWNLOAD_DESTDIR', destdir):
 | |
|             with unittest.mock.patch.object(tools, 'download',
 | |
|                                             side_effect=handle_download):
 | |
|                 with test_util.capture_sys_output() as (stdout, _):
 | |
|                     Bintool.fetch_tools(bintool.FETCH_ANY, ['_testing'] * 2)
 | |
|         self.assertTrue(os.path.exists(dest_fname))
 | |
|         data = tools.read_file(dest_fname)
 | |
|         self.assertEqual(expected, data)
 | |
| 
 | |
|         lines = stdout.getvalue().splitlines()
 | |
|         self.assertTrue(len(lines) > 2)
 | |
|         self.assertEqual('Tools fetched:    1: _testing', lines[-2])
 | |
|         self.assertEqual('Failures:         1: _testing', lines[-1])
 | |
| 
 | |
|     def test_tool_list(self):
 | |
|         """Test listing available tools"""
 | |
|         self.assertGreater(len(Bintool.get_tool_list()), 3)
 | |
| 
 | |
|     def check_fetch_all(self, method):
 | |
|         """Helper to check the operation of fetching all tools"""
 | |
| 
 | |
|         # pylint: disable=W0613
 | |
|         def fake_fetch(method, col, skip_present):
 | |
|             """Fakes the Binutils.fetch() function
 | |
| 
 | |
|             Returns FETCHED and FAIL on alternate calls
 | |
|             """
 | |
|             self.seq += 1
 | |
|             result = bintool.FETCHED if self.seq & 1 else bintool.FAIL
 | |
|             self.count[result] += 1
 | |
|             return result
 | |
| 
 | |
|         self.seq = 0
 | |
|         self.count = collections.defaultdict(int)
 | |
|         with unittest.mock.patch.object(bintool.Bintool, 'fetch_tool',
 | |
|                                         side_effect=fake_fetch):
 | |
|             with test_util.capture_sys_output() as (stdout, _):
 | |
|                 Bintool.fetch_tools(method, ['all'])
 | |
|         lines = stdout.getvalue().splitlines()
 | |
|         self.assertIn(f'{self.count[bintool.FETCHED]}: ', lines[-2])
 | |
|         self.assertIn(f'{self.count[bintool.FAIL]}: ', lines[-1])
 | |
| 
 | |
|     def test_fetch_all(self):
 | |
|         """Test fetching all tools"""
 | |
|         self.check_fetch_all(bintool.FETCH_ANY)
 | |
| 
 | |
|     def test_fetch_all_specific(self):
 | |
|         """Test fetching all tools with a specific method"""
 | |
|         self.check_fetch_all(bintool.FETCH_BIN)
 | |
| 
 | |
|     def test_fetch_missing(self):
 | |
|         """Test fetching missing tools"""
 | |
|         # pylint: disable=W0613
 | |
|         def fake_fetch2(method, col, skip_present):
 | |
|             """Fakes the Binutils.fetch() function
 | |
| 
 | |
|             Returns PRESENT only for the '_testing' bintool
 | |
|             """
 | |
|             btool = list(self.btools.values())[self.seq]
 | |
|             self.seq += 1
 | |
|             print('fetch', btool.name)
 | |
|             if btool.name == '_testing':
 | |
|                 return bintool.PRESENT
 | |
|             return bintool.FETCHED
 | |
| 
 | |
|         # Preload a list of tools to return when get_tool_list() and create()
 | |
|         # are called
 | |
|         all_tools = Bintool.get_tool_list(True)
 | |
|         self.btools = collections.OrderedDict()
 | |
|         for name in all_tools:
 | |
|             self.btools[name] = Bintool.create(name)
 | |
|         self.seq = 0
 | |
|         with unittest.mock.patch.object(bintool.Bintool, 'fetch_tool',
 | |
|                                         side_effect=fake_fetch2):
 | |
|             with unittest.mock.patch.object(bintool.Bintool,
 | |
|                                             'get_tool_list',
 | |
|                                             side_effect=[all_tools]):
 | |
|                 with unittest.mock.patch.object(bintool.Bintool, 'create',
 | |
|                                                 side_effect=self.btools.values()):
 | |
|                     with test_util.capture_sys_output() as (stdout, _):
 | |
|                         Bintool.fetch_tools(bintool.FETCH_ANY, ['missing'])
 | |
|         lines = stdout.getvalue().splitlines()
 | |
|         num_tools = len(self.btools)
 | |
|         fetched = [line for line in lines if 'Tools fetched:' in line].pop()
 | |
|         present = [line for line in lines if 'Already present:' in line].pop()
 | |
|         self.assertIn(f'{num_tools - 1}: ', fetched)
 | |
|         self.assertIn('1: ', present)
 | |
| 
 | |
|     def check_build_method(self, write_file):
 | |
|         """Check the output from fetching using the BUILD method
 | |
| 
 | |
|         Args:
 | |
|             write_file (bool): True to write the output file when 'make' is
 | |
|                 called
 | |
| 
 | |
|         Returns:
 | |
|             tuple:
 | |
|                 str: Filename of written file (or missing 'make' output)
 | |
|                 str: Contents of stdout
 | |
|         """
 | |
|         def fake_run(*cmd):
 | |
|             if cmd[0] == 'make':
 | |
|                 # See Bintool.build_from_git()
 | |
|                 tmpdir = cmd[2]
 | |
|                 self.fname = os.path.join(tmpdir, 'pathname')
 | |
|                 if write_file:
 | |
|                     tools.write_file(self.fname, b'hello')
 | |
| 
 | |
|         btest = Bintool.create('_testing')
 | |
|         col = terminal.Color()
 | |
|         self.fname = None
 | |
|         with unittest.mock.patch.object(bintool, 'DOWNLOAD_DESTDIR',
 | |
|                                         self._indir):
 | |
|             with unittest.mock.patch.object(tools, 'run', side_effect=fake_run):
 | |
|                 with test_util.capture_sys_output() as (stdout, _):
 | |
|                     btest.fetch_tool(bintool.FETCH_BUILD, col, False)
 | |
|         fname = os.path.join(self._indir, '_testing')
 | |
|         return fname if write_file else self.fname, stdout.getvalue()
 | |
| 
 | |
|     def test_build_method(self):
 | |
|         """Test fetching using the build method"""
 | |
|         fname, stdout = self.check_build_method(write_file=True)
 | |
|         self.assertTrue(os.path.exists(fname))
 | |
|         self.assertIn(f"writing to '{fname}", stdout)
 | |
| 
 | |
|     def test_build_method_fail(self):
 | |
|         """Test fetching using the build method when no file is produced"""
 | |
|         fname, stdout = self.check_build_method(write_file=False)
 | |
|         self.assertFalse(os.path.exists(fname))
 | |
|         self.assertIn(f"File '{fname}' was not produced", stdout)
 | |
| 
 | |
|     def test_install(self):
 | |
|         """Test fetching using the install method"""
 | |
|         btest = Bintool.create('_testing')
 | |
|         btest.install = True
 | |
|         col = terminal.Color()
 | |
|         with unittest.mock.patch.object(tools, 'run', return_value=None):
 | |
|             with test_util.capture_sys_output() as _:
 | |
|                 result = btest.fetch_tool(bintool.FETCH_BIN, col, False)
 | |
|         self.assertEqual(bintool.FETCHED, result)
 | |
| 
 | |
|     def test_no_fetch(self):
 | |
|         """Test fetching when there is no method"""
 | |
|         btest = Bintool.create('_testing')
 | |
|         btest.disable = True
 | |
|         col = terminal.Color()
 | |
|         with test_util.capture_sys_output() as _:
 | |
|             result = btest.fetch_tool(bintool.FETCH_BIN, col, False)
 | |
|         self.assertEqual(bintool.FAIL, result)
 | |
| 
 | |
|     def test_all_bintools(self):
 | |
|         """Test that all bintools can handle all available fetch types"""
 | |
|         def handle_download(_):
 | |
|             """Take the tools.download() function by writing a file"""
 | |
|             tools.write_file(fname, expected)
 | |
|             return fname, dirname
 | |
| 
 | |
|         def fake_run(*cmd):
 | |
|             if cmd[0] == 'make':
 | |
|                 # See Bintool.build_from_git()
 | |
|                 tmpdir = cmd[2]
 | |
|                 self.fname = os.path.join(tmpdir, 'pathname')
 | |
|                 tools.write_file(self.fname, b'hello')
 | |
| 
 | |
|         expected = b'this is a test'
 | |
|         dirname = os.path.join(self._indir, 'download_dir')
 | |
|         os.mkdir(dirname)
 | |
|         fname = os.path.join(dirname, 'downloaded')
 | |
| 
 | |
|         with unittest.mock.patch.object(tools, 'run', side_effect=fake_run):
 | |
|             with unittest.mock.patch.object(tools, 'download',
 | |
|                                             side_effect=handle_download):
 | |
|                 with test_util.capture_sys_output() as _:
 | |
|                     for name in Bintool.get_tool_list():
 | |
|                         btool = Bintool.create(name)
 | |
|                         for method in range(bintool.FETCH_COUNT):
 | |
|                             result = btool.fetch(method)
 | |
|                             self.assertTrue(result is not False)
 | |
|                             if result is not True and result is not None:
 | |
|                                 result_fname, _ = result
 | |
|                                 self.assertTrue(os.path.exists(result_fname))
 | |
|                                 data = tools.read_file(result_fname)
 | |
|                                 self.assertEqual(expected, data)
 | |
|                                 os.remove(result_fname)
 | |
| 
 | |
|     def test_all_bintool_versions(self):
 | |
|         """Test handling of bintool version when it cannot be run"""
 | |
|         all_tools = Bintool.get_tool_list()
 | |
|         for name in all_tools:
 | |
|             btool = Bintool.create(name)
 | |
|             with unittest.mock.patch.object(
 | |
|                 btool, 'run_cmd_result', return_value=command.CommandResult()):
 | |
|                 self.assertEqual('unknown', btool.version())
 | |
| 
 | |
|     def test_force_missing(self):
 | |
|         btool = Bintool.create('_testing')
 | |
|         btool.present = True
 | |
|         self.assertTrue(btool.is_present())
 | |
| 
 | |
|         btool.present = None
 | |
|         Bintool.set_missing_list(['_testing'])
 | |
|         self.assertFalse(btool.is_present())
 | |
| 
 | |
|     def test_failed_command(self):
 | |
|         """Check that running a command that does not exist returns None"""
 | |
|         btool = Bintool.create('_testing')
 | |
|         result = btool.run_cmd_result('fred')
 | |
|         self.assertIsNone(result)
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     unittest.main()
 |