env: Allow U-Boot scripts to be placed in a .env file
[platform/kernel/u-boot.git] / test / py / tests / test_env.py
1 # SPDX-License-Identifier: GPL-2.0
2 # Copyright (c) 2015 Stephen Warren
3 # Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
4
5 # Test operation of shell commands relating to environment variables.
6
7 import os
8 import os.path
9 from subprocess import call, check_call, CalledProcessError
10 import tempfile
11
12 import pytest
13 import u_boot_utils
14
15 # FIXME: This might be useful for other tests;
16 # perhaps refactor it into ConsoleBase or some other state object?
17 class StateTestEnv(object):
18     """Container that represents the state of all U-Boot environment variables.
19     This enables quick determination of existant/non-existant variable
20     names.
21     """
22
23     def __init__(self, u_boot_console):
24         """Initialize a new StateTestEnv object.
25
26         Args:
27             u_boot_console: A U-Boot console.
28
29         Returns:
30             Nothing.
31         """
32
33         self.u_boot_console = u_boot_console
34         self.get_env()
35         self.set_var = self.get_non_existent_var()
36
37     def get_env(self):
38         """Read all current environment variables from U-Boot.
39
40         Args:
41             None.
42
43         Returns:
44             Nothing.
45         """
46
47         if self.u_boot_console.config.buildconfig.get(
48                 'config_version_variable', 'n') == 'y':
49             with self.u_boot_console.disable_check('main_signon'):
50                 response = self.u_boot_console.run_command('printenv')
51         else:
52             response = self.u_boot_console.run_command('printenv')
53         self.env = {}
54         for l in response.splitlines():
55             if not '=' in l:
56                 continue
57             (var, value) = l.split('=', 1)
58             self.env[var] = value
59
60     def get_existent_var(self):
61         """Return the name of an environment variable that exists.
62
63         Args:
64             None.
65
66         Returns:
67             The name of an environment variable.
68         """
69
70         for var in self.env:
71             return var
72
73     def get_non_existent_var(self):
74         """Return the name of an environment variable that does not exist.
75
76         Args:
77             None.
78
79         Returns:
80             The name of an environment variable.
81         """
82
83         n = 0
84         while True:
85             var = 'test_env_' + str(n)
86             if var not in self.env:
87                 return var
88             n += 1
89
90 ste = None
91 @pytest.fixture(scope='function')
92 def state_test_env(u_boot_console):
93     """pytest fixture to provide a StateTestEnv object to tests."""
94
95     global ste
96     if not ste:
97         ste = StateTestEnv(u_boot_console)
98     return ste
99
100 def unset_var(state_test_env, var):
101     """Unset an environment variable.
102
103     This both executes a U-Boot shell command and updates a StateTestEnv
104     object.
105
106     Args:
107         state_test_env: The StateTestEnv object to update.
108         var: The variable name to unset.
109
110     Returns:
111         Nothing.
112     """
113
114     state_test_env.u_boot_console.run_command('setenv %s' % var)
115     if var in state_test_env.env:
116         del state_test_env.env[var]
117
118 def set_var(state_test_env, var, value):
119     """Set an environment variable.
120
121     This both executes a U-Boot shell command and updates a StateTestEnv
122     object.
123
124     Args:
125         state_test_env: The StateTestEnv object to update.
126         var: The variable name to set.
127         value: The value to set the variable to.
128
129     Returns:
130         Nothing.
131     """
132
133     bc = state_test_env.u_boot_console.config.buildconfig
134     if bc.get('config_hush_parser', None):
135         quote = '"'
136     else:
137         quote = ''
138         if ' ' in value:
139             pytest.skip('Space in variable value on non-Hush shell')
140
141     state_test_env.u_boot_console.run_command(
142         'setenv %s %s%s%s' % (var, quote, value, quote))
143     state_test_env.env[var] = value
144
145 def validate_empty(state_test_env, var):
146     """Validate that a variable is not set, using U-Boot shell commands.
147
148     Args:
149         var: The variable name to test.
150
151     Returns:
152         Nothing.
153     """
154
155     response = state_test_env.u_boot_console.run_command('echo ${%s}' % var)
156     assert response == ''
157
158 def validate_set(state_test_env, var, value):
159     """Validate that a variable is set, using U-Boot shell commands.
160
161     Args:
162         var: The variable name to test.
163         value: The value the variable is expected to have.
164
165     Returns:
166         Nothing.
167     """
168
169     # echo does not preserve leading, internal, or trailing whitespace in the
170     # value. printenv does, and hence allows more complete testing.
171     response = state_test_env.u_boot_console.run_command('printenv %s' % var)
172     assert response == ('%s=%s' % (var, value))
173
174 def test_env_echo_exists(state_test_env):
175     """Test echoing a variable that exists."""
176
177     var = state_test_env.get_existent_var()
178     value = state_test_env.env[var]
179     validate_set(state_test_env, var, value)
180
181 @pytest.mark.buildconfigspec('cmd_echo')
182 def test_env_echo_non_existent(state_test_env):
183     """Test echoing a variable that doesn't exist."""
184
185     var = state_test_env.set_var
186     validate_empty(state_test_env, var)
187
188 def test_env_printenv_non_existent(state_test_env):
189     """Test printenv error message for non-existant variables."""
190
191     var = state_test_env.set_var
192     c = state_test_env.u_boot_console
193     with c.disable_check('error_notification'):
194         response = c.run_command('printenv %s' % var)
195     assert(response == '## Error: "%s" not defined' % var)
196
197 @pytest.mark.buildconfigspec('cmd_echo')
198 def test_env_unset_non_existent(state_test_env):
199     """Test unsetting a nonexistent variable."""
200
201     var = state_test_env.get_non_existent_var()
202     unset_var(state_test_env, var)
203     validate_empty(state_test_env, var)
204
205 def test_env_set_non_existent(state_test_env):
206     """Test set a non-existant variable."""
207
208     var = state_test_env.set_var
209     value = 'foo'
210     set_var(state_test_env, var, value)
211     validate_set(state_test_env, var, value)
212
213 def test_env_set_existing(state_test_env):
214     """Test setting an existant variable."""
215
216     var = state_test_env.set_var
217     value = 'bar'
218     set_var(state_test_env, var, value)
219     validate_set(state_test_env, var, value)
220
221 @pytest.mark.buildconfigspec('cmd_echo')
222 def test_env_unset_existing(state_test_env):
223     """Test unsetting a variable."""
224
225     var = state_test_env.set_var
226     unset_var(state_test_env, var)
227     validate_empty(state_test_env, var)
228
229 def test_env_expansion_spaces(state_test_env):
230     """Test expanding a variable that contains a space in its value."""
231
232     var_space = None
233     var_test = None
234     try:
235         var_space = state_test_env.get_non_existent_var()
236         set_var(state_test_env, var_space, ' ')
237
238         var_test = state_test_env.get_non_existent_var()
239         value = ' 1${%(var_space)s}${%(var_space)s} 2 ' % locals()
240         set_var(state_test_env, var_test, value)
241         value = ' 1   2 '
242         validate_set(state_test_env, var_test, value)
243     finally:
244         if var_space:
245             unset_var(state_test_env, var_space)
246         if var_test:
247             unset_var(state_test_env, var_test)
248
249 @pytest.mark.buildconfigspec('cmd_importenv')
250 def test_env_import_checksum_no_size(state_test_env):
251     """Test that omitted ('-') size parameter with checksum validation fails the
252        env import function.
253     """
254     c = state_test_env.u_boot_console
255     ram_base = u_boot_utils.find_ram_base(state_test_env.u_boot_console)
256     addr = '%08x' % ram_base
257
258     with c.disable_check('error_notification'):
259         response = c.run_command('env import -c %s -' % addr)
260     assert(response == '## Error: external checksum format must pass size')
261
262 @pytest.mark.buildconfigspec('cmd_importenv')
263 def test_env_import_whitelist_checksum_no_size(state_test_env):
264     """Test that omitted ('-') size parameter with checksum validation fails the
265        env import function when variables are passed as parameters.
266     """
267     c = state_test_env.u_boot_console
268     ram_base = u_boot_utils.find_ram_base(state_test_env.u_boot_console)
269     addr = '%08x' % ram_base
270
271     with c.disable_check('error_notification'):
272         response = c.run_command('env import -c %s - foo1 foo2 foo4' % addr)
273     assert(response == '## Error: external checksum format must pass size')
274
275 @pytest.mark.buildconfigspec('cmd_exportenv')
276 @pytest.mark.buildconfigspec('cmd_importenv')
277 def test_env_import_whitelist(state_test_env):
278     """Test importing only a handful of env variables from an environment."""
279     c = state_test_env.u_boot_console
280     ram_base = u_boot_utils.find_ram_base(state_test_env.u_boot_console)
281     addr = '%08x' % ram_base
282
283     set_var(state_test_env, 'foo1', 'bar1')
284     set_var(state_test_env, 'foo2', 'bar2')
285     set_var(state_test_env, 'foo3', 'bar3')
286
287     c.run_command('env export %s' % addr)
288
289     unset_var(state_test_env, 'foo1')
290     set_var(state_test_env, 'foo2', 'test2')
291     set_var(state_test_env, 'foo4', 'bar4')
292
293     # no foo1 in current env, foo2 overridden, foo3 should be of the value
294     # before exporting and foo4 should be of the value before importing.
295     c.run_command('env import %s - foo1 foo2 foo4' % addr)
296
297     validate_set(state_test_env, 'foo1', 'bar1')
298     validate_set(state_test_env, 'foo2', 'bar2')
299     validate_set(state_test_env, 'foo3', 'bar3')
300     validate_set(state_test_env, 'foo4', 'bar4')
301
302     # Cleanup test environment
303     unset_var(state_test_env, 'foo1')
304     unset_var(state_test_env, 'foo2')
305     unset_var(state_test_env, 'foo3')
306     unset_var(state_test_env, 'foo4')
307
308 @pytest.mark.buildconfigspec('cmd_exportenv')
309 @pytest.mark.buildconfigspec('cmd_importenv')
310 def test_env_import_whitelist_delete(state_test_env):
311
312     """Test importing only a handful of env variables from an environment, with.
313        deletion if a var A that is passed to env import is not in the
314        environment to be imported.
315     """
316     c = state_test_env.u_boot_console
317     ram_base = u_boot_utils.find_ram_base(state_test_env.u_boot_console)
318     addr = '%08x' % ram_base
319
320     set_var(state_test_env, 'foo1', 'bar1')
321     set_var(state_test_env, 'foo2', 'bar2')
322     set_var(state_test_env, 'foo3', 'bar3')
323
324     c.run_command('env export %s' % addr)
325
326     unset_var(state_test_env, 'foo1')
327     set_var(state_test_env, 'foo2', 'test2')
328     set_var(state_test_env, 'foo4', 'bar4')
329
330     # no foo1 in current env, foo2 overridden, foo3 should be of the value
331     # before exporting and foo4 should be empty.
332     c.run_command('env import -d %s - foo1 foo2 foo4' % addr)
333
334     validate_set(state_test_env, 'foo1', 'bar1')
335     validate_set(state_test_env, 'foo2', 'bar2')
336     validate_set(state_test_env, 'foo3', 'bar3')
337     validate_empty(state_test_env, 'foo4')
338
339     # Cleanup test environment
340     unset_var(state_test_env, 'foo1')
341     unset_var(state_test_env, 'foo2')
342     unset_var(state_test_env, 'foo3')
343     unset_var(state_test_env, 'foo4')
344
345 @pytest.mark.buildconfigspec('cmd_nvedit_info')
346 def test_env_info(state_test_env):
347
348     """Test 'env info' command with all possible options.
349     """
350     c = state_test_env.u_boot_console
351
352     response = c.run_command('env info')
353     nb_line = 0
354     for l in response.split('\n'):
355         if 'env_valid = ' in l:
356             assert '= invalid' in l or '= valid' in l or '= redundant' in l
357             nb_line += 1
358         elif 'env_ready =' in l or 'env_use_default =' in l:
359             assert '= true' in l or '= false' in l
360             nb_line += 1
361         else:
362             assert true
363     assert nb_line == 3
364
365     response = c.run_command('env info -p -d')
366     assert 'Default environment is used' in response or "Environment was loaded from persistent storage" in response
367     assert 'Environment can be persisted' in response or "Environment cannot be persisted" in response
368
369     response = c.run_command('env info -p -d -q')
370     assert response == ""
371
372     response = c.run_command('env info -p -q')
373     assert response == ""
374
375     response = c.run_command('env info -d -q')
376     assert response == ""
377
378 @pytest.mark.boardspec('sandbox')
379 @pytest.mark.buildconfigspec('cmd_nvedit_info')
380 @pytest.mark.buildconfigspec('cmd_echo')
381 def test_env_info_sandbox(state_test_env):
382     """Test 'env info' command result with several options on sandbox
383        with a known ENV configuration: ready & default & persistent
384     """
385     c = state_test_env.u_boot_console
386
387     response = c.run_command('env info')
388     assert 'env_ready = true' in response
389     assert 'env_use_default = true' in response
390
391     response = c.run_command('env info -p -d')
392     assert 'Default environment is used' in response
393     assert 'Environment cannot be persisted' in response
394
395     response = c.run_command('env info -d -q')
396     response = c.run_command('echo $?')
397     assert response == "0"
398
399     response = c.run_command('env info -p -q')
400     response = c.run_command('echo $?')
401     assert response == "1"
402
403     response = c.run_command('env info -d -p -q')
404     response = c.run_command('echo $?')
405     assert response == "1"
406
407 def mk_env_ext4(state_test_env):
408
409     """Create a empty ext4 file system volume."""
410     c = state_test_env.u_boot_console
411     filename = 'env.ext4.img'
412     persistent = c.config.persistent_data_dir + '/' + filename
413     fs_img = c.config.result_dir  + '/' + filename
414
415     if os.path.exists(persistent):
416         c.log.action('Disk image file ' + persistent + ' already exists')
417     else:
418         # Some distributions do not add /sbin to the default PATH, where mkfs.ext4 lives
419         os.environ["PATH"] += os.pathsep + '/sbin'
420         try:
421             u_boot_utils.run_and_log(c, 'dd if=/dev/zero of=%s bs=1M count=16' % persistent)
422             u_boot_utils.run_and_log(c, 'mkfs.ext4 %s' % persistent)
423             sb_content = u_boot_utils.run_and_log(c, 'tune2fs -l %s' % persistent)
424             if 'metadata_csum' in sb_content:
425                 u_boot_utils.run_and_log(c, 'tune2fs -O ^metadata_csum %s' % persistent)
426         except CalledProcessError:
427             call('rm -f %s' % persistent, shell=True)
428             raise
429
430     u_boot_utils.run_and_log(c, ['cp',  '-f', persistent, fs_img])
431     return fs_img
432
433 @pytest.mark.boardspec('sandbox')
434 @pytest.mark.buildconfigspec('cmd_echo')
435 @pytest.mark.buildconfigspec('cmd_nvedit_info')
436 @pytest.mark.buildconfigspec('cmd_nvedit_load')
437 @pytest.mark.buildconfigspec('cmd_nvedit_select')
438 @pytest.mark.buildconfigspec('env_is_in_ext4')
439 def test_env_ext4(state_test_env):
440
441     """Test ENV in EXT4 on sandbox."""
442     c = state_test_env.u_boot_console
443     fs_img = ''
444     try:
445         fs_img = mk_env_ext4(state_test_env)
446
447         c.run_command('host bind 0  %s' % fs_img)
448
449         response = c.run_command('ext4ls host 0:0')
450         assert 'uboot.env' not in response
451
452         # force env location: EXT4 (prio 1 in sandbox)
453         response = c.run_command('env select EXT4')
454         assert 'Select Environment on EXT4: OK' in response
455
456         response = c.run_command('env save')
457         assert 'Saving Environment to EXT4' in response
458
459         response = c.run_command('env load')
460         assert 'Loading Environment from EXT4... OK' in response
461
462         response = c.run_command('ext4ls host 0:0')
463         assert '8192 uboot.env' in response
464
465         response = c.run_command('env info')
466         assert 'env_valid = valid' in response
467         assert 'env_ready = true' in response
468         assert 'env_use_default = false' in response
469
470         response = c.run_command('env info -p -d')
471         assert 'Environment was loaded from persistent storage' in response
472         assert 'Environment can be persisted' in response
473
474         response = c.run_command('env info -d -q')
475         assert response == ""
476         response = c.run_command('echo $?')
477         assert response == "1"
478
479         response = c.run_command('env info -p -q')
480         assert response == ""
481         response = c.run_command('echo $?')
482         assert response == "0"
483
484         response = c.run_command('env erase')
485         assert 'OK' in response
486
487         response = c.run_command('env load')
488         assert 'Loading Environment from EXT4... ' in response
489         assert 'bad CRC, using default environment' in response
490
491         response = c.run_command('env info')
492         assert 'env_valid = invalid' in response
493         assert 'env_ready = true' in response
494         assert 'env_use_default = true' in response
495
496         response = c.run_command('env info -p -d')
497         assert 'Default environment is used' in response
498         assert 'Environment can be persisted' in response
499
500         # restore env location: NOWHERE (prio 0 in sandbox)
501         response = c.run_command('env select nowhere')
502         assert 'Select Environment on nowhere: OK' in response
503
504         response = c.run_command('env load')
505         assert 'Loading Environment from nowhere... OK' in response
506
507         response = c.run_command('env info')
508         assert 'env_valid = invalid' in response
509         assert 'env_ready = true' in response
510         assert 'env_use_default = true' in response
511
512         response = c.run_command('env info -p -d')
513         assert 'Default environment is used' in response
514         assert 'Environment cannot be persisted' in response
515
516     finally:
517         if fs_img:
518             call('rm -f %s' % fs_img, shell=True)
519
520 def test_env_text(u_boot_console):
521     """Test the script that converts the environment to a text file"""
522
523     def check_script(intext, expect_val):
524         """Check a test case
525
526         Args:
527             intext: Text to pass to the script
528             expect_val: Expected value of the CONFIG_EXTRA_ENV_TEXT string, or
529                 None if we expect it not to be defined
530         """
531         with tempfile.TemporaryDirectory() as path:
532             fname = os.path.join(path, 'infile')
533             with open(fname, 'w') as inf:
534                 print(intext, file=inf)
535             result = u_boot_utils.run_and_log(cons, ['awk', '-f', script, fname])
536             if expect_val is not None:
537                 expect = '#define CONFIG_EXTRA_ENV_TEXT "%s"\n' % expect_val
538                 assert result == expect
539             else:
540                 assert result == ''
541
542     cons = u_boot_console
543     script = os.path.join(cons.config.source_dir, 'scripts', 'env2string.awk')
544
545     # simple script with a single var
546     check_script('fred=123', 'fred=123\\0')
547
548     # no vars
549     check_script('', None)
550
551     # two vars
552     check_script('''fred=123
553 ernie=456''', 'fred=123\\0ernie=456\\0')
554
555     # blank lines
556     check_script('''fred=123
557
558
559 ernie=456
560
561 ''', 'fred=123\\0ernie=456\\0')
562
563     # append
564     check_script('''fred=123
565 ernie=456
566 fred+= 456''', 'fred=123 456\\0ernie=456\\0')
567
568     # append from empty
569     check_script('''fred=
570 ernie=456
571 fred+= 456''', 'fred= 456\\0ernie=456\\0')
572
573     # variable with + in it
574     check_script('fred+ernie=123', 'fred+ernie=123\\0')
575
576     # ignores variables that are empty
577     check_script('''fred=
578 fred+=
579 ernie=456''', 'ernie=456\\0')
580
581     # single-character env name
582     check_script('''f=123
583 e=456
584 f+= 456''', 'e=456\\0f=123 456\\0')
585
586     # contains quotes
587     check_script('''fred="my var"
588 ernie=another"''', 'fred=\\"my var\\"\\0ernie=another\\"\\0')
589
590     # variable name ending in +
591     check_script('''fred\\+=my var
592 fred++= again''', 'fred+=my var again\\0')
593
594     # variable name containing +
595     check_script('''fred+jane=both
596 fred+jane+=again
597 ernie=456''', 'fred+jane=bothagain\\0ernie=456\\0')
598
599     # multi-line vars - new vars always start at column 1
600     check_script('''fred=first
601  second
602 \tthird with tab
603
604    after blank
605  confusing=oops
606 ernie=another"''', 'fred=first second third with tab after blank confusing=oops\\0ernie=another\\"\\0')
607
608     # real-world example
609     check_script('''ubifs_boot=
610         env exists bootubipart ||
611                 env set bootubipart UBI;
612         env exists bootubivol ||
613                 env set bootubivol boot;
614         if ubi part ${bootubipart} &&
615                 ubifsmount ubi${devnum}:${bootubivol};
616         then
617                 devtype=ubi;
618                 run scan_dev_for_boot;
619         fi
620 ''',
621         'ubifs_boot=env exists bootubipart || env set bootubipart UBI; '
622         'env exists bootubivol || env set bootubivol boot; '
623         'if ubi part ${bootubipart} && ubifsmount ubi${devnum}:${bootubivol}; '
624         'then devtype=ubi; run scan_dev_for_boot; fi\\0')