Prepare v2023.10
[platform/kernel/u-boot.git] / tools / binman / bintool_test.py
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 u_boot_pylib import command
20 from u_boot_pylib import terminal
21 from u_boot_pylib import test_util
22 from u_boot_pylib 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
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
357 if __name__ == "__main__":
358     unittest.main()