|
9 | 9 | from typing import Any |
10 | 10 |
|
11 | 11 | import pytest |
12 | | -from conftest import chdir, cmd, cmd_raises, update_env |
| 12 | +from conftest import WINDOWS, chdir, cmd, cmd_raises, tmp_west_topdir, update_env |
13 | 13 |
|
14 | 14 | from west import configuration as config |
| 15 | +from west.configuration import MalformedConfig |
15 | 16 | from west.util import PathType, WestNotFound |
16 | 17 |
|
17 | 18 | SYSTEM = config.ConfigFile.SYSTEM |
@@ -619,6 +620,154 @@ def test_config_precedence(): |
619 | 620 | assert cfg(f=LOCAL)['pytest']['precedence'] == 'local' |
620 | 621 |
|
621 | 622 |
|
| 623 | +def test_config_multiple(config_tmpdir): |
| 624 | + # Verify that local settings take precedence over global ones, |
| 625 | + # but that both values are still available, and that setting |
| 626 | + # either doesn't affect system settings. |
| 627 | + def write_config(config_file, section, key1, value1, key2, value2): |
| 628 | + config_file.parent.mkdir(exist_ok=True) |
| 629 | + |
| 630 | + content = textwrap.dedent(f''' |
| 631 | + [{section}] |
| 632 | + {key1} = {value1} |
| 633 | + {key2} = {value2} |
| 634 | + ''') |
| 635 | + |
| 636 | + with open(config_file, 'w') as conf: |
| 637 | + conf.write(content) |
| 638 | + |
| 639 | + # helper function to assert multiple config values |
| 640 | + def run_and_assert(expected_values: dict[str, dict[str, str]]): |
| 641 | + for scope, meta in expected_values.items(): |
| 642 | + for flags, expected in meta.items(): |
| 643 | + stdout = cmd(f'config --{scope} {flags}').rstrip() |
| 644 | + if type(expected) is list: |
| 645 | + stdout = stdout.splitlines() |
| 646 | + assert stdout == expected, f"{scope} {flags}: {expected} =! {stdout}" |
| 647 | + |
| 648 | + # config file paths |
| 649 | + config_dir = pathlib.Path(config_tmpdir) / 'configs' |
| 650 | + config_s1 = config_dir / 'system 1' |
| 651 | + config_s2 = config_dir / 'system 2' |
| 652 | + config_g1 = config_dir / 'global 1' |
| 653 | + config_g2 = config_dir / 'global 2' |
| 654 | + config_l1 = config_dir / 'local 1' |
| 655 | + config_l2 = config_dir / 'local 2' |
| 656 | + |
| 657 | + # create some configs with |
| 658 | + # - some individual option per config file (s1/s2/g1/g2/l1/l2)) |
| 659 | + # - the same option (s/g/l) defined in multiple configs |
| 660 | + write_config(config_s1, 'sec', 's', '1 !"$&/()=?', 's1', '1 !"$&/()=?') |
| 661 | + write_config(config_s2, 'sec', 's', '2', 's2', '2') |
| 662 | + write_config(config_g1, 'sec', 'g', '1', 'g1', '1') |
| 663 | + write_config(config_g2, 'sec', 'g', '2', 'g2', '2') |
| 664 | + write_config(config_l1, 'sec', 'l', '1', 'l1', '1') |
| 665 | + write_config(config_l2, 'sec', 'l', '2', 'l2', '2') |
| 666 | + |
| 667 | + # config file without read permission (does not work on Windows) |
| 668 | + if not WINDOWS: |
| 669 | + config_non_readable = config_dir / 'non-readable' |
| 670 | + config_non_readable.touch() |
| 671 | + config_non_readable.chmod(0o000) |
| 672 | + with update_env({'WEST_CONFIG_GLOBAL': f'{config_g1}{os.pathsep}{config_non_readable}'}): |
| 673 | + _, stderr = cmd_raises('config --global some.section', MalformedConfig) |
| 674 | + expected = f"Error while reading one of '{[str(config_g1), str(config_non_readable)]}'" |
| 675 | + assert expected in stderr |
| 676 | + |
| 677 | + # specify multiple configs for each config level (separated by os.pathsep) |
| 678 | + os.environ["WEST_CONFIG_GLOBAL"] = f'{config_g1}{os.pathsep}{config_g2}' |
| 679 | + os.environ["WEST_CONFIG_SYSTEM"] = f'{config_s1}{os.pathsep}{config_s2}' |
| 680 | + os.environ["WEST_CONFIG_LOCAL"] = f'{config_l1}{os.pathsep}{config_l2}' |
| 681 | + |
| 682 | + # check options from individual files and that options from latter configs override |
| 683 | + expected = { |
| 684 | + 'system': {'sec.s1': '1 !"$&/()=?', 'sec.s2': '2', 'sec.s': '2'}, |
| 685 | + 'global': {'sec.g1': '1', 'sec.g2': '2', 'sec.g': '2'}, |
| 686 | + 'local': {'sec.l1': '1', 'sec.l2': '2', 'sec.l': '2'}, |
| 687 | + } |
| 688 | + run_and_assert(expected) |
| 689 | + |
| 690 | + # check that list-paths gives correct output |
| 691 | + expected = { |
| 692 | + 'system': {'--list-paths': [str(config_s1), str(config_s2)]}, |
| 693 | + 'global': {'--list-paths': [str(config_g1), str(config_g2)]}, |
| 694 | + 'local': {'--list-paths': [str(config_l1), str(config_l2)]}, |
| 695 | + } |
| 696 | + run_and_assert(expected) |
| 697 | + |
| 698 | + # writing not possible if multiple configs are used |
| 699 | + _, stderr = cmd_raises('config --local sec.l3 3', ValueError) |
| 700 | + assert f'Cannot set value if multiple configs in use: {[config_l1, config_l2]}' in stderr |
| 701 | + |
| 702 | + |
| 703 | +@pytest.mark.parametrize("location", [LOCAL, GLOBAL, SYSTEM]) |
| 704 | +def test_config_multiple_write(location): |
| 705 | + # write to a config with a single config file must work, even if other |
| 706 | + # locations have multiple configs in use |
| 707 | + flag = west_flag[location] |
| 708 | + env_var = west_env[location] |
| 709 | + |
| 710 | + configs_dir = pathlib.Path("configs") |
| 711 | + config1 = (configs_dir / 'config 1').resolve() |
| 712 | + config2 = (configs_dir / 'config 2').resolve() |
| 713 | + config3 = (configs_dir / 'config 3').resolve() |
| 714 | + |
| 715 | + env = {west_env[location]: f'{config1}'} |
| 716 | + other_locations = [c for c in [LOCAL, GLOBAL, SYSTEM] if c != location] |
| 717 | + for loc in other_locations: |
| 718 | + env[west_env[loc]] = f'{config2}{os.pathsep}{config3}' |
| 719 | + |
| 720 | + with update_env(env): |
| 721 | + cmd(f'config {flag} key.value {env_var}') |
| 722 | + stdout = cmd(f'config {flag} key.value') |
| 723 | + assert [env_var] == stdout.rstrip().splitlines() |
| 724 | + |
| 725 | + |
| 726 | +@pytest.mark.parametrize("location", [LOCAL, GLOBAL, SYSTEM]) |
| 727 | +def test_config_multiple_relative(location): |
| 728 | + # specify multiple configs for each config level (separated by os.pathsep). |
| 729 | + # The paths may be relative relative paths, which are always anchored to |
| 730 | + # west topdir. For the test, the cwd is changed to another cwd to ensure |
| 731 | + # that relative paths are anchored correctly. |
| 732 | + flag = west_flag[location] |
| 733 | + env_var = west_env[location] |
| 734 | + |
| 735 | + msg = "'{file}' is relative but 'west topdir' is not defined" |
| 736 | + |
| 737 | + # create some configs |
| 738 | + configs_dir = pathlib.Path('config') |
| 739 | + configs_dir.mkdir() |
| 740 | + config1 = (configs_dir / 'config 1').resolve() |
| 741 | + config2 = (configs_dir / 'config 2').resolve() |
| 742 | + config1.touch() |
| 743 | + config2.touch() |
| 744 | + |
| 745 | + west_topdir = pathlib.Path.cwd() |
| 746 | + cwd = west_topdir / 'any' / 'other cwd' |
| 747 | + cwd.mkdir(parents=True) |
| 748 | + with chdir(cwd): |
| 749 | + config2_rel = config2.relative_to(west_topdir) |
| 750 | + command = f'config {flag} --list-paths' |
| 751 | + env_value = f'{config1}{os.pathsep}{config2_rel}' |
| 752 | + with update_env({env_var: env_value}): |
| 753 | + # cannot anchor relative path if no west topdir exists |
| 754 | + exc, _ = cmd_raises(command, WestNotFound) |
| 755 | + assert msg.format(file=config2_rel) in str(exc.value) |
| 756 | + |
| 757 | + # relative paths are anchored to west topdir |
| 758 | + with tmp_west_topdir(west_topdir): |
| 759 | + stdout = cmd(command) |
| 760 | + assert [str(config1), str(config2)] == stdout.rstrip().splitlines() |
| 761 | + |
| 762 | + # if a wrong separator is used, no config file must be found |
| 763 | + wrong_sep = ':' if WINDOWS else ';' |
| 764 | + env_value = f'{config1}{wrong_sep}{config2_rel}' |
| 765 | + with update_env({env_var: env_value}): |
| 766 | + # no path is listed |
| 767 | + stdout = cmd(command) |
| 768 | + assert not stdout |
| 769 | + |
| 770 | + |
622 | 771 | def test_config_missing_key(): |
623 | 772 | _, err_msg = cmd_raises('config pytest', SystemExit) |
624 | 773 | assert 'invalid configuration option "pytest"; expected "section.key" format' in err_msg |
|
0 commit comments