blob: f9b16d4c73b555bc0aa3fe1badd1ad48c8047546 [file] [log] [blame]
Simon Glass3b47dfa2022-01-09 20:13:51 -07001# 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
Simon Glass4583c002023-02-23 18:18:04 -070019from u_boot_pylib import command
20from u_boot_pylib import terminal
21from u_boot_pylib import test_util
22from u_boot_pylib import tools
Simon Glass3b47dfa2022-01-09 20:13:51 -070023
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
Simon Glassc1aa66e2022-01-29 14:14:04 -070083 tools.download()
Simon Glass3b47dfa2022-01-09 20:13:51 -070084 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 Glassc1aa66e2022-01-29 14:14:04 -070091 with unittest.mock.patch.object(tools, 'download',
Simon Glass3b47dfa2022-01-09 20:13:51 -070092 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 Glassc1aa66e2022-01-29 14:14:04 -0700100 """Take the tools.download() function by raising an exception"""
Simon Glass3b47dfa2022-01-09 20:13:51 -0700101 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 Glassc1aa66e2022-01-29 14:14:04 -0700117 """Take the tools.download() function by raising an exception"""
Simon Glass3b47dfa2022-01-09 20:13:51 -0700118 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 Glassc1aa66e2022-01-29 14:14:04 -0700126 """Take the tools.download() function by writing a file"""
Simon Glass3b47dfa2022-01-09 20:13:51 -0700127 if self.seq:
128 raise urllib.error.URLError('not found')
129 self.seq += 1
Simon Glassc1aa66e2022-01-29 14:14:04 -0700130 tools.write_file(fname, expected)
Simon Glass3b47dfa2022-01-09 20:13:51 -0700131 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')
Simon Glassfe7e9242023-02-22 12:14:49 -0700137
138 # Rely on bintool to create this directory
Simon Glass3b47dfa2022-01-09 20:13:51 -0700139 destdir = os.path.join(self._indir, 'dest_dir')
Simon Glassfe7e9242023-02-22 12:14:49 -0700140
Simon Glass3b47dfa2022-01-09 20:13:51 -0700141 dest_fname = os.path.join(destdir, '_testing')
142 self.seq = 0
143
Simon Glass00f674d2023-02-22 12:14:47 -0700144 with unittest.mock.patch.object(bintool.Bintool, 'tooldir', destdir):
Simon Glassc1aa66e2022-01-29 14:14:04 -0700145 with unittest.mock.patch.object(tools, 'download',
Simon Glass3b47dfa2022-01-09 20:13:51 -0700146 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))
Simon Glassc1aa66e2022-01-29 14:14:04 -0700150 data = tools.read_file(dest_fname)
Simon Glass3b47dfa2022-01-09 20:13:51 -0700151 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:
Simon Glassc1aa66e2022-01-29 14:14:04 -0700250 tools.write_file(self.fname, b'hello')
Simon Glass3b47dfa2022-01-09 20:13:51 -0700251
252 btest = Bintool.create('_testing')
253 col = terminal.Color()
254 self.fname = None
Simon Glass00f674d2023-02-22 12:14:47 -0700255 with unittest.mock.patch.object(bintool.Bintool, 'tooldir',
Simon Glass3b47dfa2022-01-09 20:13:51 -0700256 self._indir):
Simon Glassc1aa66e2022-01-29 14:14:04 -0700257 with unittest.mock.patch.object(tools, 'run', side_effect=fake_run):
Simon Glass3b47dfa2022-01-09 20:13:51 -0700258 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()
Simon Glassc1aa66e2022-01-29 14:14:04 -0700280 with unittest.mock.patch.object(tools, 'run', return_value=None):
Simon Glass3b47dfa2022-01-09 20:13:51 -0700281 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(_):
Simon Glassc1aa66e2022-01-29 14:14:04 -0700297 """Take the tools.download() function by writing a file"""
298 tools.write_file(fname, expected)
Simon Glass3b47dfa2022-01-09 20:13:51 -0700299 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')
Simon Glassc1aa66e2022-01-29 14:14:04 -0700306 tools.write_file(self.fname, b'hello')
Simon Glass3b47dfa2022-01-09 20:13:51 -0700307
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
Simon Glassc1aa66e2022-01-29 14:14:04 -0700313 with unittest.mock.patch.object(tools, 'run', side_effect=fake_run):
314 with unittest.mock.patch.object(tools, 'download',
Simon Glass3b47dfa2022-01-09 20:13:51 -0700315 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))
Simon Glassc1aa66e2022-01-29 14:14:04 -0700325 data = tools.read_file(result_fname)
Simon Glass3b47dfa2022-01-09 20:13:51 -0700326 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"""
Simon Glassfe7e9242023-02-22 12:14:49 -0700349 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')
Simon Glass3b47dfa2022-01-09 20:13:51 -0700354 self.assertIsNone(result)
355
356
357if __name__ == "__main__":
358 unittest.main()