west/tests/test_config.py

528 lines
21 KiB
Python

# Copyright (c) 2019, Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0
import configparser
import os
import pathlib
import subprocess
import pytest
from west import configuration as config
from conftest import cmd
assert 'TOXTEMPDIR' in os.environ, "you must run these tests using tox"
SYSTEM = config.ConfigFile.SYSTEM
GLOBAL = config.ConfigFile.GLOBAL
LOCAL = config.ConfigFile.LOCAL
ALL = config.ConfigFile.ALL
@pytest.fixture(autouse=True)
def config_tmpdir(tmpdir):
# Fixture for running from a temporary directory with
# environmental overrides in place so all configuration files
# live inside of it. This makes sure we don't touch
# the user's actual files.
#
# We also set ZEPHYR_BASE (to avoid complaints in subcommand
# stderr), but to a spurious location (so that attempts to read
# from inside of it are caught here).
#
# Using this makes the tests run faster than if we used
# west_init_tmpdir from conftest.py, and also ensures that the
# configuration code doesn't depend on features like the existence
# of a manifest file, helping separate concerns.
system = tmpdir / 'config.system'
glbl = tmpdir / 'config.global'
local = tmpdir / 'config.local'
os.environ['ZEPHYR_BASE'] = str(tmpdir.join('no-zephyr-here'))
os.environ['WEST_CONFIG_SYSTEM'] = str(system)
os.environ['WEST_CONFIG_GLOBAL'] = str(glbl)
os.environ['WEST_CONFIG_LOCAL'] = str(local)
# Make sure our environment variables (as well as other topdirs)
# are respected from tmpdir, and we aren't going to touch the
# user's real files.
start_dir = os.getcwd()
tmpdir.chdir()
try:
assert config._location(SYSTEM) == str(system)
assert config._location(GLOBAL) == str(glbl)
td = tmpdir / 'test-topdir'
td.ensure(dir=True)
(td / '.west').ensure(dir=True)
(td / '.west' / 'config').ensure(file=True)
assert config._location(LOCAL) == str(local)
assert config._location(LOCAL, topdir=str(td)) == str(local)
td.remove(rec=1)
assert not td.exists()
assert not local.exists()
# All clear: switch to the temporary directory and run the test.
yield tmpdir
finally:
# Go back to where we started, for repeatability of results.
os.chdir(start_dir)
# Clean up after ourselves so other test cases don't know
# about this tmpdir. It's OK if test cases deleted these
# settings already.
if 'ZEPHYR_BASE' in os.environ:
del os.environ['ZEPHYR_BASE']
if 'WEST_CONFIG_SYSTEM' in os.environ:
del os.environ['WEST_CONFIG_SYSTEM']
if 'WEST_CONFIG_GLOBAL' in os.environ:
del os.environ['WEST_CONFIG_GLOBAL']
if 'WEST_CONFIG_LOCAL' in os.environ:
del os.environ['WEST_CONFIG_LOCAL']
def cfg(f=ALL, topdir=None):
# Load a fresh configuration object at the given level, and return it.
cp = configparser.ConfigParser(allow_no_value=True)
config.read_config(configfile=f, config=cp, topdir=topdir)
return cp
def test_config_global():
# Set a global config option via the command interface. Make sure
# it can be read back using the API calls and at the command line
# at ALL and GLOBAL locations only.
cmd('config --global pytest.global foo')
assert cfg(f=GLOBAL)['pytest']['global'] == 'foo'
assert cfg(f=ALL)['pytest']['global'] == 'foo'
assert 'pytest' not in cfg(f=SYSTEM)
assert 'pytest' not in cfg(f=LOCAL)
assert cmd('config pytest.global').rstrip() == 'foo'
assert cmd('config --global pytest.global').rstrip() == 'foo'
# Make sure we can change the value of an existing variable.
cmd('config --global pytest.global bar')
assert cfg(f=GLOBAL)['pytest']['global'] == 'bar'
assert cfg(f=ALL)['pytest']['global'] == 'bar'
assert cmd('config pytest.global').rstrip() == 'bar'
assert cmd('config --global pytest.global').rstrip() == 'bar'
# Sanity check that we can create multiple variables per section.
# Just use the API here; there's coverage already that the command line
# and API match.
cmd('config --global pytest.global2 foo2')
all, glb, lcl = cfg(f=ALL), cfg(f=GLOBAL), cfg(f=LOCAL)
assert all['pytest']['global'] == 'bar'
assert glb['pytest']['global'] == 'bar'
assert 'pytest' not in lcl
assert all['pytest']['global2'] == 'foo2'
assert glb['pytest']['global2'] == 'foo2'
assert 'pytest' not in lcl
def test_config_local():
# test_config_system for local variables.
cmd('config --local pytest.local foo')
assert cfg(f=LOCAL)['pytest']['local'] == 'foo'
assert cfg(f=ALL)['pytest']['local'] == 'foo'
assert 'pytest' not in cfg(f=SYSTEM)
assert 'pytest' not in cfg(f=GLOBAL)
assert cmd('config pytest.local').rstrip() == 'foo'
assert cmd('config --local pytest.local').rstrip() == 'foo'
cmd('config --local pytest.local bar')
assert cfg(f=LOCAL)['pytest']['local'] == 'bar'
assert cfg(f=ALL)['pytest']['local'] == 'bar'
assert 'pytest' not in cfg(f=SYSTEM)
assert 'pytest' not in cfg(f=GLOBAL)
assert cmd('config pytest.local').rstrip() == 'bar'
assert cmd('config --local pytest.local').rstrip() == 'bar'
cmd('config --local pytest.local2 foo2')
all, glb, lcl = cfg(f=ALL), cfg(f=GLOBAL), cfg(f=LOCAL)
assert all['pytest']['local'] == 'bar'
assert 'pytest' not in glb
assert lcl['pytest']['local'] == 'bar'
assert all['pytest']['local2'] == 'foo2'
assert 'pytest' not in glb
assert lcl['pytest']['local2'] == 'foo2'
def test_config_system():
# Basic test of system-level configuration.
config.update_config('pytest', 'key', 'val', configfile=SYSTEM)
assert cfg(f=ALL)['pytest']['key'] == 'val'
assert cfg(f=SYSTEM)['pytest']['key'] == 'val'
assert 'pytest' not in cfg(f=GLOBAL)
assert 'pytest' not in cfg(f=LOCAL)
config.update_config('pytest', 'key', 'val2', configfile=SYSTEM)
assert cfg(f=SYSTEM)['pytest']['key'] == 'val2'
def test_config_system_precedence():
# Test precedence rules, including system level.
config.update_config('pytest', 'key', 'sys', configfile=SYSTEM)
assert cfg(f=SYSTEM)['pytest']['key'] == 'sys'
assert cfg(f=ALL)['pytest']['key'] == 'sys'
config.update_config('pytest', 'key', 'glb', configfile=GLOBAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'sys'
assert cfg(f=GLOBAL)['pytest']['key'] == 'glb'
assert cfg(f=ALL)['pytest']['key'] == 'glb'
config.update_config('pytest', 'key', 'lcl', configfile=LOCAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'sys'
assert cfg(f=GLOBAL)['pytest']['key'] == 'glb'
assert cfg(f=LOCAL)['pytest']['key'] == 'lcl'
assert cfg(f=ALL)['pytest']['key'] == 'lcl'
def test_system_creation():
# Test that the system file -- and just that file -- is created on
# demand.
assert not os.path.isfile(config._location(SYSTEM))
assert not os.path.isfile(config._location(GLOBAL))
assert not os.path.isfile(config._location(LOCAL))
config.update_config('pytest', 'key', 'val', configfile=SYSTEM)
assert os.path.isfile(config._location(SYSTEM))
assert not os.path.isfile(config._location(GLOBAL))
assert not os.path.isfile(config._location(LOCAL))
assert cfg(f=ALL)['pytest']['key'] == 'val'
assert cfg(f=SYSTEM)['pytest']['key'] == 'val'
assert 'pytest' not in cfg(f=GLOBAL)
assert 'pytest' not in cfg(f=LOCAL)
def test_global_creation():
# Like test_system_creation, for global config options.
assert not os.path.isfile(config._location(SYSTEM))
assert not os.path.isfile(config._location(GLOBAL))
assert not os.path.isfile(config._location(LOCAL))
config.update_config('pytest', 'key', 'val', configfile=GLOBAL)
assert not os.path.isfile(config._location(SYSTEM))
assert os.path.isfile(config._location(GLOBAL))
assert not os.path.isfile(config._location(LOCAL))
assert cfg(f=ALL)['pytest']['key'] == 'val'
assert 'pytest' not in cfg(f=SYSTEM)
assert cfg(f=GLOBAL)['pytest']['key'] == 'val'
assert 'pytest' not in cfg(f=LOCAL)
def test_local_creation():
# Like test_system_creation, for local config options.
assert not os.path.isfile(config._location(SYSTEM))
assert not os.path.isfile(config._location(GLOBAL))
assert not os.path.isfile(config._location(LOCAL))
config.update_config('pytest', 'key', 'val', configfile=LOCAL)
assert not os.path.isfile(config._location(SYSTEM))
assert not os.path.isfile(config._location(GLOBAL))
assert os.path.isfile(config._location(LOCAL))
assert cfg(f=ALL)['pytest']['key'] == 'val'
assert 'pytest' not in cfg(f=SYSTEM)
assert 'pytest' not in cfg(f=GLOBAL)
assert cfg(f=LOCAL)['pytest']['key'] == 'val'
def test_local_creation_with_topdir():
# Like test_local_creation, with a specified topdir.
system = pathlib.Path(config._location(SYSTEM))
glbl = pathlib.Path(config._location(GLOBAL))
local = pathlib.Path(config._location(LOCAL))
topdir = pathlib.Path(os.getcwd()) / 'test-topdir'
topdir_west = topdir / '.west'
assert not topdir_west.exists()
topdir_west.mkdir(parents=True)
topdir_config = topdir_west / 'config'
assert not system.exists()
assert not glbl.exists()
assert not local.exists()
assert not topdir_config.exists()
# The autouse fixture at the top of this file has set up an
# environment variable for our local config file. Disable it
# to make sure the API works with a 'real' topdir.
del os.environ['WEST_CONFIG_LOCAL']
# We should be able to write into our topdir's config file now.
config.update_config('pytest', 'key', 'val', configfile=LOCAL,
topdir=str(topdir))
assert not system.exists()
assert not glbl.exists()
assert not local.exists()
assert topdir_config.exists()
assert cfg(f=ALL, topdir=str(topdir))['pytest']['key'] == 'val'
assert 'pytest' not in cfg(f=SYSTEM)
assert 'pytest' not in cfg(f=GLOBAL)
assert cfg(f=LOCAL, topdir=str(topdir))['pytest']['key'] == 'val'
def test_delete_basic():
# Basic deletion test: write local, verify global and system deletions
# don't work, then delete local does work.
config.update_config('pytest', 'key', 'val', configfile=LOCAL)
assert cfg(f=ALL)['pytest']['key'] == 'val'
with pytest.raises(KeyError):
config.delete_config('pytest', 'key', configfile=SYSTEM)
with pytest.raises(KeyError):
config.delete_config('pytest', 'key', configfile=GLOBAL)
config.delete_config('pytest', 'key', configfile=LOCAL)
assert 'pytest' not in cfg(f=ALL)
def test_delete_all():
# Deleting ConfigFile.ALL should delete from everywhere.
config.update_config('pytest', 'key', 'system', configfile=SYSTEM)
config.update_config('pytest', 'key', 'global', configfile=GLOBAL)
config.update_config('pytest', 'key', 'local', configfile=LOCAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'system'
assert cfg(f=GLOBAL)['pytest']['key'] == 'global'
assert cfg(f=LOCAL)['pytest']['key'] == 'local'
config.delete_config('pytest', 'key', configfile=ALL)
assert 'pytest' not in cfg(f=ALL)
def test_delete_none():
# Deleting None should delete from lowest-precedence global or
# local file only.
config.update_config('pytest', 'key', 'system', configfile=SYSTEM)
config.update_config('pytest', 'key', 'global', configfile=GLOBAL)
config.update_config('pytest', 'key', 'local', configfile=LOCAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'system'
assert cfg(f=GLOBAL)['pytest']['key'] == 'global'
assert cfg(f=LOCAL)['pytest']['key'] == 'local'
config.delete_config('pytest', 'key', configfile=None)
assert cfg(f=ALL)['pytest']['key'] == 'global'
config.delete_config('pytest', 'key', configfile=None)
assert cfg(f=ALL)['pytest']['key'] == 'system'
with pytest.raises(KeyError):
config.delete_config('pytest', 'key', configfile=None)
def test_delete_list():
# Test delete of a list of places.
config.update_config('pytest', 'key', 'system', configfile=SYSTEM)
config.update_config('pytest', 'key', 'global', configfile=GLOBAL)
config.update_config('pytest', 'key', 'local', configfile=LOCAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'system'
assert cfg(f=GLOBAL)['pytest']['key'] == 'global'
assert cfg(f=LOCAL)['pytest']['key'] == 'local'
config.delete_config('pytest', 'key', configfile=[GLOBAL, LOCAL])
assert cfg(f=SYSTEM)['pytest']['key'] == 'system'
assert 'pytest' not in cfg(f=GLOBAL)
assert 'pytest' not in cfg(f=LOCAL)
def test_delete_system():
# Test SYSTEM-only delete.
config.update_config('pytest', 'key', 'system', configfile=SYSTEM)
config.update_config('pytest', 'key', 'global', configfile=GLOBAL)
config.update_config('pytest', 'key', 'local', configfile=LOCAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'system'
assert cfg(f=GLOBAL)['pytest']['key'] == 'global'
assert cfg(f=LOCAL)['pytest']['key'] == 'local'
config.delete_config('pytest', 'key', configfile=SYSTEM)
assert 'pytest' not in cfg(f=SYSTEM)
assert cfg(f=GLOBAL)['pytest']['key'] == 'global'
assert cfg(f=LOCAL)['pytest']['key'] == 'local'
def test_delete_global():
# Test GLOBAL-only delete.
config.update_config('pytest', 'key', 'system', configfile=SYSTEM)
config.update_config('pytest', 'key', 'global', configfile=GLOBAL)
config.update_config('pytest', 'key', 'local', configfile=LOCAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'system'
assert cfg(f=GLOBAL)['pytest']['key'] == 'global'
assert cfg(f=LOCAL)['pytest']['key'] == 'local'
config.delete_config('pytest', 'key', configfile=GLOBAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'system'
assert 'pytest' not in cfg(f=GLOBAL)
assert cfg(f=LOCAL)['pytest']['key'] == 'local'
def test_delete_local():
# Test LOCAL-only delete.
config.update_config('pytest', 'key', 'system', configfile=SYSTEM)
config.update_config('pytest', 'key', 'global', configfile=GLOBAL)
config.update_config('pytest', 'key', 'local', configfile=LOCAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'system'
assert cfg(f=GLOBAL)['pytest']['key'] == 'global'
assert cfg(f=LOCAL)['pytest']['key'] == 'local'
config.delete_config('pytest', 'key', configfile=LOCAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'system'
assert cfg(f=GLOBAL)['pytest']['key'] == 'global'
assert 'pytest' not in cfg(f=LOCAL)
def test_delete_local_with_topdir():
# Test LOCAL-only delete with specified topdir.
config.update_config('pytest', 'key', 'system', configfile=SYSTEM)
config.update_config('pytest', 'key', 'global', configfile=GLOBAL)
config.update_config('pytest', 'key', 'local', configfile=LOCAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'system'
assert cfg(f=GLOBAL)['pytest']['key'] == 'global'
assert cfg(f=LOCAL)['pytest']['key'] == 'local'
config.delete_config('pytest', 'key', configfile=LOCAL)
assert cfg(f=SYSTEM)['pytest']['key'] == 'system'
assert cfg(f=GLOBAL)['pytest']['key'] == 'global'
assert 'pytest' not in cfg(f=LOCAL)
def test_delete_local_one():
# Test LOCAL-only delete of one option doesn't affect the other.
config.update_config('pytest', 'key1', 'foo', configfile=LOCAL)
config.update_config('pytest', 'key2', 'bar', configfile=LOCAL)
config.delete_config('pytest', 'key1', configfile=LOCAL)
assert 'pytest' in cfg(f=LOCAL)
assert cfg(f=LOCAL)['pytest']['key2'] == 'bar'
def test_delete_cmd_all():
# west config -D should delete from everywhere
cmd('config --system pytest.key system')
cmd('config --global pytest.key global')
cmd('config --local pytest.key local')
assert cfg(f=ALL)['pytest']['key'] == 'local'
cmd('config -D pytest.key')
assert 'pytest' not in cfg(f=ALL)
with pytest.raises(subprocess.CalledProcessError):
cmd('config -D pytest.key')
def test_delete_cmd_none():
# west config -d should delete from lowest-precedence global or
# local file only.
cmd('config --system pytest.key system')
cmd('config --global pytest.key global')
cmd('config --local pytest.key local')
cmd('config -d pytest.key')
assert cmd('config pytest.key').rstrip() == 'global'
cmd('config -d pytest.key')
assert cmd('config pytest.key').rstrip() == 'system'
with pytest.raises(subprocess.CalledProcessError):
cmd('config -d pytest.key')
def test_delete_cmd_system():
# west config -d --system should only delete from system
cmd('config --system pytest.key system')
cmd('config --global pytest.key global')
cmd('config --local pytest.key local')
cmd('config -d --system pytest.key')
with pytest.raises(subprocess.CalledProcessError):
cmd('config --system pytest.key')
assert cmd('config --global pytest.key').rstrip() == 'global'
assert cmd('config --local pytest.key').rstrip() == 'local'
def test_delete_cmd_global():
# west config -d --global should only delete from global
cmd('config --system pytest.key system')
cmd('config --global pytest.key global')
cmd('config --local pytest.key local')
cmd('config -d --global pytest.key')
assert cmd('config --system pytest.key').rstrip() == 'system'
with pytest.raises(subprocess.CalledProcessError):
cmd('config --global pytest.key')
assert cmd('config --local pytest.key').rstrip() == 'local'
def test_delete_cmd_local():
# west config -d --local should only delete from local
cmd('config --system pytest.key system')
cmd('config --global pytest.key global')
cmd('config --local pytest.key local')
cmd('config -d --local pytest.key')
assert cmd('config --system pytest.key').rstrip() == 'system'
assert cmd('config --global pytest.key').rstrip() == 'global'
with pytest.raises(subprocess.CalledProcessError):
cmd('config --local pytest.key')
def test_delete_cmd_error():
# Verify illegal combinations of flags error out.
with pytest.raises(subprocess.CalledProcessError) as e:
cmd('config -l -d pytest.key')
assert '-l cannot be combined with -d or -D' in str(e)
with pytest.raises(subprocess.CalledProcessError) as e:
cmd('config -l -D pytest.key')
assert '-l cannot be combined with -d or -D' in str(e)
with pytest.raises(subprocess.CalledProcessError) as e:
cmd('config -d -D pytest.key')
assert '-d cannot be combined with -D' in str(e)
def test_default_config():
# Writing to a value without a config destination should default
# to --local.
cmd('config pytest.local foo')
assert cmd('config pytest.local').rstrip() == 'foo'
assert cmd('config --local pytest.local').rstrip() == 'foo'
assert cfg(f=ALL)['pytest']['local'] == 'foo'
assert 'pytest' not in cfg(f=SYSTEM)
assert 'pytest' not in cfg(f=GLOBAL)
assert cfg(f=LOCAL)['pytest']['local'] == 'foo'
def test_config_precedence():
# Verify that local settings take precedence over global ones,
# but that both values are still available, and that setting
# either doesn't affect system settings.
cmd('config --global pytest.precedence global')
cmd('config --local pytest.precedence local')
assert cmd('config --global pytest.precedence').rstrip() == 'global'
assert cmd('config --local pytest.precedence').rstrip() == 'local'
assert cmd('config pytest.precedence').rstrip() == 'local'
assert cfg(f=ALL)['pytest']['precedence'] == 'local'
assert 'pytest' not in cfg(f=SYSTEM)
assert cfg(f=GLOBAL)['pytest']['precedence'] == 'global'
assert cfg(f=LOCAL)['pytest']['precedence'] == 'local'
def test_config_missing_key():
with pytest.raises(subprocess.CalledProcessError) as e:
cmd('config pytest')
assert str(e) == 'west config: error: missing key, please invoke ' \
'as: west config <section>.<key>\n'
def test_unset_config():
# Getting unset configuration options should raise an error.
# With verbose output, the exact missing option should be printed.
with pytest.raises(subprocess.CalledProcessError) as e:
cmd('-v config pytest.missing')
assert 'pytest.missing is unset' in str(e)
def test_no_args():
with pytest.raises(subprocess.CalledProcessError) as e:
cmd('config')
assert 'missing argument name' in str(e)
def test_list():
def sorted_list(other_args=''):
return list(sorted(cmd('config -l ' + other_args).splitlines()))
with pytest.raises(subprocess.CalledProcessError) as e:
cmd('config -l pytest.foo')
assert '-l cannot be combined with name argument' in str(e)
assert cmd('config -l').strip() == ''
cmd('config pytest.foo who')
assert sorted_list() == ['pytest.foo=who']
cmd('config pytest.bar what')
assert sorted_list() == ['pytest.bar=what',
'pytest.foo=who']
cmd('config --global pytest.baz where')
assert sorted_list() == ['pytest.bar=what',
'pytest.baz=where',
'pytest.foo=who']
assert sorted_list('--system') == []
assert sorted_list('--global') == ['pytest.baz=where']
assert sorted_list('--local') == ['pytest.bar=what',
'pytest.foo=who']
def test_round_trip():
cmd('config pytest.foo bar,baz')
assert cmd('config pytest.foo').strip() == 'bar,baz'