]>
Commit | Line | Data |
---|---|---|
76b46939 SW |
1 | # Copyright (c) 2016, NVIDIA CORPORATION. All rights reserved. |
2 | # | |
3 | # SPDX-License-Identifier: GPL-2.0 | |
4 | ||
5 | # Utility code shared across multiple tests. | |
6 | ||
7 | import hashlib | |
ac122efd | 8 | import inspect |
76b46939 SW |
9 | import os |
10 | import os.path | |
b8218a91 | 11 | import pytest |
76b46939 SW |
12 | import sys |
13 | import time | |
7e6621a1 | 14 | import pytest |
76b46939 SW |
15 | |
16 | def md5sum_data(data): | |
e8debf39 | 17 | """Calculate the MD5 hash of some data. |
76b46939 SW |
18 | |
19 | Args: | |
20 | data: The data to hash. | |
21 | ||
22 | Returns: | |
23 | The hash of the data, as a binary string. | |
e8debf39 | 24 | """ |
76b46939 SW |
25 | |
26 | h = hashlib.md5() | |
27 | h.update(data) | |
28 | return h.digest() | |
29 | ||
30 | def md5sum_file(fn, max_length=None): | |
e8debf39 | 31 | """Calculate the MD5 hash of the contents of a file. |
76b46939 SW |
32 | |
33 | Args: | |
34 | fn: The filename of the file to hash. | |
35 | max_length: The number of bytes to hash. If the file has more | |
36 | bytes than this, they will be ignored. If None or omitted, the | |
37 | entire file will be hashed. | |
38 | ||
39 | Returns: | |
40 | The hash of the file content, as a binary string. | |
e8debf39 | 41 | """ |
76b46939 SW |
42 | |
43 | with open(fn, 'rb') as fh: | |
44 | if max_length: | |
45 | params = [max_length] | |
46 | else: | |
47 | params = [] | |
48 | data = fh.read(*params) | |
49 | return md5sum_data(data) | |
50 | ||
51 | class PersistentRandomFile(object): | |
e8debf39 SW |
52 | """Generate and store information about a persistent file containing |
53 | random data.""" | |
76b46939 SW |
54 | |
55 | def __init__(self, u_boot_console, fn, size): | |
e8debf39 | 56 | """Create or process the persistent file. |
76b46939 SW |
57 | |
58 | If the file does not exist, it is generated. | |
59 | ||
60 | If the file does exist, its content is hashed for later comparison. | |
61 | ||
62 | These files are always located in the "persistent data directory" of | |
63 | the current test run. | |
64 | ||
65 | Args: | |
66 | u_boot_console: A console connection to U-Boot. | |
67 | fn: The filename (without path) to create. | |
68 | size: The desired size of the file in bytes. | |
69 | ||
70 | Returns: | |
71 | Nothing. | |
e8debf39 | 72 | """ |
76b46939 SW |
73 | |
74 | self.fn = fn | |
75 | ||
76 | self.abs_fn = u_boot_console.config.persistent_data_dir + '/' + fn | |
77 | ||
78 | if os.path.exists(self.abs_fn): | |
79 | u_boot_console.log.action('Persistent data file ' + self.abs_fn + | |
80 | ' already exists') | |
81 | self.content_hash = md5sum_file(self.abs_fn) | |
82 | else: | |
83 | u_boot_console.log.action('Generating ' + self.abs_fn + | |
84 | ' (random, persistent, %d bytes)' % size) | |
85 | data = os.urandom(size) | |
86 | with open(self.abs_fn, 'wb') as fh: | |
87 | fh.write(data) | |
88 | self.content_hash = md5sum_data(data) | |
89 | ||
90 | def attempt_to_open_file(fn): | |
e8debf39 | 91 | """Attempt to open a file, without throwing exceptions. |
76b46939 SW |
92 | |
93 | Any errors (exceptions) that occur during the attempt to open the file | |
94 | are ignored. This is useful in order to test whether a file (in | |
95 | particular, a device node) exists and can be successfully opened, in order | |
96 | to poll for e.g. USB enumeration completion. | |
97 | ||
98 | Args: | |
99 | fn: The filename to attempt to open. | |
100 | ||
101 | Returns: | |
102 | An open file handle to the file, or None if the file could not be | |
103 | opened. | |
e8debf39 | 104 | """ |
76b46939 SW |
105 | |
106 | try: | |
107 | return open(fn, 'rb') | |
108 | except: | |
109 | return None | |
110 | ||
111 | def wait_until_open_succeeds(fn): | |
e8debf39 | 112 | """Poll until a file can be opened, or a timeout occurs. |
76b46939 SW |
113 | |
114 | Continually attempt to open a file, and return when this succeeds, or | |
115 | raise an exception after a timeout. | |
116 | ||
117 | Args: | |
118 | fn: The filename to attempt to open. | |
119 | ||
120 | Returns: | |
121 | An open file handle to the file. | |
e8debf39 | 122 | """ |
76b46939 SW |
123 | |
124 | for i in xrange(100): | |
125 | fh = attempt_to_open_file(fn) | |
126 | if fh: | |
127 | return fh | |
128 | time.sleep(0.1) | |
129 | raise Exception('File could not be opened') | |
130 | ||
131 | def wait_until_file_open_fails(fn, ignore_errors): | |
e8debf39 | 132 | """Poll until a file cannot be opened, or a timeout occurs. |
76b46939 SW |
133 | |
134 | Continually attempt to open a file, and return when this fails, or | |
135 | raise an exception after a timeout. | |
136 | ||
137 | Args: | |
138 | fn: The filename to attempt to open. | |
139 | ignore_errors: Indicate whether to ignore timeout errors. If True, the | |
140 | function will simply return if a timeout occurs, otherwise an | |
141 | exception will be raised. | |
142 | ||
143 | Returns: | |
144 | Nothing. | |
e8debf39 | 145 | """ |
76b46939 SW |
146 | |
147 | for i in xrange(100): | |
148 | fh = attempt_to_open_file(fn) | |
149 | if not fh: | |
150 | return | |
151 | fh.close() | |
152 | time.sleep(0.1) | |
153 | if ignore_errors: | |
154 | return | |
155 | raise Exception('File can still be opened') | |
156 | ||
157 | def run_and_log(u_boot_console, cmd, ignore_errors=False): | |
e8debf39 | 158 | """Run a command and log its output. |
76b46939 SW |
159 | |
160 | Args: | |
161 | u_boot_console: A console connection to U-Boot. | |
ec70f8a9 SG |
162 | cmd: The command to run, as an array of argv[], or a string. |
163 | If a string, note that it is split up so that quoted spaces | |
164 | will not be preserved. E.g. "fred and" becomes ['"fred', 'and"'] | |
76b46939 SW |
165 | ignore_errors: Indicate whether to ignore errors. If True, the function |
166 | will simply return if the command cannot be executed or exits with | |
167 | an error code, otherwise an exception will be raised if such | |
168 | problems occur. | |
169 | ||
170 | Returns: | |
f3d3e95c | 171 | The output as a string. |
e8debf39 | 172 | """ |
ec70f8a9 SG |
173 | if isinstance(cmd, str): |
174 | cmd = cmd.split() | |
76b46939 | 175 | runner = u_boot_console.log.get_runner(cmd[0], sys.stdout) |
f3d3e95c | 176 | output = runner.run(cmd, ignore_errors=ignore_errors) |
76b46939 | 177 | runner.close() |
f3d3e95c | 178 | return output |
05266103 | 179 | |
9e17b034 | 180 | def run_and_log_expect_exception(u_boot_console, cmd, retcode, msg): |
72f52268 | 181 | """Run a command that is expected to fail. |
9e17b034 SG |
182 | |
183 | This runs a command and checks that it fails with the expected return code | |
184 | and exception method. If not, an exception is raised. | |
185 | ||
186 | Args: | |
187 | u_boot_console: A console connection to U-Boot. | |
188 | cmd: The command to run, as an array of argv[]. | |
189 | retcode: Expected non-zero return code from the command. | |
72f52268 | 190 | msg: String that should be contained within the command's output. |
9e17b034 SG |
191 | """ |
192 | try: | |
193 | runner = u_boot_console.log.get_runner(cmd[0], sys.stdout) | |
194 | runner.run(cmd) | |
195 | except Exception as e: | |
7f64b187 | 196 | assert(retcode == runner.exit_status) |
9e17b034 SG |
197 | assert(msg in runner.output) |
198 | else: | |
7f64b187 SG |
199 | raise Exception("Expected an exception with retcode %d message '%s'," |
200 | "but it was not raised" % (retcode, msg)) | |
9e17b034 SG |
201 | finally: |
202 | runner.close() | |
203 | ||
05266103 SW |
204 | ram_base = None |
205 | def find_ram_base(u_boot_console): | |
e8debf39 | 206 | """Find the running U-Boot's RAM location. |
05266103 SW |
207 | |
208 | Probe the running U-Boot to determine the address of the first bank | |
209 | of RAM. This is useful for tests that test reading/writing RAM, or | |
210 | load/save files that aren't associated with some standard address | |
211 | typically represented in an environment variable such as | |
212 | ${kernel_addr_r}. The value is cached so that it only needs to be | |
213 | actively read once. | |
214 | ||
215 | Args: | |
216 | u_boot_console: A console connection to U-Boot. | |
217 | ||
218 | Returns: | |
219 | The address of U-Boot's first RAM bank, as an integer. | |
e8debf39 | 220 | """ |
05266103 SW |
221 | |
222 | global ram_base | |
223 | if u_boot_console.config.buildconfig.get('config_cmd_bdi', 'n') != 'y': | |
224 | pytest.skip('bdinfo command not supported') | |
225 | if ram_base == -1: | |
226 | pytest.skip('Previously failed to find RAM bank start') | |
227 | if ram_base is not None: | |
228 | return ram_base | |
229 | ||
230 | with u_boot_console.log.section('find_ram_base'): | |
231 | response = u_boot_console.run_command('bdinfo') | |
232 | for l in response.split('\n'): | |
d56dd0b1 | 233 | if '-> start' in l or 'memstart =' in l: |
05266103 SW |
234 | ram_base = int(l.split('=')[1].strip(), 16) |
235 | break | |
236 | if ram_base is None: | |
237 | ram_base = -1 | |
238 | raise Exception('Failed to find RAM bank start in `bdinfo`') | |
239 | ||
240 | return ram_base | |
ac122efd SW |
241 | |
242 | class PersistentFileHelperCtxMgr(object): | |
243 | """A context manager for Python's "with" statement, which ensures that any | |
244 | generated file is deleted (and hence regenerated) if its mtime is older | |
245 | than the mtime of the Python module which generated it, and gets an mtime | |
246 | newer than the mtime of the Python module which generated after it is | |
247 | generated. Objects of this type should be created by factory function | |
248 | persistent_file_helper rather than directly.""" | |
249 | ||
250 | def __init__(self, log, filename): | |
251 | """Initialize a new object. | |
252 | ||
253 | Args: | |
254 | log: The Logfile object to log to. | |
255 | filename: The filename of the generated file. | |
256 | ||
257 | Returns: | |
258 | Nothing. | |
259 | """ | |
260 | ||
261 | self.log = log | |
262 | self.filename = filename | |
263 | ||
264 | def __enter__(self): | |
265 | frame = inspect.stack()[1] | |
266 | module = inspect.getmodule(frame[0]) | |
267 | self.module_filename = module.__file__ | |
268 | self.module_timestamp = os.path.getmtime(self.module_filename) | |
269 | ||
270 | if os.path.exists(self.filename): | |
271 | filename_timestamp = os.path.getmtime(self.filename) | |
272 | if filename_timestamp < self.module_timestamp: | |
273 | self.log.action('Removing stale generated file ' + | |
274 | self.filename) | |
275 | os.unlink(self.filename) | |
276 | ||
277 | def __exit__(self, extype, value, traceback): | |
278 | if extype: | |
279 | try: | |
280 | os.path.unlink(self.filename) | |
281 | except: | |
282 | pass | |
283 | return | |
284 | logged = False | |
285 | for i in range(20): | |
286 | filename_timestamp = os.path.getmtime(self.filename) | |
287 | if filename_timestamp > self.module_timestamp: | |
288 | break | |
289 | if not logged: | |
290 | self.log.action( | |
291 | 'Waiting for generated file timestamp to increase') | |
292 | logged = True | |
293 | os.utime(self.filename) | |
294 | time.sleep(0.1) | |
295 | ||
296 | def persistent_file_helper(u_boot_log, filename): | |
297 | """Manage the timestamps and regeneration of a persistent generated | |
298 | file. This function creates a context manager for Python's "with" | |
299 | statement | |
300 | ||
301 | Usage: | |
302 | with persistent_file_helper(u_boot_console.log, filename): | |
303 | code to generate the file, if it's missing. | |
304 | ||
305 | Args: | |
306 | u_boot_log: u_boot_console.log. | |
307 | filename: The filename of the generated file. | |
308 | ||
309 | Returns: | |
310 | A context manager object. | |
311 | """ | |
312 | ||
313 | return PersistentFileHelperCtxMgr(u_boot_log, filename) |