Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 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 | |
| 8 | import collections |
| 9 | import os |
| 10 | import shutil |
| 11 | import tempfile |
| 12 | import unittest |
| 13 | import unittest.mock |
| 14 | import urllib.error |
| 15 | |
| 16 | from binman import bintool |
| 17 | from binman.bintool import Bintool |
| 18 | |
| 19 | from patman import command |
| 20 | from patman import terminal |
| 21 | from patman import test_util |
| 22 | from patman import tools |
| 23 | |
| 24 | # pylint: disable=R0904 |
| 25 | class 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 |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 83 | tools.download() |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 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() |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 91 | with unittest.mock.patch.object(tools, 'download', |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 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): |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 100 | """Take the tools.download() function by raising an exception""" |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 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): |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 117 | """Take the tools.download() function by raising an exception""" |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 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(_): |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 126 | """Take the tools.download() function by writing a file""" |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 127 | if self.seq: |
| 128 | raise urllib.error.URLError('not found') |
| 129 | self.seq += 1 |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 130 | tools.write_file(fname, expected) |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 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 | destdir = os.path.join(self._indir, 'dest_dir') |
| 138 | os.mkdir(destdir) |
| 139 | dest_fname = os.path.join(destdir, '_testing') |
| 140 | self.seq = 0 |
| 141 | |
| 142 | with unittest.mock.patch.object(bintool, 'DOWNLOAD_DESTDIR', destdir): |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 143 | with unittest.mock.patch.object(tools, 'download', |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 144 | side_effect=handle_download): |
| 145 | with test_util.capture_sys_output() as (stdout, _): |
| 146 | Bintool.fetch_tools(bintool.FETCH_ANY, ['_testing'] * 2) |
| 147 | self.assertTrue(os.path.exists(dest_fname)) |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 148 | data = tools.read_file(dest_fname) |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 149 | self.assertEqual(expected, data) |
| 150 | |
| 151 | lines = stdout.getvalue().splitlines() |
| 152 | self.assertTrue(len(lines) > 2) |
| 153 | self.assertEqual('Tools fetched: 1: _testing', lines[-2]) |
| 154 | self.assertEqual('Failures: 1: _testing', lines[-1]) |
| 155 | |
| 156 | def test_tool_list(self): |
| 157 | """Test listing available tools""" |
| 158 | self.assertGreater(len(Bintool.get_tool_list()), 3) |
| 159 | |
| 160 | def check_fetch_all(self, method): |
| 161 | """Helper to check the operation of fetching all tools""" |
| 162 | |
| 163 | # pylint: disable=W0613 |
| 164 | def fake_fetch(method, col, skip_present): |
| 165 | """Fakes the Binutils.fetch() function |
| 166 | |
| 167 | Returns FETCHED and FAIL on alternate calls |
| 168 | """ |
| 169 | self.seq += 1 |
| 170 | result = bintool.FETCHED if self.seq & 1 else bintool.FAIL |
| 171 | self.count[result] += 1 |
| 172 | return result |
| 173 | |
| 174 | self.seq = 0 |
| 175 | self.count = collections.defaultdict(int) |
| 176 | with unittest.mock.patch.object(bintool.Bintool, 'fetch_tool', |
| 177 | side_effect=fake_fetch): |
| 178 | with test_util.capture_sys_output() as (stdout, _): |
| 179 | Bintool.fetch_tools(method, ['all']) |
| 180 | lines = stdout.getvalue().splitlines() |
| 181 | self.assertIn(f'{self.count[bintool.FETCHED]}: ', lines[-2]) |
| 182 | self.assertIn(f'{self.count[bintool.FAIL]}: ', lines[-1]) |
| 183 | |
| 184 | def test_fetch_all(self): |
| 185 | """Test fetching all tools""" |
| 186 | self.check_fetch_all(bintool.FETCH_ANY) |
| 187 | |
| 188 | def test_fetch_all_specific(self): |
| 189 | """Test fetching all tools with a specific method""" |
| 190 | self.check_fetch_all(bintool.FETCH_BIN) |
| 191 | |
| 192 | def test_fetch_missing(self): |
| 193 | """Test fetching missing tools""" |
| 194 | # pylint: disable=W0613 |
| 195 | def fake_fetch2(method, col, skip_present): |
| 196 | """Fakes the Binutils.fetch() function |
| 197 | |
| 198 | Returns PRESENT only for the '_testing' bintool |
| 199 | """ |
| 200 | btool = list(self.btools.values())[self.seq] |
| 201 | self.seq += 1 |
| 202 | print('fetch', btool.name) |
| 203 | if btool.name == '_testing': |
| 204 | return bintool.PRESENT |
| 205 | return bintool.FETCHED |
| 206 | |
| 207 | # Preload a list of tools to return when get_tool_list() and create() |
| 208 | # are called |
| 209 | all_tools = Bintool.get_tool_list(True) |
| 210 | self.btools = collections.OrderedDict() |
| 211 | for name in all_tools: |
| 212 | self.btools[name] = Bintool.create(name) |
| 213 | self.seq = 0 |
| 214 | with unittest.mock.patch.object(bintool.Bintool, 'fetch_tool', |
| 215 | side_effect=fake_fetch2): |
| 216 | with unittest.mock.patch.object(bintool.Bintool, |
| 217 | 'get_tool_list', |
| 218 | side_effect=[all_tools]): |
| 219 | with unittest.mock.patch.object(bintool.Bintool, 'create', |
| 220 | side_effect=self.btools.values()): |
| 221 | with test_util.capture_sys_output() as (stdout, _): |
| 222 | Bintool.fetch_tools(bintool.FETCH_ANY, ['missing']) |
| 223 | lines = stdout.getvalue().splitlines() |
| 224 | num_tools = len(self.btools) |
| 225 | fetched = [line for line in lines if 'Tools fetched:' in line].pop() |
| 226 | present = [line for line in lines if 'Already present:' in line].pop() |
| 227 | self.assertIn(f'{num_tools - 1}: ', fetched) |
| 228 | self.assertIn('1: ', present) |
| 229 | |
| 230 | def check_build_method(self, write_file): |
| 231 | """Check the output from fetching using the BUILD method |
| 232 | |
| 233 | Args: |
| 234 | write_file (bool): True to write the output file when 'make' is |
| 235 | called |
| 236 | |
| 237 | Returns: |
| 238 | tuple: |
| 239 | str: Filename of written file (or missing 'make' output) |
| 240 | str: Contents of stdout |
| 241 | """ |
| 242 | def fake_run(*cmd): |
| 243 | if cmd[0] == 'make': |
| 244 | # See Bintool.build_from_git() |
| 245 | tmpdir = cmd[2] |
| 246 | self.fname = os.path.join(tmpdir, 'pathname') |
| 247 | if write_file: |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 248 | tools.write_file(self.fname, b'hello') |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 249 | |
| 250 | btest = Bintool.create('_testing') |
| 251 | col = terminal.Color() |
| 252 | self.fname = None |
| 253 | with unittest.mock.patch.object(bintool, 'DOWNLOAD_DESTDIR', |
| 254 | self._indir): |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 255 | with unittest.mock.patch.object(tools, 'run', side_effect=fake_run): |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 256 | with test_util.capture_sys_output() as (stdout, _): |
| 257 | btest.fetch_tool(bintool.FETCH_BUILD, col, False) |
| 258 | fname = os.path.join(self._indir, '_testing') |
| 259 | return fname if write_file else self.fname, stdout.getvalue() |
| 260 | |
| 261 | def test_build_method(self): |
| 262 | """Test fetching using the build method""" |
| 263 | fname, stdout = self.check_build_method(write_file=True) |
| 264 | self.assertTrue(os.path.exists(fname)) |
| 265 | self.assertIn(f"writing to '{fname}", stdout) |
| 266 | |
| 267 | def test_build_method_fail(self): |
| 268 | """Test fetching using the build method when no file is produced""" |
| 269 | fname, stdout = self.check_build_method(write_file=False) |
| 270 | self.assertFalse(os.path.exists(fname)) |
| 271 | self.assertIn(f"File '{fname}' was not produced", stdout) |
| 272 | |
| 273 | def test_install(self): |
| 274 | """Test fetching using the install method""" |
| 275 | btest = Bintool.create('_testing') |
| 276 | btest.install = True |
| 277 | col = terminal.Color() |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 278 | with unittest.mock.patch.object(tools, 'run', return_value=None): |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 279 | with test_util.capture_sys_output() as _: |
| 280 | result = btest.fetch_tool(bintool.FETCH_BIN, col, False) |
| 281 | self.assertEqual(bintool.FETCHED, result) |
| 282 | |
| 283 | def test_no_fetch(self): |
| 284 | """Test fetching when there is no method""" |
| 285 | btest = Bintool.create('_testing') |
| 286 | btest.disable = True |
| 287 | col = terminal.Color() |
| 288 | with test_util.capture_sys_output() as _: |
| 289 | result = btest.fetch_tool(bintool.FETCH_BIN, col, False) |
| 290 | self.assertEqual(bintool.FAIL, result) |
| 291 | |
| 292 | def test_all_bintools(self): |
| 293 | """Test that all bintools can handle all available fetch types""" |
| 294 | def handle_download(_): |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 295 | """Take the tools.download() function by writing a file""" |
| 296 | tools.write_file(fname, expected) |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 297 | return fname, dirname |
| 298 | |
| 299 | def fake_run(*cmd): |
| 300 | if cmd[0] == 'make': |
| 301 | # See Bintool.build_from_git() |
| 302 | tmpdir = cmd[2] |
| 303 | self.fname = os.path.join(tmpdir, 'pathname') |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 304 | tools.write_file(self.fname, b'hello') |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 305 | |
| 306 | expected = b'this is a test' |
| 307 | dirname = os.path.join(self._indir, 'download_dir') |
| 308 | os.mkdir(dirname) |
| 309 | fname = os.path.join(dirname, 'downloaded') |
| 310 | |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 311 | with unittest.mock.patch.object(tools, 'run', side_effect=fake_run): |
| 312 | with unittest.mock.patch.object(tools, 'download', |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 313 | side_effect=handle_download): |
| 314 | with test_util.capture_sys_output() as _: |
| 315 | for name in Bintool.get_tool_list(): |
| 316 | btool = Bintool.create(name) |
| 317 | for method in range(bintool.FETCH_COUNT): |
| 318 | result = btool.fetch(method) |
| 319 | self.assertTrue(result is not False) |
| 320 | if result is not True and result is not None: |
| 321 | result_fname, _ = result |
| 322 | self.assertTrue(os.path.exists(result_fname)) |
Simon Glass | c1aa66e | 2022-01-29 14:14:04 -0700 | [diff] [blame] | 323 | data = tools.read_file(result_fname) |
Simon Glass | 3b47dfa | 2022-01-09 20:13:51 -0700 | [diff] [blame] | 324 | self.assertEqual(expected, data) |
| 325 | os.remove(result_fname) |
| 326 | |
| 327 | def test_all_bintool_versions(self): |
| 328 | """Test handling of bintool version when it cannot be run""" |
| 329 | all_tools = Bintool.get_tool_list() |
| 330 | for name in all_tools: |
| 331 | btool = Bintool.create(name) |
| 332 | with unittest.mock.patch.object( |
| 333 | btool, 'run_cmd_result', return_value=command.CommandResult()): |
| 334 | self.assertEqual('unknown', btool.version()) |
| 335 | |
| 336 | def test_force_missing(self): |
| 337 | btool = Bintool.create('_testing') |
| 338 | btool.present = True |
| 339 | self.assertTrue(btool.is_present()) |
| 340 | |
| 341 | btool.present = None |
| 342 | Bintool.set_missing_list(['_testing']) |
| 343 | self.assertFalse(btool.is_present()) |
| 344 | |
| 345 | def test_failed_command(self): |
| 346 | """Check that running a command that does not exist returns None""" |
| 347 | btool = Bintool.create('_testing') |
| 348 | result = btool.run_cmd_result('fred') |
| 349 | self.assertIsNone(result) |
| 350 | |
| 351 | |
| 352 | if __name__ == "__main__": |
| 353 | unittest.main() |