From 9687476ddaa78061cd74e98259f10bfc2d26d572 Mon Sep 17 00:00:00 2001 From: Jack Rosenthal Date: Thu, 28 Jan 2021 08:06:41 -0700 Subject: zephyr: zmake: add a test for BuildConfig objects Implement test coverage for BuildConfigs, including merging using `|` operator, and running `popen_cmake`. BUG=b:178642582 BRANCH=none TEST=pytest Cq-Depend: chromium:2654350 Signed-off-by: Jack Rosenthal Change-Id: I07d8e913af999dac4f66f904b9d53e974e57eb40 Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/ec/+/2656515 Reviewed-by: Simon Glass --- .gitignore | 1 + zephyr/zmake/tests/test_build_config.py | 184 ++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 zephyr/zmake/tests/test_build_config.py diff --git a/.gitignore b/.gitignore index 167dec2ca1..8e6fefd841 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ cscope.* *.pyc __pycache__ *.egg-info +.hypothesis/ diff --git a/zephyr/zmake/tests/test_build_config.py b/zephyr/zmake/tests/test_build_config.py new file mode 100644 index 0000000000..6b835fdca4 --- /dev/null +++ b/zephyr/zmake/tests/test_build_config.py @@ -0,0 +1,184 @@ +# Copyright 2021 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import argparse +import hypothesis +import hypothesis.strategies as st +import os +import pathlib +import pytest +import string +import tempfile + +import zmake.jobserver +import zmake.util as util +from zmake.build_config import BuildConfig + +# Strategies for use with hypothesis +filenames = st.text( + alphabet=set(string.printable) - {'/', ';'}, + min_size=1, + max_size=254) +paths = st.builds(lambda parts: pathlib.Path('/', *parts), + st.iterables(filenames, min_size=1)) +config_keys = st.text(alphabet=set(string.ascii_uppercase) | {'_'}, min_size=1) +config_values = st.builds(str, st.just('y') | st.just('n') | st.integers()) +config_dicts = st.dictionaries(keys=config_keys, values=config_values) +config_dicts_at_least_one_entry = st.dictionaries(keys=config_keys, + values=config_values, + min_size=1) + +build_configs = st.builds(BuildConfig, + environ_defs=config_dicts, + cmake_defs=config_dicts, + kconfig_defs=config_dicts, + kconfig_files=st.lists(paths)) +build_configs_no_kconfig = st.builds(BuildConfig, + environ_defs=config_dicts, + cmake_defs=config_dicts) +build_configs_with_at_least_one_kconfig = st.builds( + BuildConfig, + environ_defs=config_dicts, + cmake_defs=config_dicts, + kconfig_defs=config_dicts_at_least_one_entry) + + +@hypothesis.given(st.data(), build_configs) +def test_merge(coins, combined): + """Test that when splitting a config in half and merging the two + halves, we get the original config back. + """ + def split(iterable): + left = [] + right = [] + bools = st.booleans() + for item in iterable: + if coins.draw(bools): + left.append(item) + else: + right.append(item) + return left, right + + # Split the original config into two + env1, env2 = split(combined.environ_defs.items()) + cmake1, cmake2 = split(combined.cmake_defs.items()) + kconf1, kconf2 = split(combined.kconfig_defs.items()) + files1, files2 = split(combined.kconfig_files) + + c1 = BuildConfig(environ_defs=dict(env1), cmake_defs=dict(cmake1), + kconfig_defs=dict(kconf1), kconfig_files=files1) + c2 = BuildConfig(environ_defs=dict(env2), cmake_defs=dict(cmake2), + kconfig_defs=dict(kconf2), kconfig_files=files2) + + # Merge the split configs + merged = c1 | c2 + + # Assert that the merged split configs is the original config + assert merged.environ_defs == combined.environ_defs + assert merged.cmake_defs == combined.cmake_defs + assert merged.kconfig_defs == combined.kconfig_defs + assert set(merged.kconfig_files) == set(combined.kconfig_files) + + +class FakeJobClient(zmake.jobserver.JobClient): + """Simple job client to capture argv/environ.""" + def __init__(self): + self.captured_argv = [] + self.captured_env = {} + + def get_job(self): + return zmake.jobserver.JobHandle(lambda: None) + + def popen(self, argv, env={}, **kwargs): + self.captured_argv = [str(arg) for arg in argv] + self.captured_env = {str(k): str(v) for k, v in env.items()} + + +def parse_cmake_args(argv): + """Parse command line arguments like cmake does. + + This is an intenionally minimal implementation, which only + understands the subset of arguments actually used by zmake. + + Args: + argv: The argument list. + + Returns: + A 2-tuple of a namespace from argparse and the corresponding + parsed Cmake definitions. + """ + assert argv[0] == '/usr/bin/cmake' + + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument('-S', dest='source_dir', type=pathlib.Path) + parser.add_argument('-B', dest='build_dir', type=pathlib.Path) + parser.add_argument('-G', dest='generator') + parser.add_argument('-D', dest='defs', action='append', default=[]) + args = parser.parse_args(argv[1:]) + + # Build the definition dictionary + cmake_defs = {} + for defn in args.defs: + key, sep, val = defn.partition('=') + if not sep: + val = '1' + assert key not in cmake_defs + cmake_defs[key] = val + + return args, cmake_defs + + +@hypothesis.given(build_configs_no_kconfig, paths, paths) +def test_popen_cmake_no_kconfig(conf, project_dir, build_dir): + """Test popen_cmake for a config with no kconfig definitions.""" + job_client = FakeJobClient() + conf.popen_cmake(job_client, project_dir, build_dir) + + args, cmake_defs = parse_cmake_args(job_client.captured_argv) + + assert cmake_defs == conf.cmake_defs + assert job_client.captured_env == conf.environ_defs + + +@hypothesis.given(build_configs_with_at_least_one_kconfig, paths, paths) +def test_popen_cmake_kconfig_but_no_file(conf, project_dir, build_dir): + """Test that running popen_cmake with Kconfig definitions to write + out, but no path to do so, should raise an error. + """ + job_client = FakeJobClient() + + with pytest.raises(ValueError): + conf.popen_cmake(job_client, project_dir, build_dir) + + +@hypothesis.given(build_configs, paths, paths) +def test_popen_cmake_kconfig(conf, project_dir, build_dir): + job_client = FakeJobClient() + + with tempfile.NamedTemporaryFile('w', delete=False) as f: + temp_path = f.name + + try: + conf.popen_cmake(job_client, project_dir, build_dir, + kconfig_path=pathlib.Path(temp_path)) + + args, cmake_defs = parse_cmake_args(job_client.captured_argv) + + expected_kconfig_files = set(str(f) for f in conf.kconfig_files) + expected_kconfig_files.add(temp_path) + + if expected_kconfig_files: + kconfig_files = set(cmake_defs.pop('CONF_FILE').split(';')) + else: + assert 'CONF_FILE' not in cmake_defs + kconfig_files = set() + + assert cmake_defs == conf.cmake_defs + assert job_client.captured_env == conf.environ_defs + assert kconfig_files == expected_kconfig_files + + kconfig_defs = util.read_kconfig_file(temp_path) + assert kconfig_defs == conf.kconfig_defs + finally: + os.unlink(temp_path) -- cgit v1.2.1