1# SPDX-License-Identifier: GPL-2.0+ 2# Copyright 2022 Google LLC 3# Written by Simon Glass <sjg@chromium.org> 4# 5 6"""Tests for the Bintool class""" 7 8import collections 9import os 10import shutil 11import tempfile 12import unittest 13import unittest.mock 14import urllib.error 15 16from binman import bintool 17from binman.bintool import Bintool 18 19from u_boot_pylib import command 20from u_boot_pylib import terminal 21from u_boot_pylib import test_util 22from u_boot_pylib import tools 23 24# pylint: disable=R0904 25class TestBintool(unittest.TestCase): 26 """Tests for the Bintool class""" 27 def setUp(self): 28 # Create a temporary directory for test files 29 self._indir = tempfile.mkdtemp(prefix='bintool.') 30 self.seq = None 31 self.count = None 32 self.fname = None 33 self.btools = None 34 35 def tearDown(self): 36 """Remove the temporary input directory and its contents""" 37 if self._indir: 38 shutil.rmtree(self._indir) 39 self._indir = None 40 41 def test_missing_btype(self): 42 """Test that unknown bintool types are detected""" 43 with self.assertRaises(ValueError) as exc: 44 Bintool.create('missing') 45 self.assertIn("No module named 'binman.btool.missing'", 46 str(exc.exception)) 47 48 def test_fresh_bintool(self): 49 """Check that the _testing bintool is not cached""" 50 btest = Bintool.create('_testing') 51 btest.present = True 52 btest2 = Bintool.create('_testing') 53 self.assertFalse(btest2.present) 54 55 def test_version(self): 56 """Check handling of a tool being present or absent""" 57 btest = Bintool.create('_testing') 58 with test_util.capture_sys_output() as (stdout, _): 59 btest.show() 60 self.assertFalse(btest.is_present()) 61 self.assertIn('-', stdout.getvalue()) 62 btest.present = True 63 self.assertTrue(btest.is_present()) 64 self.assertEqual('123', btest.version()) 65 with test_util.capture_sys_output() as (stdout, _): 66 btest.show() 67 self.assertIn('123', stdout.getvalue()) 68 69 def test_fetch_present(self): 70 """Test fetching of a tool""" 71 btest = Bintool.create('_testing') 72 btest.present = True 73 col = terminal.Color() 74 self.assertEqual(bintool.PRESENT, 75 btest.fetch_tool(bintool.FETCH_ANY, col, True)) 76 77 @classmethod 78 def check_fetch_url(cls, fake_download, method): 79 """Check the output from fetching a tool 80 81 Args: 82 fake_download (function): Function to call instead of 83 tools.download() 84 method (bintool.FETCH_...: Fetch method to use 85 86 Returns: 87 str: Contents of stdout 88 """ 89 btest = Bintool.create('_testing') 90 col = terminal.Color() 91 with unittest.mock.patch.object(tools, 'download', 92 side_effect=fake_download): 93 with test_util.capture_sys_output() as (stdout, _): 94 btest.fetch_tool(method, col, False) 95 return stdout.getvalue() 96 97 def test_fetch_url_err(self): 98 """Test an error while fetching a tool from a URL""" 99 def fail_download(url): 100 """Take the tools.download() function by raising an exception""" 101 raise urllib.error.URLError('my error') 102 103 stdout = self.check_fetch_url(fail_download, bintool.FETCH_ANY) 104 self.assertIn('my error', stdout) 105 106 def test_fetch_url_exception(self): 107 """Test an exception while fetching a tool from a URL""" 108 def cause_exc(url): 109 raise ValueError('exc error') 110 111 stdout = self.check_fetch_url(cause_exc, bintool.FETCH_ANY) 112 self.assertIn('exc error', stdout) 113 114 def test_fetch_method(self): 115 """Test fetching using a particular method""" 116 def fail_download(url): 117 """Take the tools.download() function by raising an exception""" 118 raise urllib.error.URLError('my error') 119 120 stdout = self.check_fetch_url(fail_download, bintool.FETCH_BIN) 121 self.assertIn('my error', stdout) 122 123 def test_fetch_pass_fail(self): 124 """Test fetching multiple tools with some passing and some failing""" 125 def handle_download(_): 126 """Take the tools.download() function by writing a file""" 127 if self.seq: 128 raise urllib.error.URLError('not found') 129 self.seq += 1 130 tools.write_file(fname, expected) 131 return fname, dirname 132 133 expected = b'this is a test' 134 dirname = os.path.join(self._indir, 'download_dir') 135 os.mkdir(dirname) 136 fname = os.path.join(dirname, 'downloaded') 137 138 # Rely on bintool to create this directory 139 destdir = os.path.join(self._indir, 'dest_dir') 140 141 dest_fname = os.path.join(destdir, '_testing') 142 self.seq = 0 143 144 with unittest.mock.patch.object(bintool.Bintool, 'tooldir', destdir): 145 with unittest.mock.patch.object(tools, 'download', 146 side_effect=handle_download): 147 with test_util.capture_sys_output() as (stdout, _): 148 Bintool.fetch_tools(bintool.FETCH_ANY, ['_testing'] * 2) 149 self.assertTrue(os.path.exists(dest_fname)) 150 data = tools.read_file(dest_fname) 151 self.assertEqual(expected, data) 152 153 lines = stdout.getvalue().splitlines() 154 self.assertTrue(len(lines) > 2) 155 self.assertEqual('Tools fetched: 1: _testing', lines[-2]) 156 self.assertEqual('Failures: 1: _testing', lines[-1]) 157 158 def test_tool_list(self): 159 """Test listing available tools""" 160 self.assertGreater(len(Bintool.get_tool_list()), 3) 161 162 def check_fetch_all(self, method): 163 """Helper to check the operation of fetching all tools""" 164 165 # pylint: disable=W0613 166 def fake_fetch(method, col, skip_present): 167 """Fakes the Binutils.fetch() function 168 169 Returns FETCHED and FAIL on alternate calls 170 """ 171 self.seq += 1 172 result = bintool.FETCHED if self.seq & 1 else bintool.FAIL 173 self.count[result] += 1 174 return result 175 176 self.seq = 0 177 self.count = collections.defaultdict(int) 178 with unittest.mock.patch.object(bintool.Bintool, 'fetch_tool', 179 side_effect=fake_fetch): 180 with test_util.capture_sys_output() as (stdout, _): 181 Bintool.fetch_tools(method, ['all']) 182 lines = stdout.getvalue().splitlines() 183 self.assertIn(f'{self.count[bintool.FETCHED]}: ', lines[-2]) 184 self.assertIn(f'{self.count[bintool.FAIL]}: ', lines[-1]) 185 186 def test_fetch_all(self): 187 """Test fetching all tools""" 188 self.check_fetch_all(bintool.FETCH_ANY) 189 190 def test_fetch_all_specific(self): 191 """Test fetching all tools with a specific method""" 192 self.check_fetch_all(bintool.FETCH_BIN) 193 194 def test_fetch_missing(self): 195 """Test fetching missing tools""" 196 # pylint: disable=W0613 197 def fake_fetch2(method, col, skip_present): 198 """Fakes the Binutils.fetch() function 199 200 Returns PRESENT only for the '_testing' bintool 201 """ 202 btool = list(self.btools.values())[self.seq] 203 self.seq += 1 204 print('fetch', btool.name) 205 if btool.name == '_testing': 206 return bintool.PRESENT 207 return bintool.FETCHED 208 209 # Preload a list of tools to return when get_tool_list() and create() 210 # are called 211 all_tools = Bintool.get_tool_list(True) 212 self.btools = collections.OrderedDict() 213 for name in all_tools: 214 self.btools[name] = Bintool.create(name) 215 self.seq = 0 216 with unittest.mock.patch.object(bintool.Bintool, 'fetch_tool', 217 side_effect=fake_fetch2): 218 with unittest.mock.patch.object(bintool.Bintool, 219 'get_tool_list', 220 side_effect=[all_tools]): 221 with unittest.mock.patch.object(bintool.Bintool, 'create', 222 side_effect=self.btools.values()): 223 with test_util.capture_sys_output() as (stdout, _): 224 Bintool.fetch_tools(bintool.FETCH_ANY, ['missing']) 225 lines = stdout.getvalue().splitlines() 226 num_tools = len(self.btools) 227 fetched = [line for line in lines if 'Tools fetched:' in line].pop() 228 present = [line for line in lines if 'Already present:' in line].pop() 229 self.assertIn(f'{num_tools - 1}: ', fetched) 230 self.assertIn('1: ', present) 231 232 def check_build_method(self, write_file): 233 """Check the output from fetching using the BUILD method 234 235 Args: 236 write_file (bool): True to write the output file when 'make' is 237 called 238 239 Returns: 240 tuple: 241 str: Filename of written file (or missing 'make' output) 242 str: Contents of stdout 243 """ 244 def fake_run(*cmd): 245 if cmd[0] == 'make': 246 # See Bintool.build_from_git() 247 tmpdir = cmd[2] 248 self.fname = os.path.join(tmpdir, 'pathname') 249 if write_file: 250 tools.write_file(self.fname, b'hello') 251 252 btest = Bintool.create('_testing') 253 col = terminal.Color() 254 self.fname = None 255 with unittest.mock.patch.object(bintool.Bintool, 'tooldir', 256 self._indir): 257 with unittest.mock.patch.object(tools, 'run', side_effect=fake_run): 258 with test_util.capture_sys_output() as (stdout, _): 259 btest.fetch_tool(bintool.FETCH_BUILD, col, False) 260 fname = os.path.join(self._indir, '_testing') 261 return fname if write_file else self.fname, stdout.getvalue() 262 263 def test_build_method(self): 264 """Test fetching using the build method""" 265 fname, stdout = self.check_build_method(write_file=True) 266 self.assertTrue(os.path.exists(fname)) 267 self.assertIn(f"writing to '{fname}", stdout) 268 269 def test_build_method_fail(self): 270 """Test fetching using the build method when no file is produced""" 271 fname, stdout = self.check_build_method(write_file=False) 272 self.assertFalse(os.path.exists(fname)) 273 self.assertIn(f"File '{fname}' was not produced", stdout) 274 275 def test_install(self): 276 """Test fetching using the install method""" 277 btest = Bintool.create('_testing') 278 btest.install = True 279 col = terminal.Color() 280 with unittest.mock.patch.object(tools, 'run', return_value=None): 281 with test_util.capture_sys_output() as _: 282 result = btest.fetch_tool(bintool.FETCH_BIN, col, False) 283 self.assertEqual(bintool.FETCHED, result) 284 285 def test_no_fetch(self): 286 """Test fetching when there is no method""" 287 btest = Bintool.create('_testing') 288 btest.disable = True 289 col = terminal.Color() 290 with test_util.capture_sys_output() as _: 291 result = btest.fetch_tool(bintool.FETCH_BIN, col, False) 292 self.assertEqual(bintool.FAIL, result) 293 294 def test_all_bintools(self): 295 """Test that all bintools can handle all available fetch types""" 296 def handle_download(_): 297 """Take the tools.download() function by writing a file""" 298 tools.write_file(fname, expected) 299 return fname, dirname 300 301 def fake_run(*cmd): 302 if cmd[0] == 'make': 303 # See Bintool.build_from_git() 304 tmpdir = cmd[2] 305 self.fname = os.path.join(tmpdir, 'pathname') 306 tools.write_file(self.fname, b'hello') 307 308 expected = b'this is a test' 309 dirname = os.path.join(self._indir, 'download_dir') 310 os.mkdir(dirname) 311 fname = os.path.join(dirname, 'downloaded') 312 313 with unittest.mock.patch.object(tools, 'run', side_effect=fake_run): 314 with unittest.mock.patch.object(tools, 'download', 315 side_effect=handle_download): 316 with test_util.capture_sys_output() as _: 317 for name in Bintool.get_tool_list(): 318 btool = Bintool.create(name) 319 for method in range(bintool.FETCH_COUNT): 320 result = btool.fetch(method) 321 self.assertTrue(result is not False) 322 if result is not True and result is not None: 323 result_fname, _ = result 324 self.assertTrue(os.path.exists(result_fname)) 325 data = tools.read_file(result_fname) 326 self.assertEqual(expected, data) 327 os.remove(result_fname) 328 329 def test_all_bintool_versions(self): 330 """Test handling of bintool version when it cannot be run""" 331 all_tools = Bintool.get_tool_list() 332 for name in all_tools: 333 btool = Bintool.create(name) 334 with unittest.mock.patch.object( 335 btool, 'run_cmd_result', return_value=command.CommandResult()): 336 self.assertEqual('unknown', btool.version()) 337 338 def test_force_missing(self): 339 btool = Bintool.create('_testing') 340 btool.present = True 341 self.assertTrue(btool.is_present()) 342 343 btool.present = None 344 Bintool.set_missing_list(['_testing']) 345 self.assertFalse(btool.is_present()) 346 347 def test_failed_command(self): 348 """Check that running a command that does not exist returns None""" 349 destdir = os.path.join(self._indir, 'dest_dir') 350 os.mkdir(destdir) 351 with unittest.mock.patch.object(bintool.Bintool, 'tooldir', destdir): 352 btool = Bintool.create('_testing') 353 result = btool.run_cmd_result('fred') 354 self.assertIsNone(result) 355 356 357if __name__ == "__main__": 358 unittest.main() 359