093e8d0678710304659a59663e07cfc32d46fe0e
[platform/kernel/u-boot.git] / test / py / tests / test_dfu.py
1 # Copyright (c) 2016, NVIDIA CORPORATION. All rights reserved.
2 #
3 # SPDX-License-Identifier: GPL-2.0
4
5 # Test U-Boot's "dfu" command. The test starts DFU in U-Boot, waits for USB
6 # device enumeration on the host, executes dfu-util multiple times to test
7 # various transfer sizes, many of which trigger USB driver edge cases, and
8 # finally aborts the "dfu" command in U-Boot.
9
10 import os
11 import os.path
12 import pytest
13 import u_boot_utils
14
15 """
16 Note: This test relies on:
17
18 a) boardenv_* to contain configuration values to define which USB ports are
19 available for testing. Without this, this test will be automatically skipped.
20 For example:
21
22 env__usb_dev_ports = (
23     {
24         "fixture_id": "micro_b",
25         "tgt_usb_ctlr": "0",
26         "host_usb_dev_node": "/dev/usbdev-p2371-2180",
27         # This parameter is optional /if/ you only have a single board
28         # attached to your host at a time.
29         "host_usb_port_path": "3-13",
30     },
31 )
32
33 env__dfu_configs = (
34     # eMMC, partition 1
35     {
36         "fixture_id": "emmc",
37         "alt_info": "/dfu_test.bin ext4 0 1;/dfu_dummy.bin ext4 0 1",
38         "cmd_params": "mmc 0",
39         # This value is optional.
40         # If present, it specified the set of transfer sizes tested.
41         # If missing, a default list of sizes will be used, which covers
42         #   various useful corner cases.
43         # Manually specifying test sizes is useful if you wish to test 4 DFU
44         # configurations, but don't want to test every single transfer size
45         # on each, to avoid bloating the overall time taken by testing.
46         "test_sizes": (63, 64, 65),
47     },
48 )
49
50 b) udev rules to set permissions on devices nodes, so that sudo is not
51 required. For example:
52
53 ACTION=="add", SUBSYSTEM=="block", SUBSYSTEMS=="usb", KERNELS=="3-13", MODE:="666"
54
55 (You may wish to change the group ID instead of setting the permissions wide
56 open. All that matters is that the user ID running the test can access the
57 device.)
58 """
59
60 # The set of file sizes to test. These values trigger various edge-cases such
61 # as one less than, equal to, and one greater than typical USB max packet
62 # sizes, and similar boundary conditions.
63 test_sizes_default = (
64     64 - 1,
65     64,
66     64 + 1,
67     128 - 1,
68     128,
69     128 + 1,
70     960 - 1,
71     960,
72     960 + 1,
73     4096 - 1,
74     4096,
75     4096 + 1,
76     1024 * 1024 - 1,
77     1024 * 1024,
78     8 * 1024 * 1024,
79 )
80
81 first_usb_dev_port = None
82
83 @pytest.mark.buildconfigspec('cmd_dfu')
84 def test_dfu(u_boot_console, env__usb_dev_port, env__dfu_config):
85     """Test the "dfu" command; the host system must be able to enumerate a USB
86     device when "dfu" is running, various DFU transfers are tested, and the
87     USB device must disappear when "dfu" is aborted.
88
89     Args:
90         u_boot_console: A U-Boot console connection.
91         env__usb_dev_port: The single USB device-mode port specification on
92             which to run the test. See the file-level comment above for
93             details of the format.
94         env__dfu_config: The single DFU (memory region) configuration on which
95             to run the test. See the file-level comment above for details
96             of the format.
97
98     Returns:
99         Nothing.
100     """
101
102     def start_dfu():
103         """Start U-Boot's dfu shell command.
104
105         This also waits for the host-side USB enumeration process to complete.
106
107         Args:
108             None.
109
110         Returns:
111             Nothing.
112         """
113
114         fh = u_boot_utils.attempt_to_open_file(
115             env__usb_dev_port['host_usb_dev_node'])
116         if fh:
117             fh.close()
118             raise Exception('USB device present before dfu command invoked')
119
120         u_boot_console.log.action(
121             'Starting long-running U-Boot dfu shell command')
122
123         cmd = 'setenv dfu_alt_info "%s"' % env__dfu_config['alt_info']
124         u_boot_console.run_command(cmd)
125
126         cmd = 'dfu 0 ' + env__dfu_config['cmd_params']
127         u_boot_console.run_command(cmd, wait_for_prompt=False)
128         u_boot_console.log.action('Waiting for DFU USB device to appear')
129         fh = u_boot_utils.wait_until_open_succeeds(
130             env__usb_dev_port['host_usb_dev_node'])
131         fh.close()
132
133     def stop_dfu(ignore_errors):
134         """Stop U-Boot's dfu shell command from executing.
135
136         This also waits for the host-side USB de-enumeration process to
137         complete.
138
139         Args:
140             ignore_errors: Ignore any errors. This is useful if an error has
141                 already been detected, and the code is performing best-effort
142                 cleanup. In this case, we do not want to mask the original
143                 error by "honoring" any new errors.
144
145         Returns:
146             Nothing.
147         """
148
149         try:
150             u_boot_console.log.action(
151                 'Stopping long-running U-Boot dfu shell command')
152             u_boot_console.ctrlc()
153             u_boot_console.log.action(
154                 'Waiting for DFU USB device to disappear')
155             u_boot_utils.wait_until_file_open_fails(
156                 env__usb_dev_port['host_usb_dev_node'], ignore_errors)
157         except:
158             if not ignore_errors:
159                 raise
160
161     def run_dfu_util(alt_setting, fn, up_dn_load_arg):
162         """Invoke dfu-util on the host.
163
164         Args:
165             alt_setting: The DFU "alternate setting" identifier to interact
166                 with.
167             fn: The host-side file name to transfer.
168             up_dn_load_arg: '-U' or '-D' depending on whether a DFU upload or
169                 download operation should be performed.
170
171         Returns:
172             Nothing.
173         """
174
175         cmd = ['dfu-util', '-a', str(alt_setting), up_dn_load_arg, fn]
176         if 'host_usb_port_path' in env__usb_dev_port:
177             cmd += ['-p', env__usb_dev_port['host_usb_port_path']]
178         u_boot_utils.run_and_log(u_boot_console, cmd)
179         u_boot_console.wait_for('Ctrl+C to exit ...')
180
181     def dfu_write(alt_setting, fn):
182         """Write a file to the target board using DFU.
183
184         Args:
185             alt_setting: The DFU "alternate setting" identifier to interact
186                 with.
187             fn: The host-side file name to transfer.
188
189         Returns:
190             Nothing.
191         """
192
193         run_dfu_util(alt_setting, fn, '-D')
194
195     def dfu_read(alt_setting, fn):
196         """Read a file from the target board using DFU.
197
198         Args:
199             alt_setting: The DFU "alternate setting" identifier to interact
200                 with.
201             fn: The host-side file name to transfer.
202
203         Returns:
204             Nothing.
205         """
206
207         # dfu-util fails reads/uploads if the host file already exists
208         if os.path.exists(fn):
209             os.remove(fn)
210         run_dfu_util(alt_setting, fn, '-U')
211
212     def dfu_write_read_check(size):
213         """Test DFU transfers of a specific size of data
214
215         This function first writes data to the board then reads it back and
216         compares the written and read back data. Measures are taken to avoid
217         certain types of false positives.
218
219         Args:
220             size: The data size to test.
221
222         Returns:
223             Nothing.
224         """
225
226         test_f = u_boot_utils.PersistentRandomFile(u_boot_console,
227             'dfu_%d.bin' % size, size)
228         readback_fn = u_boot_console.config.result_dir + '/dfu_readback.bin'
229
230         u_boot_console.log.action('Writing test data to DFU primary ' +
231             'altsetting')
232         dfu_write(0, test_f.abs_fn)
233
234         u_boot_console.log.action('Writing dummy data to DFU secondary ' +
235             'altsetting to clear DFU buffers')
236         dfu_write(1, dummy_f.abs_fn)
237
238         u_boot_console.log.action('Reading DFU primary altsetting for ' +
239             'comparison')
240         dfu_read(0, readback_fn)
241
242         u_boot_console.log.action('Comparing written and read data')
243         written_hash = test_f.content_hash
244         read_back_hash = u_boot_utils.md5sum_file(readback_fn, size)
245         assert(written_hash == read_back_hash)
246
247     # This test may be executed against multiple USB ports. The test takes a
248     # long time, so we don't want to do the whole thing each time. Instead,
249     # execute the full test on the first USB port, and perform a very limited
250     # test on other ports. In the limited case, we solely validate that the
251     # host PC can enumerate the U-Boot USB device.
252     global first_usb_dev_port
253     if not first_usb_dev_port:
254         first_usb_dev_port = env__usb_dev_port
255     if env__usb_dev_port == first_usb_dev_port:
256         sizes = env__dfu_config.get('test_sizes', test_sizes_default)
257     else:
258         sizes = []
259
260     dummy_f = u_boot_utils.PersistentRandomFile(u_boot_console,
261         'dfu_dummy.bin', 1024)
262
263     ignore_cleanup_errors = True
264     try:
265         start_dfu()
266
267         u_boot_console.log.action(
268             'Overwriting DFU primary altsetting with dummy data')
269         dfu_write(0, dummy_f.abs_fn)
270
271         for size in sizes:
272             with u_boot_console.log.section('Data size %d' % size):
273                 dfu_write_read_check(size)
274                 # Make the status of each sub-test obvious. If the test didn't
275                 # pass, an exception was thrown so this code isn't executed.
276                 u_boot_console.log.status_pass('OK')
277         ignore_cleanup_errors = False
278     finally:
279         stop_dfu(ignore_cleanup_errors)