diff --git a/lisa/env.py b/lisa/env.py index 0e2396067840798c0a1bf0013c579637e6d9cbe9..7e752ebef3de3dad5a0b4d2d534849657fec0d0e 100644 --- a/lisa/env.py +++ b/lisa/env.py @@ -113,9 +113,8 @@ class TargetConf(MultiSrcConf, HideExekallID): target-conf: name: myboard """ - YAML_MAP_TOP_LEVEL_KEY = 'target-conf' - STRUCTURE = TopLevelKeyDesc(YAML_MAP_TOP_LEVEL_KEY, 'target connection settings', ( + STRUCTURE = TopLevelKeyDesc('target-conf', 'target connection settings', ( KeyDesc('name', 'Board name, free-form value only used to embelish logs', [str]), KeyDesc('kind', 'Target kind. Can be "linux" (ssh) or "android" (adb)', [str]), diff --git a/lisa/platforms/platinfo.py b/lisa/platforms/platinfo.py index c61b745a1b6780f840245eb0d030ceef8434692a..65e72d7f85ee29ec232fe3b7e2a0410a5f34e3fa 100644 --- a/lisa/platforms/platinfo.py +++ b/lisa/platforms/platinfo.py @@ -40,12 +40,11 @@ class PlatformInfo(MultiSrcConf, HideExekallID): {generated_help} """ - YAML_MAP_TOP_LEVEL_KEY = 'platform-info' # we could use mypy.subtypes.is_subtype and use the infrastructure provided # by typing module, but adding an external dependency is overkill for what # we need. - STRUCTURE = TopLevelKeyDesc(YAML_MAP_TOP_LEVEL_KEY, 'Platform-specific information', ( + STRUCTURE = TopLevelKeyDesc('platform-info', 'Platform-specific information', ( LevelKeyDesc('rtapp', 'RTapp configuration', ( KeyDesc('calib', 'RTapp calibration dictionary', [IntIntDict]), )), diff --git a/lisa/tests/lisa/test_conf.py b/lisa/tests/lisa/test_conf.py new file mode 100644 index 0000000000000000000000000000000000000000..66c5b4ae41e9f3dc736006d33cdb200809b14a09 --- /dev/null +++ b/lisa/tests/lisa/test_conf.py @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2018, ARM Limited and contributors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +import os +import copy +from unittest import TestCase + +from lisa.utils import MultiSrcConf, KeyDesc, LevelKeyDesc, TopLevelKeyDesc, IntList +from lisa.tests.lisa.utils import StorageTestCase, HOST_PLAT_INFO, HOST_TARGET_CONF + +""" A test suite for the MultiSrcConf subclasses.""" + + +class TestMultiSrcConfBase: + """ + A test class that exercise various APIs of MultiSrcConf + """ + def test_serialization(self): + path = os.path.join(self.res_dir, "conf.serialized.yml") + self.conf.to_path(path) + self.conf.from_path(path) + + def test_conf_file(self): + path = os.path.join(self.res_dir, "conf.yml") + self.conf.to_yaml_map(path) + self.conf.from_yaml_map(path) + + def test_add_src(self): + updated_conf = copy.deepcopy(self.conf) + # Add the same values in a new source. This is guaranteed to be valid + updated_conf.add_src('foo', self.conf) + self.assertEqual(dict(updated_conf), dict(self.conf)) + + def test_disallowed_key(self): + with self.assertRaises(KeyError): + self.conf['this-key-does-not-exists-and-is-not-allowed'] + + def test_copy(self): + self.assertEqual(dict(self.conf), dict(copy.copy(self.conf))) + + def test_deepcopy(self): + self.assertEqual(dict(self.conf), dict(copy.deepcopy(self.conf))) + +class TestPlatformInfo(StorageTestCase, TestMultiSrcConfBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Make copies to avoid mutating the original one + self.conf = copy.copy(HOST_PLAT_INFO) + +class TestTargetConf(StorageTestCase, TestMultiSrcConfBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Make copies to avoid mutating the original one + self.conf = copy.copy(HOST_TARGET_CONF) + +INTERNAL_STRUCTURE = ( + KeyDesc('foo', 'foo help', [int]), + KeyDesc('bar', 'bar help', [IntList]), + KeyDesc('multitypes', 'multitypes help', [IntList, str, None]), +) + +class TestConf(MultiSrcConf): + STRUCTURE = TopLevelKeyDesc('lisa-self-test-test-conf', 'lisa self test', + INTERNAL_STRUCTURE + ) + +class TestConfWithDefault(MultiSrcConf): + STRUCTURE = TopLevelKeyDesc('lisa-self-test-test-conf-with-default', 'lisa self test', + INTERNAL_STRUCTURE + ) + + DEFAULT_SRC = {'bar': [0, 1, 2]} + +class TestMultiSrcConf(TestMultiSrcConfBase): + def test_add_src_one_key(self): + conf = copy.deepcopy(self.conf) + conf_src = {'foo': 22} + + conf.add_src('mysrc', conf_src) + + goal = dict(self.conf) + goal.update(conf_src) + self.assertEqual(dict(conf), goal) + + self.assertEqual(conf.resolve_src('foo'), 'mysrc') + + def test_disallowed_val(self): + with self.assertRaises(TypeError): + self.conf.add_src('bar', ['a', 'b']) + + def test_multitypes(self): + conf = copy.deepcopy(self.conf) + conf.add_src('mysrc', {'multitypes': 'a'}) + conf.add_src('mysrc', {'multitypes': [1, 2]}) + conf.add_src('mysrc', {'multitypes': None}) + +class TestTestConf(StorageTestCase, TestMultiSrcConf): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.conf = TestConf() + + def test_unset_key(self): + with self.assertRaises(KeyError): + self.conf['foo'] + + def test_force_src_nested(self): + conf = copy.deepcopy(self.conf) + conf.add_src('mysrc', {'bar': [6,7]}) + + # Check without any actual source + conf.force_src_nested({ + 'bar': ['src-that-does-not-exist', 'another-one-that-does-not-exists'], + }) + with self.assertRaises(KeyError): + conf['bar'] + + # Check the first existing source is taken + conf.force_src_nested({ + 'bar': ['src-that-does-not-exist', 'mysrc2', 'mysrc', 'this-src-does-not-exist', 'mysrc'], + }) + self.assertEqual(conf['bar'], [6, 7]) + + # Add one source that was specified earlier, and that has priority + conf.add_src('mysrc2', {'bar': [99,100]}) + self.assertEqual(conf['bar'], [99, 100]) + + # Reset the source priority, so the last source added will win + conf.force_src('bar', None) + self.assertEqual(conf['bar'], [99, 100]) + + src_map = conf.get_src_map('bar') + self.assertEqual(list(src_map.keys()) , ['mysrc2', 'mysrc']) + + +class TestTestConfWithDefault(StorageTestCase, TestMultiSrcConf): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.conf = TestConfWithDefault() + + def test_default_src(self): + self.assertEqual(dict(self.conf), dict(TestConfWithDefault.DEFAULT_SRC)) + + def test_add_src_one_key_fallback(self): + conf = copy.deepcopy(self.conf) + # Since there is a default value for this key, it should not impact the + # result + conf_src = {'bar': [1,2]} + + conf.add_src('bar', conf_src, fallback=True) + + self.assertEqual(dict(conf), dict(self.conf)) + self.assertEqual(conf.resolve_src('bar'), 'default') + +# vim :set tabstop=4 shiftwidth=4 textwidth=80 expandtab diff --git a/lisa/utils.py b/lisa/utils.py index a1d5fed111d7a7d39e09d1a9cb34271ef286d5c5..d7fafff05791fe336759fe184664e63429edfed3 100644 --- a/lisa/utils.py +++ b/lisa/utils.py @@ -420,13 +420,6 @@ Serializable._init_yaml() class SerializableConfABC(Serializable, abc.ABC): _registered_toplevel_keys = {} - @abc.abstractmethod - def YAML_MAP_TOP_LEVEL_KEY(): - """Top-level key used when dumping and loading the data to a YAML file. - This allows using a single file for different purposes. - """ - pass - @abc.abstractmethod def to_map(self): raise NotImplementedError @@ -440,37 +433,41 @@ class SerializableConfABC(Serializable, abc.ABC): def from_yaml_map(cls, path): """ Allow reloading from a plain mapping, to avoid having to specify a tag - in the configuration file. The content is hosted under a top-level key - specified in :attr:`YAML_MAP_TOP_LEVEL_KEY`. + in the configuration file. The content is hosted under the top-level + key specified in ``STRUCTURE``. """ + toplevel_key = cls.STRUCTURE.name + mapping = cls._from_path(path, fmt='yaml') assert isinstance(mapping, Mapping) - data = mapping[cls.YAML_MAP_TOP_LEVEL_KEY] + data = mapping[toplevel_key] # "unwrap" an extra layer of toplevel key, to play well with !include - if len(data) == 1 and cls.YAML_MAP_TOP_LEVEL_KEY in data.keys(): - data = data[cls.YAML_MAP_TOP_LEVEL_KEY] + if len(data) == 1 and toplevel_key in data.keys(): + data = data[toplevel_key] return cls.from_map(data) def to_yaml_map(self, path): data = self.to_map() - mapping = {self.YAML_MAP_TOP_LEVEL_KEY: data} + mapping = {self.STRUCTURE.name: data} return self._to_path(mapping, path, fmt='yaml') # Only used with Python >= 3.6, but since that is just a sanity check it # should be okay @classmethod def __init_subclass__(cls, **kwargs): - # Ensure uniqueness of toplevel key - toplevel_key = cls.YAML_MAP_TOP_LEVEL_KEY - if toplevel_key in cls._registered_toplevel_keys: - raise RuntimeError('Class {name} cannot reuse YAML_MAP_TOP_LEVEL_KEY="{key}" as it is already used by {user}'.format( - name = cls.__qualname__, - key = toplevel_key, - user = cls._registered_toplevel_keys[toplevel_key] - )) - else: - cls._registered_toplevel_keys[toplevel_key] = cls + # Ignore abstract classes, since there can be no instance of them + if not inspect.isabstract(cls): + # Ensure uniqueness of toplevel key + toplevel_key = cls.STRUCTURE.name + if toplevel_key in cls._registered_toplevel_keys: + raise RuntimeError('Class {name} cannot reuse top level key "{key}" as it is already used by {user}'.format( + name = cls.__qualname__, + key = toplevel_key, + user = cls._registered_toplevel_keys[toplevel_key] + )) + else: + cls._registered_toplevel_keys[toplevel_key] = cls super().__init_subclass__(**kwargs)