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