diff --git a/tools/build_api.py b/tools/build_api.py index f1980b72d83..62fff74c874 100644 --- a/tools/build_api.py +++ b/tools/build_api.py @@ -276,7 +276,8 @@ def get_mbed_official_release(version): def prepare_toolchain(src_paths, target, toolchain_name, macros=None, options=None, clean=False, jobs=1, notify=None, silent=False, verbose=False, - extra_verbose=False, config=None): + extra_verbose=False, config=None, + app_config=None): """ Prepares resource related objects - toolchain, target, config Positional arguments: @@ -294,6 +295,7 @@ def prepare_toolchain(src_paths, target, toolchain_name, verbose - Write the actual tools command lines used if True extra_verbose - even more output! config - a Config object to use instead of creating one + app_config - location of a chosen mbed_app.json file """ # We need to remove all paths which are repeated to avoid @@ -301,7 +303,7 @@ def prepare_toolchain(src_paths, target, toolchain_name, src_paths = [src_paths[0]] + list(set(src_paths[1:])) # If the configuration object was not yet created, create it now - config = config or Config(target, src_paths) + config = config or Config(target, src_paths, app_config=app_config) # If the 'target' argument is a string, convert it to a target instance if isinstance(target, basestring): @@ -369,7 +371,8 @@ def build_project(src_paths, build_path, target, toolchain_name, clean=False, notify=None, verbose=False, name=None, macros=None, inc_dirs=None, jobs=1, silent=False, report=None, properties=None, project_id=None, - project_description=None, extra_verbose=False, config=None): + project_description=None, extra_verbose=False, config=None, + app_config=None): """ Build a project. A project may be a test or a user program. Positional arguments: @@ -397,6 +400,7 @@ def build_project(src_paths, build_path, target, toolchain_name, project_description - the human-readable version of what this thing does extra_verbose - even more output! config - a Config object to use instead of creating one + app_config - location of a chosen mbed_app.json file """ # Convert src_path to a list if needed @@ -415,7 +419,7 @@ def build_project(src_paths, build_path, target, toolchain_name, toolchain = prepare_toolchain( src_paths, target, toolchain_name, macros=macros, options=options, clean=clean, jobs=jobs, notify=notify, silent=silent, verbose=verbose, - extra_verbose=extra_verbose, config=config) + extra_verbose=extra_verbose, config=config, app_config=app_config) # The first path will give the name to the library if name is None: @@ -489,7 +493,7 @@ def build_library(src_paths, build_path, target, toolchain_name, archive=True, notify=None, verbose=False, macros=None, inc_dirs=None, jobs=1, silent=False, report=None, properties=None, extra_verbose=False, project_id=None, - remove_config_header_file=False): + remove_config_header_file=False, app_config=None): """ Build a library Positional arguments: @@ -516,6 +520,7 @@ def build_library(src_paths, build_path, target, toolchain_name, extra_verbose - even more output! project_id - the name that goes in the report remove_config_header_file - delete config header file when done building + app_config - location of a chosen mbed_app.json file """ # Convert src_path to a list if needed @@ -539,7 +544,7 @@ def build_library(src_paths, build_path, target, toolchain_name, toolchain = prepare_toolchain( src_paths, target, toolchain_name, macros=macros, options=options, clean=clean, jobs=jobs, notify=notify, silent=silent, verbose=verbose, - extra_verbose=extra_verbose) + extra_verbose=extra_verbose, app_config=app_config) # The first path will give the name to the library if name is None: diff --git a/tools/config.py b/tools/config.py index a4766b3e2db..03ce5309a69 100644 --- a/tools/config.py +++ b/tools/config.py @@ -350,7 +350,7 @@ class Config(object): "UVISOR", "BLE", "CLIENT", "IPV4", "IPV6", "COMMON_PAL", "STORAGE" ] - def __init__(self, target, top_level_dirs=None): + def __init__(self, target, top_level_dirs=None, app_config=None): """Construct a mbed configuration Positional arguments: @@ -359,7 +359,8 @@ def __init__(self, target, top_level_dirs=None): Keyword argumets: top_level_dirs - a list of top level source directories (where - mbed_abb_config.json could be found) + mbed_app_config.json could be found) + app_config - location of a chosen mbed_app.json file NOTE: Construction of a Config object will look for the application configuration file in top_level_dirs. If found once, it'll parse it and @@ -368,22 +369,24 @@ def __init__(self, target, top_level_dirs=None): exception is raised. top_level_dirs may be None (in this case, the constructor will not search for a configuration file) """ - app_config_location = None - for directory in top_level_dirs or []: - full_path = os.path.join(directory, self.__mbed_app_config_name) - if os.path.isfile(full_path): - if app_config_location is not None: - raise ConfigException("Duplicate '%s' file in '%s' and '%s'" - % (self.__mbed_app_config_name, - app_config_location, full_path)) - else: - app_config_location = full_path + app_config_location = app_config + if app_config_location is None: + for directory in top_level_dirs or []: + full_path = os.path.join(directory, self.__mbed_app_config_name) + if os.path.isfile(full_path): + if app_config_location is not None: + raise ConfigException("Duplicate '%s' file in '%s' and '%s'" + % (self.__mbed_app_config_name, + app_config_location, full_path)) + else: + app_config_location = full_path try: self.app_config_data = json_file_to_dict(app_config_location) \ if app_config_location else {} except ValueError as exc: sys.stderr.write(str(exc) + "\n") self.app_config_data = {} + # Check the keys in the application configuration data unknown_keys = set(self.app_config_data.keys()) - \ self.__allowed_keys["application"] diff --git a/tools/make.py b/tools/make.py index 7cf7ca4a48e..0541d5e7608 100644 --- a/tools/make.py +++ b/tools/make.py @@ -52,7 +52,7 @@ if __name__ == '__main__': # Parse Options - parser = get_default_options_parser() + parser = get_default_options_parser(add_app_config=True) group = parser.add_mutually_exclusive_group(required=False) group.add_argument("-p", type=argparse_many(test_known), @@ -274,7 +274,8 @@ silent=options.silent, macros=options.macros, jobs=options.jobs, - name=options.artifact_name) + name=options.artifact_name, + app_config=options.app_config) print 'Image: %s'% bin_file if options.disk: diff --git a/tools/options.py b/tools/options.py index 057394c7b8f..45f3cfb0160 100644 --- a/tools/options.py +++ b/tools/options.py @@ -18,9 +18,11 @@ from tools.toolchains import TOOLCHAINS from tools.targets import TARGET_NAMES from tools.utils import argparse_force_uppercase_type, \ - argparse_lowercase_hyphen_type, argparse_many + argparse_lowercase_hyphen_type, argparse_many, \ + argparse_filestring_type -def get_default_options_parser(add_clean=True, add_options=True): +def get_default_options_parser(add_clean=True, add_options=True, + add_app_config=False): """Create a new options parser with the default compiler options added Keyword arguments: @@ -80,4 +82,9 @@ def get_default_options_parser(add_clean=True, add_options=True): 'std-lib'], "build option")) + if add_app_config: + parser.add_argument("--app-config", default=None, dest="app_config", + type=argparse_filestring_type, + help="Path of an app configuration file (Default is to look for 'mbed_app.json')") + return parser diff --git a/tools/test.py b/tools/test.py index 39a4d62f3bc..101af3fb336 100644 --- a/tools/test.py +++ b/tools/test.py @@ -41,7 +41,7 @@ if __name__ == '__main__': try: # Parse Options - parser = get_default_options_parser() + parser = get_default_options_parser(add_app_config=True) parser.add_argument("-D", action="append", @@ -117,7 +117,8 @@ # Find all tests in the relevant paths for path in all_paths: - all_tests.update(find_tests(path, mcu, toolchain, options.options)) + all_tests.update(find_tests(path, mcu, toolchain, options.options, + app_config=options.app_config)) # Filter tests by name if specified if options.names: @@ -177,7 +178,8 @@ verbose=options.verbose, notify=notify, archive=False, - remove_config_header_file=True) + remove_config_header_file=True, + app_config=options.app_config) library_build_success = True except ToolException, e: @@ -203,7 +205,8 @@ verbose=options.verbose, notify=notify, jobs=options.jobs, - continue_on_build_fail=options.continue_on_build_fail) + continue_on_build_fail=options.continue_on_build_fail, + app_config=options.app_config) # If a path to a test spec is provided, write it to a file if options.test_spec: diff --git a/tools/test/build_api/build_api_test.py b/tools/test/build_api/build_api_test.py new file mode 100644 index 00000000000..97c2f1cf219 --- /dev/null +++ b/tools/test/build_api/build_api_test.py @@ -0,0 +1,188 @@ +""" +mbed SDK +Copyright (c) 2016 ARM Limited + +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 unittest +from mock import patch +from tools.build_api import prepare_toolchain, build_project, build_library + +""" +Tests for build_api.py +""" + +class BuildApiTests(unittest.TestCase): + """ + Test cases for Build Api + """ + + def setUp(self): + """ + Called before each test case + + :return: + """ + self.target = "K64F" + self.src_paths = ['.'] + self.toolchain_name = "ARM" + self.build_path = "build_path" + + def tearDown(self): + """ + Called after each test case + + :return: + """ + pass + + @patch('tools.config.Config.__init__') + def test_prepare_toolchain_app_config(self, mock_config_init): + """ + Test that prepare_toolchain uses app_config correctly + + :param mock_config_init: mock of Config __init__ + :return: + """ + app_config = "app_config" + mock_config_init.return_value = None + + prepare_toolchain(self.src_paths, self.target, self.toolchain_name, + app_config=app_config) + + mock_config_init.assert_called_with(self.target, self.src_paths, + app_config=app_config) + + @patch('tools.config.Config.__init__') + def test_prepare_toolchain_no_app_config(self, mock_config_init): + """ + Test that prepare_toolchain correctly deals with no app_config + + :param mock_config_init: mock of Config __init__ + :return: + """ + mock_config_init.return_value = None + + prepare_toolchain(self.src_paths, self.target, self.toolchain_name) + + mock_config_init.assert_called_with(self.target, self.src_paths, + app_config=None) + + @patch('tools.build_api.scan_resources') + @patch('tools.build_api.mkdir') + @patch('os.path.exists') + @patch('tools.build_api.prepare_toolchain') + def test_build_project_app_config(self, mock_prepare_toolchain, mock_exists, _, __): + """ + Test that build_project uses app_config correctly + + :param mock_prepare_toolchain: mock of function prepare_toolchain + :param mock_exists: mock of function os.path.exists + :param _: mock of function mkdir (not tested) + :param __: mock of function scan_resources (not tested) + :return: + """ + app_config = "app_config" + mock_exists.return_value = False + mock_prepare_toolchain().link_program.return_value = 1, 2 + + build_project(self.src_paths, self.build_path, self.target, + self.toolchain_name, app_config=app_config) + + args = mock_prepare_toolchain.call_args + self.assertTrue('app_config' in args[1], + "prepare_toolchain was not called with app_config") + self.assertEqual(args[1]['app_config'], app_config, + "prepare_toolchain was called with an incorrect app_config") + + @patch('tools.build_api.scan_resources') + @patch('tools.build_api.mkdir') + @patch('os.path.exists') + @patch('tools.build_api.prepare_toolchain') + def test_build_project_no_app_config(self, mock_prepare_toolchain, mock_exists, _, __): + """ + Test that build_project correctly deals with no app_config + + :param mock_prepare_toolchain: mock of function prepare_toolchain + :param mock_exists: mock of function os.path.exists + :param _: mock of function mkdir (not tested) + :param __: mock of function scan_resources (not tested) + :return: + """ + mock_exists.return_value = False + # Needed for the unpacking of the returned value + mock_prepare_toolchain().link_program.return_value = 1, 2 + + build_project(self.src_paths, self.build_path, self.target, + self.toolchain_name) + + args = mock_prepare_toolchain.call_args + self.assertTrue('app_config' in args[1], + "prepare_toolchain was not called with app_config") + self.assertEqual(args[1]['app_config'], None, + "prepare_toolchain was called with an incorrect app_config") + + @patch('tools.build_api.scan_resources') + @patch('tools.build_api.mkdir') + @patch('os.path.exists') + @patch('tools.build_api.prepare_toolchain') + def test_build_library_app_config(self, mock_prepare_toolchain, mock_exists, _, __): + """ + Test that build_library uses app_config correctly + + :param mock_prepare_toolchain: mock of function prepare_toolchain + :param mock_exists: mock of function os.path.exists + :param _: mock of function mkdir (not tested) + :param __: mock of function scan_resources (not tested) + :return: + """ + app_config = "app_config" + mock_exists.return_value = False + + build_library(self.src_paths, self.build_path, self.target, + self.toolchain_name, app_config=app_config) + + args = mock_prepare_toolchain.call_args + self.assertTrue('app_config' in args[1], + "prepare_toolchain was not called with app_config") + self.assertEqual(args[1]['app_config'], app_config, + "prepare_toolchain was called with an incorrect app_config") + + @patch('tools.build_api.scan_resources') + @patch('tools.build_api.mkdir') + @patch('os.path.exists') + @patch('tools.build_api.prepare_toolchain') + def test_build_library_no_app_config(self, mock_prepare_toolchain, mock_exists, _, __): + """ + Test that build_library correctly deals with no app_config + + :param mock_prepare_toolchain: mock of function prepare_toolchain + :param mock_exists: mock of function os.path.exists + :param _: mock of function mkdir (not tested) + :param __: mock of function scan_resources (not tested) + :return: + """ + mock_exists.return_value = False + + build_library(self.src_paths, self.build_path, self.target, + self.toolchain_name) + + args = mock_prepare_toolchain.call_args + self.assertTrue('app_config' in args[1], + "prepare_toolchain was not called with app_config") + self.assertEqual(args[1]['app_config'], None, + "prepare_toolchain was called with an incorrect app_config") + +if __name__ == '__main__': + unittest.main() diff --git a/tools/test/config/config_test.py b/tools/test/config/config_test.py new file mode 100644 index 00000000000..114f088c28e --- /dev/null +++ b/tools/test/config/config_test.py @@ -0,0 +1,132 @@ +""" +mbed SDK +Copyright (c) 2016 ARM Limited + +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.path +import unittest +from mock import patch +from tools.config import Config + +""" +Tests for config.py +""" + +class ConfigTests(unittest.TestCase): + """ + Test cases for Config class + """ + + def setUp(self): + """ + Called before each test case + + :return: + """ + self.target = "K64F" + + def tearDown(self): + """ + Called after each test case + + :return: + """ + pass + + @patch.object(Config, '_process_config_and_overrides') + @patch('tools.config.json_file_to_dict') + def test_init_app_config(self, mock_json_file_to_dict, _): + """ + Test that the initialisation correctly uses app_config + + :param mock_json_file_to_dict: mock of function json_file_to_dict + :param _: mock of function _process_config_and_overrides (not tested) + :return: + """ + app_config = "app_config" + mock_return = {'config': 'test'} + mock_json_file_to_dict.return_value = mock_return + + config = Config(self.target, app_config=app_config) + + mock_json_file_to_dict.assert_called_with(app_config) + self.assertEqual(config.app_config_data, mock_return, + "app_config_data should be set to the returned value") + + @patch.object(Config, '_process_config_and_overrides') + @patch('tools.config.json_file_to_dict') + def test_init_no_app_config(self, mock_json_file_to_dict, _): + """ + Test that the initialisation works without app config + + :param mock_json_file_to_dict: mock of function json_file_to_dict + :param _: patch of function _process_config_and_overrides (not tested) + :return: + """ + config = Config(self.target) + + mock_json_file_to_dict.assert_not_called() + self.assertEqual(config.app_config_data, {}, + "app_config_data should be set an empty dictionary") + + @patch.object(Config, '_process_config_and_overrides') + @patch('os.path.isfile') + @patch('tools.config.json_file_to_dict') + def test_init_no_app_config_with_dir(self, mock_json_file_to_dict, mock_isfile, _): + """ + Test that the initialisation works without app config and with a + specified top level directory + + :param mock_json_file_to_dict: mock of function json_file_to_dict + :param _: patch of function _process_config_and_overrides (not tested) + :return: + """ + directory = '.' + path = os.path.join('.', 'mbed_app.json') + mock_return = {'config': 'test'} + mock_json_file_to_dict.return_value = mock_return + mock_isfile.return_value = True + + config = Config(self.target, [directory]) + + mock_isfile.assert_called_with(path) + mock_json_file_to_dict.assert_called_once_with(path) + self.assertEqual(config.app_config_data, mock_return, + "app_config_data should be set to the returned value") + + @patch.object(Config, '_process_config_and_overrides') + @patch('tools.config.json_file_to_dict') + def test_init_override_app_config(self, mock_json_file_to_dict, _): + """ + Test that the initialisation uses app_config instead of top_level_dir + when both are specified + + :param mock_json_file_to_dict: mock of function json_file_to_dict + :param _: patch of function _process_config_and_overrides (not tested) + :return: + """ + app_config = "app_config" + directory = '.' + mock_return = {'config': 'test'} + mock_json_file_to_dict.return_value = mock_return + + config = Config(self.target, [directory], app_config=app_config) + + mock_json_file_to_dict.assert_called_once_with(app_config) + self.assertEqual(config.app_config_data, mock_return, + "app_config_data should be set to the returned value") + +if __name__ == '__main__': + unittest.main() diff --git a/tools/test/test_api/test_api_test.py b/tools/test/test_api/test_api_test.py new file mode 100644 index 00000000000..9f16941b04d --- /dev/null +++ b/tools/test/test_api/test_api_test.py @@ -0,0 +1,141 @@ +""" +mbed SDK +Copyright (c) 2016 ARM Limited + +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 unittest +from mock import patch +from tools.test_api import find_tests, build_tests + +""" +Tests for test_api.py +""" + +class TestApiTests(unittest.TestCase): + """ + Test cases for Test Api + """ + + def setUp(self): + """ + Called before each test case + + :return: + """ + self.base_dir = 'base_dir' + self.target = "K64F" + self.toolchain_name = "ARM" + + def tearDown(self): + """ + Called after each test case + + :return: + """ + pass + + @patch('tools.test_api.scan_resources') + @patch('tools.test_api.prepare_toolchain') + def test_find_tests_app_config(self, mock_prepare_toolchain, mock_scan_resources): + """ + Test find_tests for correct use of app_config + + :param mock_prepare_toolchain: mock of function prepare_toolchain + :param mock_scan_resources: mock of function scan_resources + :return: + """ + app_config = "app_config" + mock_scan_resources().inc_dirs.return_value = [] + + find_tests(self.base_dir, self.target, self.toolchain_name, app_config=app_config) + + args = mock_prepare_toolchain.call_args + self.assertTrue('app_config' in args[1], + "prepare_toolchain was not called with app_config") + self.assertEqual(args[1]['app_config'], app_config, + "prepare_toolchain was called with an incorrect app_config") + + @patch('tools.test_api.scan_resources') + @patch('tools.test_api.prepare_toolchain') + def test_find_tests_no_app_config(self, mock_prepare_toolchain, mock_scan_resources): + """ + Test find_tests correctly deals with no app_config + + :param mock_prepare_toolchain: mock of function prepare_toolchain + :param mock_scan_resources: mock of function scan_resources + :return: + """ + mock_scan_resources().inc_dirs.return_value = [] + + find_tests(self.base_dir, self.target, self.toolchain_name) + + args = mock_prepare_toolchain.call_args + self.assertTrue('app_config' in args[1], + "prepare_toolchain was not called with app_config") + self.assertEqual(args[1]['app_config'], None, + "prepare_toolchain was called with an incorrect app_config") + + @patch('tools.test_api.scan_resources') + @patch('tools.test_api.build_project') + def test_build_tests_app_config(self, mock_build_project, mock_scan_resources): + """ + Test build_tests for correct use of app_config + + :param mock_prepare_toolchain: mock of function prepare_toolchain + :param mock_scan_resources: mock of function scan_resources + :return: + """ + tests = {'test1': 'test1_path','test2': 'test2_path'} + src_paths = ['.'] + build_path = "build_path" + app_config = "app_config" + mock_build_project.return_value = "build_project" + + build_tests(tests, src_paths, build_path, self.target, self.toolchain_name, + app_config=app_config) + + arg_list = mock_build_project.call_args_list + for args in arg_list: + self.assertTrue('app_config' in args[1], + "build_tests was not called with app_config") + self.assertEqual(args[1]['app_config'], app_config, + "build_tests was called with an incorrect app_config") + + @patch('tools.test_api.scan_resources') + @patch('tools.test_api.build_project') + def test_build_tests_no_app_config(self, mock_build_project, mock_scan_resources): + """ + Test build_tests correctly deals with no app_config + + :param mock_prepare_toolchain: mock of function prepare_toolchain + :param mock_scan_resources: mock of function scan_resources + :return: + """ + tests = {'test1': 'test1_path', 'test2': 'test2_path'} + src_paths = ['.'] + build_path = "build_path" + mock_build_project.return_value = "build_project" + + build_tests(tests, src_paths, build_path, self.target, self.toolchain_name) + + arg_list = mock_build_project.call_args_list + for args in arg_list: + self.assertTrue('app_config' in args[1], + "build_tests was not called with app_config") + self.assertEqual(args[1]['app_config'], None, + "build_tests was called with an incorrect app_config") + +if __name__ == '__main__': + unittest.main() diff --git a/tools/test_api.py b/tools/test_api.py index 71be1fd6575..3c05788e2b4 100644 --- a/tools/test_api.py +++ b/tools/test_api.py @@ -1990,18 +1990,20 @@ def test_path_to_name(path, base): return "-".join(name_parts).lower() -def find_tests(base_dir, target_name, toolchain_name, options=None): +def find_tests(base_dir, target_name, toolchain_name, options=None, app_config=None): """ Finds all tests in a directory recursively base_dir: path to the directory to scan for tests (ex. 'path/to/project') target_name: name of the target to use for scanning (ex. 'K64F') toolchain_name: name of the toolchain to use for scanning (ex. 'GCC_ARM') options: Compile options to pass to the toolchain (ex. ['debug-info']) + app_config - location of a chosen mbed_app.json file """ tests = {} # Prepare the toolchain - toolchain = prepare_toolchain([base_dir], target_name, toolchain_name, options=options, silent=True) + toolchain = prepare_toolchain([base_dir], target_name, toolchain_name, options=options, + silent=True, app_config=app_config) # Scan the directory for paths to probe for 'TESTS' folders base_resources = scan_resources([base_dir], toolchain) @@ -2060,7 +2062,7 @@ def norm_relative_path(path, start): def build_tests(tests, base_source_paths, build_path, target, toolchain_name, options=None, clean=False, notify=None, verbose=False, jobs=1, macros=None, silent=False, report=None, properties=None, - continue_on_build_fail=False): + continue_on_build_fail=False, app_config=None): """Given the data structure from 'find_tests' and the typical build parameters, build all the tests @@ -2101,7 +2103,8 @@ def build_tests(tests, base_source_paths, build_path, target, toolchain_name, project_id=test_name, report=report, properties=properties, - verbose=verbose) + verbose=verbose, + app_config=app_config) except Exception, e: if not isinstance(e, NotSupportedException):