diff --git a/.github/workflows/basic_checks.yml b/.github/workflows/basic_checks.yml index 49d265f3c8..980096bc76 100644 --- a/.github/workflows/basic_checks.yml +++ b/.github/workflows/basic_checks.yml @@ -25,8 +25,11 @@ jobs: - name: install dependencies shell: bash + # TODO scancode 32.0 introduced significant breaking changes to the license + # detection output format: https://github.com/nexB/scancode-toolkit/releases/tag/v32.0.0 + # Need to update Mbed's scripts for the new format. run: | - pip install -U scancode-toolkit "click>=7,<8" + pip install -U "scancode-toolkit<32.0" "click>=7,<8" - name: license check @@ -43,10 +46,14 @@ jobs: | while read file; do cp --parents "${file}" SCANCODE; done ls SCANCODE scancode -l --json-pp scancode.json SCANCODE - python ./tools/test/ci/scancode-evaluate.py scancode.json || true - cat scancode-evaluate.log - COUNT=$(cat scancode-evaluate.log | grep 'File:' | grep -v 'SPDX' | wc -l) || true - if [ $COUNT = 0 ]; then + cd tools/python + + # Run the evaluation script, which may fail, and save its exit code. + EVALUATE_EXITCODE=0 + python -m scancode_evaluate.scancode_evaluate ../../scancode.json || EVALUATE_EXITCODE=$? + + cat scancode_evaluate.log + if [ "$EVALUATE_EXITCODE" = 0 ]; then echo "License check OK"; true; else @@ -158,25 +165,18 @@ jobs: name: Checkout repo uses: actions/checkout@v3 - - - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: install dependencies run: | - pip install -r tools/requirements.txt - pip install -r tools/test/requirements.txt + xargs sudo apt-get install -y < tools/requirements.apt.txt + xargs sudo apt-get install -y < tools/python/python_tests/requirements.apt.txt - - name: pytest + name: Python Tests run: | - set -x - coverage run -a -m pytest tools/test - python tools/test/pylint.py - coverage run -a tools/project.py -S | sed -n '/^Total/p' - coverage html + cd tools/python + ./run_python_tests.sh + pin-validation: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 3d4dce2de2..04908e0e0c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ venv/ *.egg *.egg-info dist -build +/build eggs parts bin diff --git a/tools/cmake/app.cmake b/tools/cmake/app.cmake index 7f8dfb5bf4..99d52d0cde 100644 --- a/tools/cmake/app.cmake +++ b/tools/cmake/app.cmake @@ -15,7 +15,7 @@ find_package(Python3 REQUIRED COMPONENTS Interpreter) include(CheckPythonPackage) # Check python packages -set(PYTHON_PACKAGES_TO_CHECK intelhex prettytable future jinja2 mbed_tools) +set(PYTHON_PACKAGES_TO_CHECK intelhex prettytable future jinja2) foreach(PACKAGE_NAME ${PYTHON_PACKAGES_TO_CHECK}) string(TOUPPER ${PACKAGE_NAME} PACKAGE_NAME_UCASE) # Ucase name needed for CMake variable string(TOLOWER ${PACKAGE_NAME} PACKAGE_NAME_LCASE) # Lcase name needed for import statement diff --git a/tools/cmake/mbed-run-greentea-test.in.cmake b/tools/cmake/mbed-run-greentea-test.in.cmake index 0dedcd8797..b5fdfe05c3 100644 --- a/tools/cmake/mbed-run-greentea-test.in.cmake +++ b/tools/cmake/mbed-run-greentea-test.in.cmake @@ -18,6 +18,6 @@ message("Executing: mbedhtrun ${MBEDHTRUN_ARGS_FOR_DISPLAY}") # Note: For this command, we need to survive mbedhtrun not being on the PATH, so we import the package and call the main function using "python -c" execute_process( - COMMAND @Python3_EXECUTABLE@ -c "import mbed_host_tests.mbedhtrun; exit(mbed_host_tests.mbedhtrun.main())" ${MBEDHTRUN_ARGS} - WORKING_DIRECTORY "@CMAKE_CURRENT_BINARY_DIR@" + COMMAND @Python3_EXECUTABLE@ -m mbed_host_tests.mbedhtrun ${MBEDHTRUN_ARGS} + WORKING_DIRECTORY "@mbed-os_SOURCE_DIR@/tools/python" COMMAND_ERROR_IS_FATAL ANY) \ No newline at end of file diff --git a/tools/cmake/mbed_generate_configuration.cmake b/tools/cmake/mbed_generate_configuration.cmake index 7e70b502e7..c0fe51579f 100644 --- a/tools/cmake/mbed_generate_configuration.cmake +++ b/tools/cmake/mbed_generate_configuration.cmake @@ -103,7 +103,7 @@ if(MBED_NEED_TO_RECONFIGURE) execute_process( COMMAND ${MBEDTOOLS_CONFIGURE_COMMAND} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../python RESULT_VARIABLE MBEDTOOLS_CONFIGURE_RESULT OUTPUT_VARIABLE MBEDTOOLS_CONFIGURE_OUTPUT ERROR_VARIABLE MBEDTOOLS_CONFIGURE_ERROR_OUTPUT diff --git a/tools/cmake/mbed_target_functions.cmake b/tools/cmake/mbed_target_functions.cmake index 527fb63946..826545ec28 100644 --- a/tools/cmake/mbed_target_functions.cmake +++ b/tools/cmake/mbed_target_functions.cmake @@ -53,11 +53,11 @@ function(mbed_generate_map_file target) TARGET ${target} POST_BUILD - COMMAND ${Python3_EXECUTABLE} ${mbed-os_SOURCE_DIR}/tools/memap.py + COMMAND ${Python3_EXECUTABLE} -m memap.memap -t ${MBED_TOOLCHAIN} ${CMAKE_CURRENT_BINARY_DIR}/${target}${CMAKE_EXECUTABLE_SUFFIX}.map --depth ${MBED_MEMAP_DEPTH} WORKING_DIRECTORY - ${CMAKE_CURRENT_BINARY_DIR} + ${mbed-os_SOURCE_DIR}/tools/python ) # generate json file @@ -66,13 +66,13 @@ function(mbed_generate_map_file target) TARGET ${target} POST_BUILD - COMMAND ${Python3_EXECUTABLE} ${mbed-os_SOURCE_DIR}/tools/memap.py + COMMAND ${Python3_EXECUTABLE} -m memap.memap -t ${MBED_TOOLCHAIN} ${CMAKE_CURRENT_BINARY_DIR}/${target}${CMAKE_EXECUTABLE_SUFFIX}.map --depth ${MBED_MEMAP_DEPTH} -e json -o ${CMAKE_CURRENT_BINARY_DIR}/${target}${CMAKE_EXECUTABLE_SUFFIX}.memmap.json WORKING_DIRECTORY - ${CMAKE_CURRENT_BINARY_DIR} + ${mbed-os_SOURCE_DIR}/tools/python ) endif() @@ -82,13 +82,13 @@ function(mbed_generate_map_file target) TARGET ${target} POST_BUILD - COMMAND ${Python3_EXECUTABLE} ${mbed-os_SOURCE_DIR}/tools/memap.py + COMMAND ${Python3_EXECUTABLE} -m memap.memap -t ${MBED_TOOLCHAIN} ${CMAKE_CURRENT_BINARY_DIR}/${target}${CMAKE_EXECUTABLE_SUFFIX}.map --depth ${MBED_MEMAP_DEPTH} -e html -o ${CMAKE_CURRENT_BINARY_DIR}/${target}${CMAKE_EXECUTABLE_SUFFIX}.memmap.html WORKING_DIRECTORY - ${CMAKE_CURRENT_BINARY_DIR} + ${mbed-os_SOURCE_DIR}/tools/python ) endif() endfunction() diff --git a/tools/cmake/upload_methods/UploadMethodMBED.cmake b/tools/cmake/upload_methods/UploadMethodMBED.cmake index ef434c3b29..d20fbb5be0 100644 --- a/tools/cmake/upload_methods/UploadMethodMBED.cmake +++ b/tools/cmake/upload_methods/UploadMethodMBED.cmake @@ -7,8 +7,7 @@ set(UPLOAD_SUPPORTS_DEBUG FALSE) ### Check if upload method can be enabled on this machine -check_python_package(mbed_os_tools HAVE_MBED_OS_TOOLS) -set(UPLOAD_MBED_FOUND ${HAVE_MBED_OS_TOOLS}) +set(UPLOAD_MBED_FOUND ${Python3_FOUND}) if(NOT DEFINED MBED_RESET_BAUDRATE) set(MBED_RESET_BAUDRATE 9600) @@ -20,11 +19,14 @@ set(MBED_TARGET_UID "" CACHE STRING "UID of mbed target to upload to if there ar function(gen_upload_target TARGET_NAME BIN_FILE) add_custom_target(flash-${TARGET_NAME} - COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/install_bin_file.py - ${BIN_FILE} - ${MBED_TARGET} - ${MBED_RESET_BAUDRATE} - ${MBED_TARGET_UID}) + COMMAND ${Python3_EXECUTABLE} -m install_bin_file + ${BIN_FILE} + ${MBED_TARGET} + ${MBED_RESET_BAUDRATE} + ${MBED_TARGET_UID} + WORKING_DIRECTORY + ${mbed-os_SOURCE_DIR}/tools/python + VERBATIM) add_dependencies(flash-${TARGET_NAME} ${TARGET_NAME}) diff --git a/tools/python/.gitignore b/tools/python/.gitignore new file mode 100644 index 0000000000..6b64481e09 --- /dev/null +++ b/tools/python/.gitignore @@ -0,0 +1,5 @@ +# Python coverage database +.coverage + +# Coverage report +htmlcov/ \ No newline at end of file diff --git a/tools/python/LICENSE b/tools/python/LICENSE new file mode 100644 index 0000000000..895657b9a9 --- /dev/null +++ b/tools/python/LICENSE @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. \ No newline at end of file diff --git a/tools/python/__init__.py b/tools/python/__init__.py new file mode 100644 index 0000000000..2bae17afc8 --- /dev/null +++ b/tools/python/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright (c) 2020-2023 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# \ No newline at end of file diff --git a/tools/cmake/upload_methods/install_bin_file.py b/tools/python/install_bin_file.py similarity index 91% rename from tools/cmake/upload_methods/install_bin_file.py rename to tools/python/install_bin_file.py index 1b5c6c432a..3fa2900618 100644 --- a/tools/cmake/upload_methods/install_bin_file.py +++ b/tools/python/install_bin_file.py @@ -42,7 +42,7 @@ def error(lines, code=-1): sys.exit(code) if len(sys.argv) < 3: - print("Error: Usage: python " + sys.argv[0] + " [Target UID for distinguishing multiple targets]") + print("Error: Usage: " + sys.argv[0] + " [Target UID for distinguishing multiple targets]") sys.exit(1) bin_file = sys.argv[1] @@ -81,7 +81,7 @@ elif len(all_connected) > 1: error_lines = ["There are multiple of the targeted board connected to the system. Which do you wish to flash?"] for target in all_connected: error_lines.append("Board: %s, Mount Point: %s, UID: %s" % (target['name'], target['mount'], target['uid'])) - error_lines.append("Please set MBED_TARGET_UID to the UID of the board you wish to flash.") + error_lines.append("Please set the CMake variable MBED_TARGET_UID to the UID of the board you wish to flash.") error(error_lines, 5) connected = all_connected[0] diff --git a/tools/python/mbed_greentea/README.md b/tools/python/mbed_greentea/README.md new file mode 100644 index 0000000000..dabd26b895 --- /dev/null +++ b/tools/python/mbed_greentea/README.md @@ -0,0 +1,227 @@ +# Development moved + +The development of Greentea has been moved into the [mbed-os-tools](../../src/mbed_os_tools) package. You can continue to use this module for legacy reasons, however all further development should be continued in the new package. + +------------- + +[![PyPI version](https://badge.fury.io/py/mbed-greentea.svg)](https://badge.fury.io/py/mbed-greentea) + +# Greentea - test automation for mbed +_**G**eneric **re**gression **en**vironment for **te**st **a**utomation_ + +## Introduction + +Greentea is the automated testing tool for mbed OS development. It automates the process of flashing mbed boards, driving the test and accumulating test results into test reports. Developers use it for local development as well as for automation in a Continuous Integration environment. + +This document should help you start using Greentea. Please see the [htrun documentation](https://github.com/ARMmbed/htrun), the tool Greentea uses to drive tests, for the technical details of the interactions between the platform and the host machine. + +Because Greentea is an open source project, we accept contributions! Please see our [contributing document](CONTRIBUTING.md) for more information. + +### Prerequistes + +Greentea requires [Python version 2.7](https://www.python.org/downloads/). It supports the following OSes: + +- Windows +- Linux (Ubuntu preferred) +- OS X (experimental) + +### Installing + +Tools that depend on Greentea usually install it. Determine if Greentea is already installed by running: +``` +$ mbedgt --version +1.2.5 +``` + +You can also install it manually via pip. + +``` +pip install mbed-greentea +``` + +## Test specification JSON format + +The Greentea test specification format decouples the tool from your build system. It provides important data, such as test names, paths to test binaries and the platform on which the binaries should run. + +Greentea automatically looks for files called `test_spec.json` in your working directory. You can also use the `--test-spec` argument to direct Greentea to a specific test specification file. + +When you use the `-t` / `--target` argument with the `--test-spec` argument, you can select which "build" should be used. In the example below, you could provide the arguments `--test-spec test_spec.json -t K64F-ARM` to only run that build's tests. + +### Example of test specification file + +In the below example, there are two defined builds: +* Build `K64F-ARM` for NXP `K64F` platform compiled with `ARMCC` compiler. +* Build `K64F-GCC` for NXP `K64F` platform compiled with `GCC ARM` compiler. + +```json +{ + "builds": { + "K64F-ARM": { + "platform": "K64F", + "toolchain": "ARM", + "base_path": "./BUILD/K64F/ARM", + "baud_rate": 9600, + "tests": { + "tests-mbedmicro-rtos-mbed-mail": { + "binaries": [ + { + "binary_type": "bootable", + "path": "./BUILD/K64F/ARM/tests-mbedmicro-rtos-mbed-mail.bin" + } + ] + }, + "tests-mbed_drivers-c_strings": { + "binaries": [ + { + "binary_type": "bootable", + "path": "./BUILD/K64F/ARM/tests-mbed_drivers-c_strings.bin" + } + ] + } + } + }, + "K64F-GCC": { + "platform": "K64F", + "toolchain": "GCC_ARM", + "base_path": "./BUILD/K64F/GCC_ARM", + "baud_rate": 9600, + "tests": { + "tests-mbedmicro-rtos-mbed-mail": { + "binaries": [ + { + "binary_type": "bootable", + "path": "./BUILD/K64F/GCC_ARM/tests-mbedmicro-rtos-mbed-mail.bin" + } + ] + } + } + } + } +} +``` + +The examples below use the above test specification file. + +## Command-line usage +This section highlights a few of the capabilities of the Greentea command-line interface. For a full list of the available options, please run `mbedgt --help`. + +Assume for the examples below that the above `test_spec.json` file is in the current directory. + +### Listing all tests +You can use the `-l` argument to list all available tests: + +``` +$ mbedgt -l +mbedgt: greentea test automation tool ver. 1.2.5 +mbedgt: using multiple test specifications from current directory! + using 'BUILD\tests\K64F\ARM\test_spec.json' + using 'BUILD\tests\K64F\GCC_ARM\test_spec.json' +mbedgt: available tests for built 'K64F-GCC_ARM', location 'BUILD/tests/K64F/GCC_ARM' + test 'tests-mbedmicro-rtos-mbed-mail' +mbedgt: available tests for built 'K64F-ARM', location 'BUILD/tests/K64F/ARM' + test 'tests-mbed_drivers-c_strings' + test 'tests-mbedmicro-rtos-mbed-mail' +``` + +### Executing all tests +The default action of Greentea using `mbedgt` is to execute all tests that are found in `test_spec.json` files. You can also add `-V` to make the output more verbose. + + +### Limiting tests +You can select test cases by name using the `-n` argument. This command executes all tests named `tests-mbedmicro-rtos-mbed-mail` from all builds in the test specification: +``` +$ mbedgt -n tests-mbedmicro-rtos-mbed-mail +``` + +When using the `-n` argument, you can use the `*` character at the end of a test name to match all tests that share a prefix. This command executes all tests that start with `tests*`: + +``` +$ mbedgt -n tests* +``` + +You can use the `-t` argument to select which build to use when finding tests. This command executes the test `tests-mbedmicro-rtos-mbed-mail` for the `K64F-ARM` build in the test specification: +``` +$ mbedgt -n tests-mbedmicro-rtos-mbed-mail -t K64F-ARM +``` + +You can use a comma (`,`) to separate test names (argument `-n`) and build names (argument `-t`). This command executes the tests `tests-mbedmicro-rtos-mbed-mail` and `tests-mbed_drivers-c_strings` for the `K64F-ARM` and `K64F-GCC_ARM` builds in the test specification: +``` +$ mbedgt -n tests-mbedmicro-rtos-mbed-mail,tests-mbed_drivers-c_strings -t K64F-ARM,K64F-GCC_ARM +``` + +### Selecting platforms +You can limit which boards Greentea uses for testing by using the `--use-tids` argument. + +``` +$ mbedgt --use-tids 02400203C3423E603EBEC3D8,024002031E031E6AE3FFE3D2 +``` + +Where ```02400203C3423E603EBEC3D8``` and ```024002031E031E6AE3FFE3D2``` are the target IDs of platforms attached to your system. + +You can view target IDs using the [mbed-ls](https://pypi.org/project/mbed-ls) tool (installed with Greentea). + +``` +$ mbedls ++--------------+---------------------+------------+------------+-------------------------+ +|platform_name |platform_name_unique |mount_point |serial_port |target_id | ++--------------+---------------------+------------+------------+-------------------------+ +|K64F |K64F[0] |E: |COM160 |024002031E031E6AE3FFE3D2 | +|K64F |K64F[1] |F: |COM162 |02400203C3423E603EBEC3D8 | +|LPC1768 |LPC1768[0] |G: |COM5 |1010ac87cfc4f23c4c57438d | ++--------------+---------------------+------------+------------+-------------------------+ +``` +In this case, you won't test one target, the LPC1768. + +### Testing on Fast Model FVPs + +Fast Models FVPs are software models for Arm reference design platfrom + +Greentea supports running test on Fast Models. And [mbed-fastmodel-agent](https://github.com/ARMmbed/mbed-fastmodel-agent) module is required for this purpose. + +The "--fm" option only available when the `mbed-fastmodel-agent` module is installed : + +You can run tests for FVP_MPS2_Cortex-M3 models, by '--fm' option: +``` +$ mbedgt --fm FVP_MPS2_M3:DEFAULT +``` + +Where ```FVP_MPS2_M3``` is the platfrom name for the ```FVP_MPS2_Cortex-M3``` models in mbed OS. + +And ```DEFAULT``` is the config to the Fast Model, you can find out using ```mbedfm``` command + +### Creating reports +Greentea supports a number of report formats. + +#### HTML +This creates an interactive HTML page with test results and logs. +``` +mbedgt --report-html html_report.html +``` + +#### JUnit +This creates an XML JUnit report, which you can use with popular Continuous Integration software, such as [Jenkins](https://jenkins.io/index.html). +``` +mbedgt --report-junit junit_report.xml +``` + +#### JSON +This creates a general JSON report. +``` +mbedgt --report-json json_report.json +``` + +#### Plain text +This creates a human-friendly text summary of the test run. +``` +mbedgt --report-text text_report.text +``` + +## Host test detection +When developing with mbed OS, Greentea detects host tests automatically if you place them in the correct location. All tests in mbed OS are placed under a `TESTS` directory. You may place custom host test scripts in a folder named `host_tests` in this folder. For more information about the mbed OS test directory structure, please see the [mbed CLI documentation](https://docs.mbed.com/docs/mbed-os-handbook/en/latest/dev_tools/cli/#test-directory-structure). + +## Common issues + +### `IOERR_SERIAL` errors +Possible causes: +- Another program is using the serial port. Be sure all terminals and other instances of Greentea are closed before trying again. +- The mbed interface firmware is out of date. Please see the platform's page on [developer.mbed.org](https://developer.mbed.org/) for details about how to update it. diff --git a/tools/python/mbed_greentea/__init__.py b/tools/python/mbed_greentea/__init__.py new file mode 100644 index 0000000000..2ad93138a2 --- /dev/null +++ b/tools/python/mbed_greentea/__init__.py @@ -0,0 +1,29 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from .mbed_greentea_cli import main + +"""! @package mbed-greentea + +This is a test suite used by mbed project. If you have yotta package with tests you can run them on supported hardware +This test suite supports: +* mbed-ls - mbed-enabled device auto detection module +* mbed-host-test - mbed-enabled device test framework (flash, reset and make host tests) + +""" diff --git a/tools/python/mbed_greentea/__main__.py b/tools/python/mbed_greentea/__main__.py new file mode 100644 index 0000000000..91b45f0300 --- /dev/null +++ b/tools/python/mbed_greentea/__main__.py @@ -0,0 +1,19 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. +""" + +from mbed_greentea.mbed_greentea_cli import main +exit(main()) \ No newline at end of file diff --git a/tools/python/mbed_greentea/cmake_handlers.py b/tools/python/mbed_greentea/cmake_handlers.py new file mode 100644 index 0000000000..54954a7823 --- /dev/null +++ b/tools/python/mbed_greentea/cmake_handlers.py @@ -0,0 +1,25 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.cmake_handlers import ( + load_ctest_testsuite, + parse_ctesttestfile_line, + list_binaries_for_targets, + list_binaries_for_builds, +) diff --git a/tools/python/mbed_greentea/mbed_common_api.py b/tools/python/mbed_greentea/mbed_common_api.py new file mode 100644 index 0000000000..ce0251c779 --- /dev/null +++ b/tools/python/mbed_greentea/mbed_common_api.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.mbed_common_api import ( + run_cli_command, + run_cli_process, +) diff --git a/tools/python/mbed_greentea/mbed_coverage_api.py b/tools/python/mbed_greentea/mbed_coverage_api.py new file mode 100644 index 0000000000..4a025bb27d --- /dev/null +++ b/tools/python/mbed_greentea/mbed_coverage_api.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.mbed_coverage_api import ( + coverage_pack_hex_payload, + coverage_dump_file, +) diff --git a/tools/python/mbed_greentea/mbed_greentea_cli.py b/tools/python/mbed_greentea/mbed_greentea_cli.py new file mode 100644 index 0000000000..be3ab81770 --- /dev/null +++ b/tools/python/mbed_greentea/mbed_greentea_cli.py @@ -0,0 +1,1053 @@ + +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +import os +import sys +import random +import optparse +import imp +import io +from time import time +try: + from Queue import Queue +except ImportError: + # Python 3 + from queue import Queue + +from threading import Thread + + +from mbed_os_tools.test.mbed_test_api import ( + get_test_build_properties, + get_test_spec, + log_mbed_devices_in_table, + TEST_RESULTS, + TEST_RESULT_OK, + TEST_RESULT_FAIL, + parse_global_resource_mgr, + parse_fast_model_connection +) + +from mbed_os_tools.test.mbed_report_api import ( + exporter_text, + exporter_testcase_text, + exporter_json, + exporter_testcase_junit, + exporter_html, + exporter_memory_metrics_csv, +) + +from mbed_os_tools.test.mbed_greentea_log import gt_logger + +from mbed_os_tools.test.mbed_greentea_dlm import ( + GREENTEA_KETTLE_PATH, + greentea_get_app_sem, + greentea_update_kettle, + greentea_clean_kettle, +) + +from mbed_os_tools.test.mbed_greentea_hooks import GreenteaHooks +from mbed_os_tools.test.tests_spec import TestBinary +from mbed_os_tools.test.mbed_target_info import get_platform_property + +from .mbed_test_api import run_host_test + +import mbed_os_tools.detect +import mbed_os_tools.test.host_tests_plugins as host_tests_plugins +from mbed_os_tools.test.mbed_greentea_cli import ( + RET_NO_DEVICES, + RET_YOTTA_BUILD_FAIL, + LOCAL_HOST_TESTS_DIR, + get_local_host_tests_dir, + create_filtered_test_list, +) + +LOCAL_HOST_TESTS_DIR = './test/host_tests' # Used by mbedhtrun -e + + +def get_greentea_version(): + """! Get Greentea (mbed-greentea) Python module version + """ + import mbed_os_tools + return mbed_os_tools.VERSION + +def print_version(): + """! Print current package version + """ + print(get_greentea_version()) + +def get_hello_string(): + """! Hello string used as first print + """ + version = get_greentea_version() + return "greentea test automation tool ver. " + version + +def main(): + """ Closure for main_cli() function """ + parser = optparse.OptionParser() + + parser.add_option('-t', '--target', + dest='list_of_targets', + help='You can specify list of yotta targets you want to build. Use comma to separate them.' + + 'Note: If --test-spec switch is defined this list becomes optional list of builds you want to filter in your test:' + + 'Comma separated list of builds from test specification. Applicable if --test-spec switch is specified') + + parser.add_option('-n', '--test-by-names', + dest='test_by_names', + help='Runs only test enumerated it this switch. Use comma to separate test case names.') + + parser.add_option('-i', '--skip-test', + dest='skip_test', + help='Skip tests enumerated it this switch. Use comma to separate test case names.') + + parser.add_option("-O", "--only-build", + action="store_true", + dest="only_build_tests", + default=False, + help="Only build repository and tests, skips actual test procedures (flashing etc.)") + + parser.add_option("-S", "--skip-build", + action="store_true", + dest="skip_yotta_build", + default=True, + help="Skip calling 'yotta build' on this module") + + copy_methods_str = "Plugin support: " + ', '.join(host_tests_plugins.get_plugin_caps('CopyMethod')) + parser.add_option("-c", "--copy", + dest="copy_method", + help="Copy (flash the target) method selector. " + copy_methods_str, + metavar="COPY_METHOD") + + reset_methods_str = "Plugin support: " + ', '.join(host_tests_plugins.get_plugin_caps('ResetMethod')) + parser.add_option("-r", "--reset", + dest="reset_method", + help="Reset method selector. " + reset_methods_str, + metavar="RESET_METHOD") + + parser.add_option('', '--parallel', + dest='parallel_test_exec', + default=1, + help='Experimental, you execute test runners for connected to your host MUTs in parallel (speeds up test result collection)') + + parser.add_option("-e", "--enum-host-tests", + dest="enum_host_tests", + help="Define directory with yotta module local host tests. Default: ./test/host_tests") + + parser.add_option('', '--config', + dest='verbose_test_configuration_only', + default=False, + action="store_true", + help='Displays connected boards and detected targets and exits.') + + parser.add_option('', '--release', + dest='build_to_release', + default=False, + action="store_true", + help='If possible force build in release mode (yotta -r).') + + parser.add_option('', '--debug', + dest='build_to_debug', + default=False, + action="store_true", + help='If possible force build in debug mode (yotta -d).') + + parser.add_option('-l', '--list', + dest='list_binaries', + default=False, + action="store_true", + help='List available binaries') + + parser.add_option('-g', '--grm', + dest='global_resource_mgr', + help=( + 'Global resource manager: ":' + ':[:]", ' + 'Ex. "K64F:module_name:10.2.123.43:3334", ' + '"K64F:module_name:https://example.com"' + ) + ) + + # Show --fm option only if "fm_agent" module installed + try: + imp.find_module('fm_agent') + except ImportError: + fm_help=optparse.SUPPRESS_HELP + else: + fm_help='Fast Model Connection: fastmodel name, config name, example FVP_MPS2_M3:DEFAULT' + parser.add_option('', '--fm', + dest='fast_model_connection', + help=fm_help) + + parser.add_option('-m', '--map-target', + dest='map_platform_to_yt_target', + help='List of custom mapping between platform name and yotta target. Comma separated list of YOTTA_TARGET:PLATFORM tuples') + + parser.add_option('', '--use-tids', + dest='use_target_ids', + help='Specify explicitly which devices can be used by Greentea for testing by creating list of allowed Target IDs (use comma separated list)') + + parser.add_option('-u', '--shuffle', + dest='shuffle_test_order', + default=False, + action="store_true", + help='Shuffles test execution order') + + parser.add_option('', '--shuffle-seed', + dest='shuffle_test_seed', + default=None, + help='Shuffle seed (If you want to reproduce your shuffle order please use seed provided in test summary)') + + parser.add_option('', '--sync', + dest='num_sync_packtes', + default=5, + help='Define how many times __sync packet will be sent to device: 0: none; -1: forever; 1,2,3... - number of times (the default is 5 packets)') + + parser.add_option('-P', '--polling-timeout', + dest='polling_timeout', + default=60, + metavar="NUMBER", + type="int", + help='Timeout in sec for readiness of mount point and serial port of local or remote device. Default 60 sec') + + parser.add_option('', '--tag-filters', + dest='tags', + default=None, + help='Filter list of available devices under test to only run on devices with the provided list of tags [tag-filters tag1,tag]') + + parser.add_option('', '--lock', + dest='lock_by_target', + default=False, + action="store_true", + help='Use simple resource locking mechanism to run multiple application instances') + + parser.add_option('', '--digest', + dest='digest_source', + help='Redirect input from where test suite should take console input. You can use stdin or file name to get test case console output') + + parser.add_option('-H', '--hooks', + dest='hooks_json', + help='Load hooks used drive extra functionality') + + parser.add_option('', '--test-spec', + dest='test_spec', + help='Test specification generated by build system.') + + parser.add_option('', '--test-cfg', + dest='json_test_configuration', + help='Pass to host test data with host test configuration') + + parser.add_option('', '--run', + dest='run_app', + help='Flash, reset and dump serial from selected binary application') + + parser.add_option('', '--report-junit', + dest='report_junit_file_name', + help='You can log test suite results in form of JUnit compliant XML report') + + parser.add_option('', '--report-text', + dest='report_text_file_name', + help='You can log test suite results to text file') + + parser.add_option('', '--report-json', + dest='report_json_file_name', + help='You can log test suite results to JSON formatted file') + + parser.add_option('', '--report-html', + dest='report_html_file_name', + help='You can log test suite results in the form of a HTML page') + + parser.add_option('', '--report-fails', + dest='report_fails', + default=False, + action="store_true", + help='Prints console outputs for failed tests') + + parser.add_option('', '--retry-count', + dest='retry_count', + default=1, + type=int, + help='retry count for individual test failure. By default, there is no retry') + + parser.add_option('', '--report-memory-metrics-csv', + dest='report_memory_metrics_csv_file_name', + help='You can log test suite memory metrics in the form of a CSV file') + + parser.add_option('', '--yotta-registry', + dest='yotta_search_for_mbed_target', + default=False, + action="store_true", + help='Use on-line yotta registry to search for compatible with connected mbed devices yotta targets. Default: search is done in yotta_targets directory') + + parser.add_option('-V', '--verbose-test-result', + dest='verbose_test_result_only', + default=False, + action="store_true", + help='Prints test serial output') + + parser.add_option('-v', '--verbose', + dest='verbose', + default=False, + action="store_true", + help='Verbose mode (prints some extra information)') + + parser.add_option('', '--plain', + dest='plain', + default=False, + action="store_true", + help='Do not use colours while logging') + + parser.add_option('', '--version', + dest='version', + default=False, + action="store_true", + help='Prints package version and exits') + + parser.description = """This automated test script is used to test mbed SDK 3.0 on mbed-enabled devices with support from yotta build tool""" + parser.epilog = """Example: mbedgt --target frdm-k64f-gcc""" + + (opts, args) = parser.parse_args() + + cli_ret = 0 + + if not opts.version: + # This string should not appear when fetching plain version string + gt_logger.gt_log(get_hello_string()) + + start = time() + if opts.lock_by_target: + # We are using Greentea proprietary locking mechanism to lock between platforms and targets + gt_logger.gt_log("using (experimental) simple locking mechanism") + gt_logger.gt_log_tab("kettle: %s"% GREENTEA_KETTLE_PATH) + gt_file_sem, gt_file_sem_name, gt_instance_uuid = greentea_get_app_sem() + with gt_file_sem: + greentea_update_kettle(gt_instance_uuid) + try: + cli_ret = main_cli(opts, args, gt_instance_uuid) + except KeyboardInterrupt: + greentea_clean_kettle(gt_instance_uuid) + gt_logger.gt_log_err("ctrl+c keyboard interrupt!") + return 1 # Keyboard interrupt + except: + greentea_clean_kettle(gt_instance_uuid) + gt_logger.gt_log_err("unexpected error:") + gt_logger.gt_log_tab(sys.exc_info()[0]) + raise + greentea_clean_kettle(gt_instance_uuid) + else: + # Standard mode of operation + # Other instance must provide mutually exclusive access control to platforms and targets + try: + cli_ret = main_cli(opts, args) + except KeyboardInterrupt: + gt_logger.gt_log_err("ctrl+c keyboard interrupt!") + return 1 # Keyboard interrupt + except Exception as e: + gt_logger.gt_log_err("unexpected error:") + gt_logger.gt_log_tab(str(e)) + raise + + if not any([opts.list_binaries, opts.version]): + delta = time() - start # Test execution time delta + gt_logger.gt_log("completed in %.2f sec"% delta) + + if cli_ret: + if cli_ret < 0 or cli_ret > 255: + cli_ret = 1 + gt_logger.gt_log_err("exited with code %d"% cli_ret) + + return(cli_ret) + +def run_test_thread(test_result_queue, test_queue, opts, mut, build, build_path, greentea_hooks): + test_exec_retcode = 0 + test_platforms_match = 0 + test_report = {} + + disk = mut['mount_point'] + # Set serial portl format used by mbedhtrun: 'serial_port' = ':' + port = "{}:{}".format(mut['serial_port'],mut['baud_rate']) + micro = mut['platform_name'] + program_cycle_s = get_platform_property(micro, "program_cycle_s") + forced_reset_timeout = get_platform_property(micro, "forced_reset_timeout") + copy_method = get_platform_property(micro, "copy_method") + reset_method = get_platform_property(micro, "reset_method") + + while not test_queue.empty(): + try: + test = test_queue.get(False) + except Exception as e: + gt_logger.gt_log_err(str(e)) + break + + test_result = 'SKIPPED' + + if opts.copy_method: + copy_method = opts.copy_method + elif not copy_method: + copy_method = 'shell' + + if opts.reset_method: + reset_method = opts.reset_method + + verbose = opts.verbose_test_result_only + enum_host_tests_path = get_local_host_tests_dir(opts.enum_host_tests) + + test_platforms_match += 1 + host_test_result = run_host_test(test['image_path'], + disk, + port, + build_path, + mut['target_id'], + micro=micro, + copy_method=copy_method, + reset=reset_method, + program_cycle_s=program_cycle_s, + forced_reset_timeout=forced_reset_timeout, + digest_source=opts.digest_source, + json_test_cfg=opts.json_test_configuration, + enum_host_tests_path=enum_host_tests_path, + global_resource_mgr=opts.global_resource_mgr, + fast_model_connection=opts.fast_model_connection, + compare_log=test['compare_log'], + num_sync_packtes=opts.num_sync_packtes, + tags=opts.tags, + retry_count=opts.retry_count, + polling_timeout=opts.polling_timeout, + verbose=verbose) + + # Some error in htrun, abort test execution + if isinstance(host_test_result, int): + # int(host_test_result) > 0 - Call to mbedhtrun failed + # int(host_test_result) < 0 - Something went wrong while executing mbedhtrun + gt_logger.gt_log_err("run_test_thread.run_host_test() failed, aborting...") + break + + # If execution was successful 'run_host_test' return tuple with results + single_test_result, single_test_output, single_testduration, single_timeout, result_test_cases, test_cases_summary, memory_metrics = host_test_result + test_result = single_test_result + + build_path_abs = os.path.abspath(build_path) + + if single_test_result != TEST_RESULT_OK: + test_exec_retcode += 1 + + if single_test_result in [TEST_RESULT_OK, TEST_RESULT_FAIL]: + if greentea_hooks: + # Test was successful + # We can execute test hook just after test is finished ('hook_test_end') + format = { + "test_name": test['test_bin'], + "test_bin_name": os.path.basename(test['image_path']), + "image_path": test['image_path'], + "build_path": build_path, + "build_path_abs": build_path_abs, + "build_name": build, + } + greentea_hooks.run_hook_ext('hook_test_end', format) + + # Update report for optional reporting feature + test_suite_name = test['test_bin'].lower() + if build not in test_report: + test_report[build] = {} + + if test_suite_name not in test_report[build]: + test_report[build][test_suite_name] = {} + + if not test_cases_summary and not result_test_cases: + gt_logger.gt_log_warn("test case summary event not found") + gt_logger.gt_log_tab("no test case report present, assuming test suite to be a single test case!") + + # We will map test suite result to test case to + # output valid test case in report + + # Generate "artificial" test case name from test suite name# + # E.g: + # mbed-drivers-test-dev_null -> dev_null + test_case_name = test_suite_name + test_str_idx = test_suite_name.find("-test-") + if test_str_idx != -1: + test_case_name = test_case_name[test_str_idx + 6:] + + gt_logger.gt_log_tab("test suite: %s"% test_suite_name) + gt_logger.gt_log_tab("test case: %s"% test_case_name) + + # Test case result: OK, FAIL or ERROR + tc_result_text = { + "OK": "OK", + "FAIL": "FAIL", + }.get(single_test_result, 'ERROR') + + # Test case integer success code OK, FAIL and ERROR: (0, >0, <0) + tc_result = { + "OK": 0, + "FAIL": 1024, + "ERROR": -1024, + }.get(tc_result_text, '-2048') + + # Test case passes and failures: (1 pass, 0 failures) or (0 passes, 1 failure) + tc_passed, tc_failed = { + 0: (1, 0), + }.get(tc_result, (0, 1)) + + # Test case report build for whole binary + # Add test case made from test suite result to test case report + result_test_cases = { + test_case_name: { + 'duration': single_testduration, + 'time_start': 0.0, + 'time_end': 0.0, + 'utest_log': single_test_output.splitlines(), + 'result_text': tc_result_text, + 'passed': tc_passed, + 'failed': tc_failed, + 'result': tc_result, + } + } + + # Test summary build for whole binary (as a test case) + test_cases_summary = (tc_passed, tc_failed, ) + + gt_logger.gt_log("test on hardware with target id: %s"% (mut['target_id'])) + gt_logger.gt_log("test suite '%s' %s %s in %.2f sec"% (test['test_bin'], + '.' * (80 - len(test['test_bin'])), + test_result, + single_testduration)) + + # Test report build for whole binary + test_report[build][test_suite_name]['single_test_result'] = single_test_result + test_report[build][test_suite_name]['single_test_output'] = single_test_output + test_report[build][test_suite_name]['elapsed_time'] = single_testduration + test_report[build][test_suite_name]['platform_name'] = micro + test_report[build][test_suite_name]['copy_method'] = copy_method + test_report[build][test_suite_name]['testcase_result'] = result_test_cases + test_report[build][test_suite_name]['memory_metrics'] = memory_metrics + + test_report[build][test_suite_name]['build_path'] = build_path + test_report[build][test_suite_name]['build_path_abs'] = build_path_abs + test_report[build][test_suite_name]['image_path'] = test['image_path'] + test_report[build][test_suite_name]['test_bin_name'] = os.path.basename(test['image_path']) + + passes_cnt, failures_cnt = 0, 0 + for tc_name in sorted(result_test_cases.keys()): + gt_logger.gt_log_tab("test case: '%s' %s %s in %.2f sec"% (tc_name, + '.' * (80 - len(tc_name)), + result_test_cases[tc_name].get('result_text', '_'), + result_test_cases[tc_name].get('duration', 0.0))) + if result_test_cases[tc_name].get('result_text', '_') == 'OK': + passes_cnt += 1 + else: + failures_cnt += 1 + + if test_cases_summary: + passes, failures = test_cases_summary + gt_logger.gt_log("test case summary: %d pass%s, %d failur%s"% (passes, + '' if passes == 1 else 'es', + failures, + 'e' if failures == 1 else 'es')) + if passes != passes_cnt or failures != failures_cnt: + gt_logger.gt_log_err("utest test case summary mismatch: utest reported passes and failures miscount!") + gt_logger.gt_log_tab("reported by utest: passes = %d, failures %d)"% (passes, failures)) + gt_logger.gt_log_tab("test case result count: passes = %d, failures %d)"% (passes_cnt, failures_cnt)) + + if single_test_result != 'OK' and not verbose and opts.report_fails: + # In some cases we want to print console to see why test failed + # even if we are not in verbose mode + gt_logger.gt_log_tab("test failed, reporting console output (specified with --report-fails option)") + print() + print(single_test_output) + + #greentea_release_target_id(mut['target_id'], gt_instance_uuid) + test_result_queue.put({'test_platforms_match': test_platforms_match, + 'test_exec_retcode': test_exec_retcode, + 'test_report': test_report}) + return + +def main_cli(opts, args, gt_instance_uuid=None): + """! This is main CLI function with all command line parameters + @details This function also implements CLI workflow depending on CLI parameters inputed + @return This function doesn't return, it exits to environment with proper success code + """ + + def filter_ready_devices(mbeds_list): + """! Filters list of MUTs to check if all MUTs are correctly detected with mbed-ls module. + @details This function logs a lot to help users figure out root cause of their problems + @param mbeds_list List of MUTs to verify + @return Tuple of (MUTS detected correctly, MUTs not detected fully) + """ + ready_mbed_devices = [] # Devices which can be used (are fully detected) + not_ready_mbed_devices = [] # Devices which can't be used (are not fully detected) + + required_mut_props = ['target_id', 'platform_name', 'serial_port', 'mount_point'] + + gt_logger.gt_log("detected %d device%s"% (len(mbeds_list), 's' if len(mbeds_list) != 1 else '')) + for mut in mbeds_list: + for prop in required_mut_props: + if not mut[prop]: + # Adding MUT to NOT DETECTED FULLY list + if mut not in not_ready_mbed_devices: + not_ready_mbed_devices.append(mut) + gt_logger.gt_log_err("mbed-ls was unable to enumerate correctly all properties of the device!") + gt_logger.gt_log_tab("check with 'mbedls -j' command if all properties of your device are enumerated properly") + + gt_logger.gt_log_err("mbed-ls property '%s' is '%s'"% (prop, str(mut[prop]))) + if prop == 'serial_port': + gt_logger.gt_log_tab("check if your serial port driver is correctly installed!") + if prop == 'mount_point': + gt_logger.gt_log_tab('check if your OS can detect and mount mbed device mount point!') + else: + # Adding MUT to DETECTED CORRECTLY list + ready_mbed_devices.append(mut) + return (ready_mbed_devices, not_ready_mbed_devices) + + def get_parallel_value(value): + """! Get correct value for parallel switch (--parallel) + @param value Value passed from --parallel + @return Refactored version of parallel number + """ + try: + parallel_test_exec = int(value) + if parallel_test_exec < 1: + parallel_test_exec = 1 + except ValueError: + gt_logger.gt_log_err("argument of mode --parallel is not a int, disabled parallel mode") + parallel_test_exec = 1 + return parallel_test_exec + + # This is how you magically control colours in this piece of art software + gt_logger.colorful(not opts.plain) + + # Prints version and exits + if opts.version: + print_version() + return (0) + + # Load test specification or print warnings / info messages and exit CLI mode + test_spec, ret = get_test_spec(opts) + if not test_spec: + return ret + + # Verbose flag + verbose = opts.verbose_test_result_only + + # We will load hooks from JSON file to support extra behaviour during test execution + greentea_hooks = GreenteaHooks(opts.hooks_json) if opts.hooks_json else None + + # Capture alternative test console inputs, used e.g. in 'yotta test command' + if opts.digest_source: + enum_host_tests_path = get_local_host_tests_dir(opts.enum_host_tests) + host_test_result = run_host_test(None, + None, + None, + None, + None, + digest_source=opts.digest_source, + enum_host_tests_path=enum_host_tests_path, + verbose=verbose) + + # Some error in htrun, abort test execution + if isinstance(host_test_result, int): + # int(host_test_result) > 0 - Call to mbedhtrun failed + # int(host_test_result) < 0 - Something went wrong while executing mbedhtrun + return host_test_result + + # If execution was successful 'run_host_test' return tuple with results + single_test_result, single_test_output, single_testduration, single_timeout, result_test_cases, test_cases_summary, memory_metrics = host_test_result + status = TEST_RESULTS.index(single_test_result) if single_test_result in TEST_RESULTS else -1 + return (status) + + ### Query with mbedls for available mbed-enabled devices + gt_logger.gt_log("detecting connected mbed-enabled devices...") + + ### check if argument of --parallel mode is a integer and greater or equal 1 + parallel_test_exec = get_parallel_value(opts.parallel_test_exec) + + # Detect devices connected to system + mbeds = mbed_os_tools.detect.create() + mbeds_list = mbeds.list_mbeds(unique_names=True, read_details_txt=True) + + if opts.global_resource_mgr: + # Mocking available platform requested by --grm switch + grm_values = parse_global_resource_mgr(opts.global_resource_mgr) + if grm_values: + gt_logger.gt_log_warn("entering global resource manager mbed-ls dummy mode!") + grm_platform_name, grm_module_name, grm_ip_name, grm_port_name = grm_values + mbeds_list = [] + if grm_platform_name == '*': + required_devices = [tb.get_platform() for tb in test_spec.get_test_builds()] + for _ in range(parallel_test_exec): + for device in required_devices: + mbeds_list.append(mbeds.get_dummy_platform(device)) + else: + for _ in range(parallel_test_exec): + mbeds_list.append(mbeds.get_dummy_platform(grm_platform_name)) + opts.global_resource_mgr = ':'.join([v for v in grm_values[1:] if v]) + gt_logger.gt_log_tab("adding dummy platform '%s'"% grm_platform_name) + else: + gt_logger.gt_log("global resource manager switch '--grm %s' in wrong format!"% opts.global_resource_mgr) + return (-1) + + if opts.fast_model_connection: + # Mocking available platform requested by --fm switch + fm_values = parse_fast_model_connection(opts.fast_model_connection) + if fm_values: + gt_logger.gt_log_warn("entering fastmodel connection, mbed-ls dummy simulator mode!") + fm_platform_name, fm_config_name = fm_values + mbeds_list = [] + for _ in range(parallel_test_exec): + mbeds_list.append(mbeds.get_dummy_platform(fm_platform_name)) + opts.fast_model_connection = fm_config_name + gt_logger.gt_log_tab("adding dummy fastmodel platform '%s'"% fm_platform_name) + else: + gt_logger.gt_log("fast model connection switch '--fm %s' in wrong format!"% opts.fast_model_connection) + return (-1) + + ready_mbed_devices = [] # Devices which can be used (are fully detected) + not_ready_mbed_devices = [] # Devices which can't be used (are not fully detected) + + if mbeds_list: + ready_mbed_devices, not_ready_mbed_devices = filter_ready_devices(mbeds_list) + if ready_mbed_devices: + # devices in form of a pretty formatted table + for line in log_mbed_devices_in_table(ready_mbed_devices).splitlines(): + gt_logger.gt_log_tab(line.strip(), print_text=verbose) + else: + gt_logger.gt_log_err("no compatible devices detected") + return (RET_NO_DEVICES) + + ### We can filter in only specific target ids + accepted_target_ids = None + if opts.use_target_ids: + gt_logger.gt_log("filtering out target ids not on below list (specified with --use-tids switch)") + accepted_target_ids = opts.use_target_ids.split(',') + for tid in accepted_target_ids: + gt_logger.gt_log_tab("accepting target id '%s'"% gt_logger.gt_bright(tid)) + + test_exec_retcode = 0 # Decrement this value each time test case result is not 'OK' + test_platforms_match = 0 # Count how many tests were actually ran with current settings + target_platforms_match = 0 # Count how many platforms were actually tested with current settings + + test_report = {} # Test report used to export to Junit, HTML etc... + test_queue = Queue() # contains information about test_bin and image_path for each test case + test_result_queue = Queue() # used to store results of each thread + execute_threads = [] # list of threads to run test cases + + # Values used to generate random seed for test execution order shuffle + SHUFFLE_SEED_ROUND = 10 # Value used to round float random seed + shuffle_random_seed = round(random.random(), SHUFFLE_SEED_ROUND) + + # Set shuffle seed if it is provided with command line option + if opts.shuffle_test_seed: + shuffle_random_seed = round(float(opts.shuffle_test_seed), SHUFFLE_SEED_ROUND) + + ### Testing procedures, for each target, for each target's compatible platform + # In case we are using test spec (switch --test-spec) command line option -t + # is used to enumerate builds from test spec we are supplying + filter_test_builds = opts.list_of_targets.split(',') if opts.list_of_targets else None + for test_build in test_spec.get_test_builds(filter_test_builds): + platform_name = test_build.get_platform() + gt_logger.gt_log("processing target '%s' toolchain '%s' compatible platforms... (note: switch set to --parallel %d)" % + (gt_logger.gt_bright(platform_name), + gt_logger.gt_bright(test_build.get_toolchain()), + int(opts.parallel_test_exec))) + + baudrate = test_build.get_baudrate() + + ### Select MUTS to test from list of available MUTS to start testing + mut = None + number_of_parallel_instances = 1 + muts_to_test = [] # MUTs to actually be tested + for mbed_dev in ready_mbed_devices: + if accepted_target_ids and mbed_dev['target_id'] not in accepted_target_ids: + continue + + # Check that we have a valid serial port detected. + sp = mbed_dev['serial_port'] + if not sp: + gt_logger.gt_log_err("Serial port for target %s not detected correctly\n" % mbed_dev['target_id']) + continue + + if mbed_dev['platform_name'] == platform_name: + mbed_dev['baud_rate'] = baudrate + + mut = mbed_dev + if mbed_dev not in muts_to_test: + # We will only add unique devices to list of devices "for testing" in this test run + muts_to_test.append(mbed_dev) + if number_of_parallel_instances < parallel_test_exec: + number_of_parallel_instances += 1 + else: + break + + # devices in form of a pretty formatted table + for line in log_mbed_devices_in_table(muts_to_test).splitlines(): + gt_logger.gt_log_tab(line.strip(), print_text=verbose) + + # Configuration print mode: + if opts.verbose_test_configuration_only: + continue + + ### If we have at least one available device we can proceed + if mut: + target_platforms_match += 1 + + build = test_build.get_name() + build_path = test_build.get_path() + + # Demo mode: --run implementation (already added --run to mbedhtrun) + # We want to pass file name to mbedhtrun (--run NAME => -f NAME_ and run only one binary + if opts.run_app: + gt_logger.gt_log("running '%s' for '%s'-'%s'" % (gt_logger.gt_bright(opts.run_app), + gt_logger.gt_bright(platform_name), + gt_logger.gt_bright(test_build.get_toolchain()))) + disk = mut['mount_point'] + # Set serial portl format used by mbedhtrun: 'serial_port' = ':' + port = "{}:{}".format(mut['serial_port'], mut['baud_rate']) + micro = mut['platform_name'] + program_cycle_s = get_platform_property(micro, "program_cycle_s") + copy_method = opts.copy_method if opts.copy_method else 'shell' + enum_host_tests_path = get_local_host_tests_dir(opts.enum_host_tests) + + test_platforms_match += 1 + host_test_result = run_host_test(opts.run_app, + disk, + port, + build_path, + mut['target_id'], + micro=micro, + copy_method=copy_method, + program_cycle_s=program_cycle_s, + digest_source=opts.digest_source, + json_test_cfg=opts.json_test_configuration, + run_app=opts.run_app, + enum_host_tests_path=enum_host_tests_path, + verbose=True) + + # Some error in htrun, abort test execution + if isinstance(host_test_result, int): + # int(host_test_result) > 0 - Call to mbedhtrun failed + # int(host_test_result) < 0 - Something went wrong while executing mbedhtrun + return host_test_result + + # If execution was successful 'run_host_test' return tuple with results + single_test_result, single_test_output, single_testduration, single_timeout, result_test_cases, test_cases_summary, memory_metrics = host_test_result + status = TEST_RESULTS.index(single_test_result) if single_test_result in TEST_RESULTS else -1 + if single_test_result != TEST_RESULT_OK: + test_exec_retcode += 1 + + test_list = test_build.get_tests() + + filtered_ctest_test_list = create_filtered_test_list(test_list, opts.test_by_names, opts.skip_test, test_spec=test_spec) + + gt_logger.gt_log("running %d test%s for platform '%s' and toolchain '%s'"% ( + len(filtered_ctest_test_list), + "s" if len(filtered_ctest_test_list) != 1 else "", + gt_logger.gt_bright(platform_name), + gt_logger.gt_bright(test_build.get_toolchain()) + )) + + # Test execution order can be shuffled (also with provided random seed) + # for test execution reproduction. + filtered_ctest_test_list_keys = filtered_ctest_test_list.keys() + if opts.shuffle_test_order: + # We want to shuffle test names randomly + random.shuffle(filtered_ctest_test_list_keys, lambda: shuffle_random_seed) + + for test_name in filtered_ctest_test_list_keys: + image_path = filtered_ctest_test_list[test_name].get_binary(binary_type=TestBinary.BIN_TYPE_BOOTABLE).get_path() + compare_log = filtered_ctest_test_list[test_name].get_binary(binary_type=TestBinary.BIN_TYPE_BOOTABLE).get_compare_log() + if image_path is None: + gt_logger.gt_log_err("Failed to find test binary for test %s flash method %s" % (test_name, 'usb')) + else: + test = {"test_bin": test_name, "image_path": image_path, "compare_log": compare_log} + test_queue.put(test) + + number_of_threads = 0 + for mut in muts_to_test: + # Experimental, parallel test execution + if number_of_threads < parallel_test_exec: + args = (test_result_queue, test_queue, opts, mut, build, build_path, greentea_hooks) + t = Thread(target=run_test_thread, args=args) + execute_threads.append(t) + number_of_threads += 1 + + gt_logger.gt_log_tab("use %s instance%s of execution threads for testing"% (len(execute_threads), + 's' if len(execute_threads) != 1 else str()), print_text=verbose) + for t in execute_threads: + t.daemon = True + t.start() + + # merge partial test reports from different threads to final test report + for t in execute_threads: + try: + # We can't block forever here since that prevents KeyboardInterrupts + # from being propagated correctly. Therefore, we just join with a + # timeout of 0.1 seconds until the thread isn't alive anymore. + # A time of 0.1 seconds is a fairly arbitrary choice. It needs + # to balance CPU utilization and responsiveness to keyboard interrupts. + # Checking 10 times a second seems to be stable and responsive. + while t.is_alive(): + t.join(0.1) + + test_return_data = test_result_queue.get(False) + except Exception as e: + # No test report generated + gt_logger.gt_log_err("could not generate test report" + str(e)) + test_exec_retcode += -1000 + return test_exec_retcode + + test_platforms_match += test_return_data['test_platforms_match'] + test_exec_retcode += test_return_data['test_exec_retcode'] + partial_test_report = test_return_data['test_report'] + # todo: find better solution, maybe use extend + for report_key in partial_test_report.keys(): + if report_key not in test_report: + test_report[report_key] = {} + test_report.update(partial_test_report) + else: + test_report[report_key].update(partial_test_report[report_key]) + + execute_threads = [] + + if opts.verbose_test_configuration_only: + print("Example: execute 'mbedgt --target=TARGET_NAME' to start testing for TARGET_NAME target") + return (0) + + gt_logger.gt_log("all tests finished!") + + # We will execute post test hooks on tests + for build_name in test_report: + test_name_list = [] # All test case names for particular yotta target + for test_name in test_report[build_name]: + test = test_report[build_name][test_name] + # Test was successful + if test['single_test_result'] in [TEST_RESULT_OK, TEST_RESULT_FAIL]: + test_name_list.append(test_name) + # Call hook executed for each test, just after all tests are finished + if greentea_hooks: + # We can execute this test hook just after all tests are finished ('hook_post_test_end') + format = { + "test_name": test_name, + "test_bin_name": test['test_bin_name'], + "image_path": test['image_path'], + "build_path": test['build_path'], + "build_path_abs": test['build_path_abs'], + } + greentea_hooks.run_hook_ext('hook_post_test_end', format) + if greentea_hooks: + build = test_spec.get_test_build(build_name) + assert build is not None, "Failed to find build info for build %s" % build_name + + # Call hook executed for each yotta target, just after all tests are finished + build_path = build.get_path() + build_path_abs = os.path.abspath(build_path) + # We can execute this test hook just after all tests are finished ('hook_post_test_end') + format = { + "build_path": build_path, + "build_path_abs": build_path_abs, + "test_name_list": test_name_list, + } + greentea_hooks.run_hook_ext('hook_post_all_test_end', format) + + # This tool is designed to work in CI + # We want to return success codes based on tool actions, + # only if testes were executed and all passed we want to + # return 0 (success) + if not opts.only_build_tests: + # Prints shuffle seed + gt_logger.gt_log("shuffle seed: %.*f"% (SHUFFLE_SEED_ROUND, shuffle_random_seed)) + + def dump_report_to_text_file(filename, content): + """! Closure for report dumps to text files + @param filename Name of destination file + @parm content Text content of the file to write + @return True if write was successful, else return False + """ + try: + with io.open(filename, encoding="utf-8", errors="backslashreplace", mode="w") as f: + f.write(content) + except IOError as e: + gt_logger.gt_log_err("can't export to '%s', reason:"% filename) + gt_logger.gt_log_err(str(e)) + return False + return True + + # Reports to JUNIT file + if opts.report_junit_file_name: + gt_logger.gt_log("exporting to JUNIT file '%s'..."% gt_logger.gt_bright(opts.report_junit_file_name)) + # This test specification will be used by JUnit exporter to populate TestSuite.properties (useful meta-data for Viewer) + test_suite_properties = {} + for target_name in test_report: + test_build_properties = get_test_build_properties(test_spec, target_name) + if test_build_properties: + test_suite_properties[target_name] = test_build_properties + junit_report = exporter_testcase_junit(test_report, test_suite_properties = test_suite_properties) + dump_report_to_text_file(opts.report_junit_file_name, junit_report) + + # Reports to text file + if opts.report_text_file_name: + gt_logger.gt_log("exporting to TEXT '%s'..."% gt_logger.gt_bright(opts.report_text_file_name)) + # Useful text reporter for those who do not like to copy paste to files tabale with results + text_report, text_results = exporter_text(test_report) + text_testcase_report, text_testcase_results = exporter_testcase_text(test_report) + text_final_report = '\n'.join([text_report, text_results, text_testcase_report, text_testcase_results]) + dump_report_to_text_file(opts.report_text_file_name, text_final_report) + + # Reports to JSON file + if opts.report_json_file_name: + # We will not print summary and json report together + gt_logger.gt_log("exporting to JSON '%s'..."% gt_logger.gt_bright(opts.report_json_file_name)) + json_report = exporter_json(test_report) + dump_report_to_text_file(opts.report_json_file_name, json_report) + + # Reports to HTML file + if opts.report_html_file_name: + gt_logger.gt_log("exporting to HTML file '%s'..."% gt_logger.gt_bright(opts.report_html_file_name)) + # Generate a HTML page displaying all of the results + html_report = exporter_html(test_report) + dump_report_to_text_file(opts.report_html_file_name, html_report) + + # Memory metrics to CSV file + if opts.report_memory_metrics_csv_file_name: + gt_logger.gt_log("exporting memory metrics to CSV file '%s'..."% gt_logger.gt_bright(opts.report_memory_metrics_csv_file_name)) + # Generate a CSV file page displaying all memory metrics + memory_metrics_csv_report = exporter_memory_metrics_csv(test_report) + dump_report_to_text_file(opts.report_memory_metrics_csv_file_name, memory_metrics_csv_report) + + # Final summary + if test_report: + # Test suite report + gt_logger.gt_log("test suite report:") + text_report, text_results = exporter_text(test_report) + print(text_report) + gt_logger.gt_log("test suite results: " + text_results) + # test case detailed report + gt_logger.gt_log("test case report:") + text_testcase_report, text_testcase_results = exporter_testcase_text(test_report) + print(text_testcase_report) + gt_logger.gt_log("test case results: " + text_testcase_results) + + # This flag guards 'build only' so we expect only yotta errors + if test_platforms_match == 0: + # No tests were executed + gt_logger.gt_log_warn("no platform/target matching tests were found!") + if target_platforms_match == 0: + # No platforms were tested + gt_logger.gt_log_warn("no matching platforms were found!") + + return (test_exec_retcode) diff --git a/tools/python/mbed_greentea/mbed_greentea_dlm.py b/tools/python/mbed_greentea/mbed_greentea_dlm.py new file mode 100644 index 0000000000..653c4991b2 --- /dev/null +++ b/tools/python/mbed_greentea/mbed_greentea_dlm.py @@ -0,0 +1,37 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.mbed_greentea_dlm import ( + HOME_DIR, + GREENTEA_HOME_DIR, + GREENTEA_GLOBAL_LOCK, + GREENTEA_KETTLE, + GREENTEA_KETTLE_PATH, + greentea_home_dir_init, + greentea_get_app_sem, + greentea_get_target_lock, + greentea_get_global_lock, + greentea_update_kettle, + greentea_clean_kettle, + greentea_acquire_target_id, + greentea_acquire_target_id_from_list, + greentea_release_target_id, + get_json_data_from_file, + greentea_kettle_info, +) diff --git a/tools/python/mbed_greentea/mbed_greentea_hooks.py b/tools/python/mbed_greentea/mbed_greentea_hooks.py new file mode 100644 index 0000000000..d608d5c775 --- /dev/null +++ b/tools/python/mbed_greentea/mbed_greentea_hooks.py @@ -0,0 +1,21 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.mbed_greentea_hooks import ( + GreenteaTestHook, + GreenteaCliTestHook, + LcovHook, + GreenteaHooks, +) diff --git a/tools/python/mbed_greentea/mbed_greentea_log.py b/tools/python/mbed_greentea/mbed_greentea_log.py new file mode 100644 index 0000000000..8fa6f52605 --- /dev/null +++ b/tools/python/mbed_greentea/mbed_greentea_log.py @@ -0,0 +1,24 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.mbed_greentea_log import ( + COLORAMA, + GreenTeaSimpleLockLogger, + gt_logger, +) diff --git a/tools/python/mbed_greentea/mbed_report_api.py b/tools/python/mbed_greentea/mbed_report_api.py new file mode 100644 index 0000000000..e10d72dbe3 --- /dev/null +++ b/tools/python/mbed_greentea/mbed_report_api.py @@ -0,0 +1,38 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.mbed_report_api import ( + export_to_file, + exporter_json, + exporter_text, + exporter_testcase_text, + exporter_testcase_junit, + html_template, + TEST_RESULT_COLOURS, + TEST_RESULT_DEFAULT_COLOUR, + get_result_colour_class_css, + get_result_colour_class, + get_dropdown_html, + get_result_overlay_testcase_dropdown, + get_result_overlay_testcases_dropdown_menu, + get_result_overlay_dropdowns, + get_result_overlay, + exporter_html, + exporter_memory_metrics_csv, +) diff --git a/tools/python/mbed_greentea/mbed_target_info.py b/tools/python/mbed_greentea/mbed_target_info.py new file mode 100644 index 0000000000..818df52973 --- /dev/null +++ b/tools/python/mbed_greentea/mbed_target_info.py @@ -0,0 +1,36 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.mbed_target_info import ( + TARGET_INFO_MAPPING, + TARGET_TOOLCAHINS, + get_mbed_target_call_yotta_target, + parse_yotta_json_for_build_name, + get_yotta_target_from_local_config, + get_mbed_target_from_current_dir, + parse_yotta_target_cmd_output, + get_mbed_targets_from_yotta_local_module, + parse_mbed_target_from_target_json, + get_mbed_targets_from_yotta, + parse_yotta_search_cmd_output, + add_target_info_mapping, + get_mbed_clasic_target_info, + get_binary_type_for_platform, + get_platform_property, +) diff --git a/tools/python/mbed_greentea/mbed_test_api.py b/tools/python/mbed_greentea/mbed_test_api.py new file mode 100644 index 0000000000..c11c5f3eb0 --- /dev/null +++ b/tools/python/mbed_greentea/mbed_test_api.py @@ -0,0 +1,267 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" +import os +from time import time + +from mbed_os_tools.test.mbed_test_api import ( + TEST_RESULT_OK, + TEST_RESULT_FAIL, + TEST_RESULT_ERROR, + TEST_RESULT_SKIPPED, + TEST_RESULT_UNDEF, + TEST_RESULT_IOERR_COPY, + TEST_RESULT_IOERR_DISK, + TEST_RESULT_IOERR_SERIAL, + TEST_RESULT_TIMEOUT, + TEST_RESULT_NO_IMAGE, + TEST_RESULT_MBED_ASSERT, + TEST_RESULT_BUILD_FAILED, + TEST_RESULT_SYNC_FAILED, + TEST_RESULTS, + TEST_RESULT_MAPPING, + RUN_HOST_TEST_POPEN_ERROR, + get_test_result, + run_command, + run_htrun, + get_testcase_count_and_names, + get_testcase_utest, + get_coverage_data, + get_printable_string, + get_testcase_summary, + get_testcase_result, + get_memory_metrics, + get_thread_with_max_stack_size, + get_thread_stack_info_summary, + log_mbed_devices_in_table, + get_test_spec, + get_test_build_properties, + parse_global_resource_mgr, + parse_fast_model_connection, + gt_logger, +) + +def run_host_test(image_path, + disk, + port, + build_path, + target_id, + duration=10, + micro=None, + reset=None, + verbose=False, + copy_method=None, + program_cycle_s=None, + forced_reset_timeout=None, + digest_source=None, + json_test_cfg=None, + max_failed_properties=5, + enum_host_tests_path=None, + global_resource_mgr=None, + fast_model_connection=None, + compare_log=None, + num_sync_packtes=None, + polling_timeout=None, + retry_count=1, + tags=None, + run_app=None): + """! This function runs host test supervisor (executes mbedhtrun) and checks output from host test process. + @param image_path Path to binary file for flashing + @param disk Currently mounted mbed-enabled devices disk (mount point) + @param port Currently mounted mbed-enabled devices serial port (console) + @param duration Test case timeout + @param micro Mbed-enabled device name + @param reset Reset type + @param forced_reset_timeout Reset timeout (sec) + @param verbose Verbose mode flag + @param copy_method Copy method type (name) + @param program_cycle_s Wait after flashing delay (sec) + @param json_test_cfg Additional test configuration file path passed to host tests in JSON format + @param max_failed_properties After how many unknown properties we will assume test is not ported + @param enum_host_tests_path Directory where locally defined host tests may reside + @param num_sync_packtes sync packets to send for host <---> device communication + @param polling_timeout Timeout in sec for readiness of mount point and serial port of local or remote device + @param tags Filter list of available devices under test to only run on devices with the provided list + of tags [tag-filters tag1,tag] + @param run_app Run application mode flag (we run application and grab serial port data) + @param digest_source if None mbedhtrun will be executed. If 'stdin', + stdin will be used via StdInObserver or file (if + file name was given as switch option) + @return Tuple with test results, test output, test duration times, test case results, and memory metrics. + Return int > 0 if running mbedhtrun process failed. + Retrun int < 0 if something went wrong during mbedhtrun execution. + """ + + def get_binary_host_tests_dir(binary_path, level=2): + """! Checks if in binary test group has host_tests directory + @param binary_path Path to binary in test specification + @param level How many directories above test host_tests dir exists + @return Path to host_tests dir in group binary belongs too, None if not found + """ + try: + binary_path_norm = os.path.normpath(binary_path) + current_path_norm = os.path.normpath(os.getcwd()) + host_tests_path = binary_path_norm.split(os.sep)[:-level] + ['host_tests'] + build_dir_candidates = ['BUILD', '.build'] + idx = None + + for build_dir_candidate in build_dir_candidates: + if build_dir_candidate in host_tests_path: + idx = host_tests_path.index(build_dir_candidate) + break + + if idx is None: + msg = 'The following directories were not in the path: %s' % (', '.join(build_dir_candidates)) + raise Exception(msg) + + # Cut //tests/TOOLCHAIN/TARGET + host_tests_path = host_tests_path[:idx] + host_tests_path[idx+4:] + host_tests_path = os.sep.join(host_tests_path) + except Exception as e: + gt_logger.gt_log_warn("there was a problem while looking for host_tests directory") + gt_logger.gt_log_tab("level %d, path: %s"% (level, binary_path)) + gt_logger.gt_log_tab(str(e)) + return None + + if os.path.isdir(host_tests_path): + return host_tests_path + return None + + if not enum_host_tests_path: + # If there is -e specified we will try to find a host_tests path ourselves + # + # * Path to binary starts from "build" directory, and goes 4 levels + # deep: ./build/tests/compiler/toolchain + # * Binary is inside test group. + # For example: /tests/test_group_name/test_dir/*,cpp. + # * We will search for directory called host_tests on the level of test group (level=2) + # or on the level of tests directory (level=3). + # + # If host_tests directory is found above test code will will pass it to mbedhtrun using + # switch -e + gt_logger.gt_log("checking for 'host_tests' directory above image directory structure", print_text=verbose) + test_group_ht_path = get_binary_host_tests_dir(image_path, level=2) + TESTS_dir_ht_path = get_binary_host_tests_dir(image_path, level=3) + if test_group_ht_path: + enum_host_tests_path = test_group_ht_path + elif TESTS_dir_ht_path: + enum_host_tests_path = TESTS_dir_ht_path + + if enum_host_tests_path: + gt_logger.gt_log_tab("found 'host_tests' directory in: '%s'"% enum_host_tests_path, print_text=verbose) + else: + gt_logger.gt_log_tab("'host_tests' directory not found: two directory levels above image path checked", print_text=verbose) + + gt_logger.gt_log("selecting test case observer...", print_text=verbose) + if digest_source: + gt_logger.gt_log_tab("selected digest source: %s"% digest_source, print_text=verbose) + + # Select who will digest test case serial port data + if digest_source == 'stdin': + # When we want to scan stdin for test results + raise NotImplementedError + elif digest_source is not None: + # When we want to open file to scan for test results + raise NotImplementedError + + # Command executing CLI for host test supervisor (in detect-mode) + cmd = ["mbedhtrun", + '-m', micro, + '-p', port, + '-f', '"%s"'% image_path, + ] + + if enum_host_tests_path: + cmd += ["-e", '"%s"'% enum_host_tests_path] + + if global_resource_mgr: + # Use global resource manager to execute test + # Example: + # $ mbedhtrun -p :9600 -f "tests-mbed_drivers-generic_tests.bin" -m K64F --grm raas_client:10.2.203.31:8000 + cmd += ['--grm', global_resource_mgr] + else: + # Use local resources to execute tests + # Add extra parameters to host_test + if disk: + cmd += ["-d", disk] + if copy_method: + cmd += ["-c", copy_method] + if target_id: + cmd += ["-t", target_id] + if reset: + cmd += ["-r", reset] + if run_app: + cmd += ["--run"] # -f stores binary name! + + if fast_model_connection: + # Use simulator resource manager to execute test + # Example: + # $ mbedhtrun -f "tests-mbed_drivers-generic_tests.elf" -m FVP_MPS2_M3 --fm DEFAULT + cmd += ['--fm', fast_model_connection] + if compare_log: + cmd += ['--compare-log', compare_log] + if program_cycle_s: + cmd += ["-C", str(program_cycle_s)] + if forced_reset_timeout: + cmd += ["-R", str(forced_reset_timeout)] + if json_test_cfg: + cmd += ["--test-cfg", '"%s"' % str(json_test_cfg)] + if num_sync_packtes: + cmd += ["--sync",str(num_sync_packtes)] + if tags: + cmd += ["--tag-filters", tags] + if polling_timeout: + cmd += ["-P", str(polling_timeout)] + + gt_logger.gt_log_tab("calling mbedhtrun: %s" % " ".join(cmd), print_text=verbose) + gt_logger.gt_log("mbed-host-test-runner: started") + + for retry in range(1, 1 + retry_count): + start_time = time() + returncode, htrun_output = run_htrun(cmd, verbose) + end_time = time() + if returncode < 0: + return returncode + elif returncode == 0: + break + gt_logger.gt_log("retry mbedhtrun {}/{}".format(retry, retry_count)) + else: + gt_logger.gt_log("{} failed after {} count".format(cmd, retry_count)) + + testcase_duration = end_time - start_time # Test case duration from reset to {end} + htrun_output = get_printable_string(htrun_output) + result = get_test_result(htrun_output) + result_test_cases = get_testcase_result(htrun_output) + test_cases_summary = get_testcase_summary(htrun_output) + max_heap, reserved_heap, thread_stack_info = get_memory_metrics(htrun_output) + + thread_stack_summary = [] + + if thread_stack_info: + thread_stack_summary = get_thread_stack_info_summary(thread_stack_info) + + memory_metrics = { + "max_heap": max_heap, + "reserved_heap": reserved_heap, + "thread_stack_info": thread_stack_info, + "thread_stack_summary": thread_stack_summary + } + get_coverage_data(build_path, htrun_output) + + gt_logger.gt_log("mbed-host-test-runner: stopped and returned '%s'"% result, print_text=verbose) + return (result, htrun_output, testcase_duration, duration, result_test_cases, test_cases_summary, memory_metrics) diff --git a/tools/python/mbed_greentea/mbed_yotta_api.py b/tools/python/mbed_greentea/mbed_yotta_api.py new file mode 100644 index 0000000000..67b3adbf97 --- /dev/null +++ b/tools/python/mbed_greentea/mbed_yotta_api.py @@ -0,0 +1,25 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.mbed_yotta_api import ( + YottaError, + build_with_yotta, + get_platform_name_from_yotta_target, + get_test_spec_from_yt_module, +) diff --git a/tools/python/mbed_greentea/mbed_yotta_module_parse.py b/tools/python/mbed_greentea/mbed_yotta_module_parse.py new file mode 100644 index 0000000000..2733c3a79d --- /dev/null +++ b/tools/python/mbed_greentea/mbed_yotta_module_parse.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.mbed_yotta_module_parse import ( + YottaConfig, + YottaModule +) diff --git a/tools/python/mbed_greentea/tests_spec.py b/tools/python/mbed_greentea/tests_spec.py new file mode 100644 index 0000000000..93e803e919 --- /dev/null +++ b/tools/python/mbed_greentea/tests_spec.py @@ -0,0 +1,30 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Azim Khan +""" + +""" +This module contains classes to represent Test Specification interface that defines the data to be generated by/from +a build system to give enough information to Greentea. +""" + +from mbed_os_tools.test.tests_spec import ( + TestBinary, + Test, + TestBuild, + TestSpec, +) diff --git a/tools/python/mbed_host_tests/README.md b/tools/python/mbed_host_tests/README.md new file mode 100644 index 0000000000..6451c69faf --- /dev/null +++ b/tools/python/mbed_host_tests/README.md @@ -0,0 +1,1208 @@ +# Development moved + +The development of Htrun has been moved into the [mbed-os-tools](../../src/mbed_os_tools) package. You can continue to use this module for legacy reasons, however all further development should be continued in the new package. + +------------- + +[![PyPI version](https://badge.fury.io/py/mbed-host-tests.svg)](https://badge.fury.io/py/mbed-host-tests) + +# Htrun (mbed-host-tests) + +`htrun` has extensive command line. In most cases `htrun` (or its command line avatar `mbedhtrun`) will be run in background: +* driving test binary flashing, +* device reset and +* test execution. + +Default binary flashing method is one supported by [mbed-enabled](https://www.mbed.com/en/about-mbed/mbed-enabled/) devices: binary file is copied on mbed-enabled DUT (Device Under Test) mounted drive (MSD). This procedure will automatically flash device with given binary file content. + +Default DUT reset method is one supported by [mbed-enabled](https://www.mbed.com/en/about-mbed/mbed-enabled/) devices: serial port (CDC) "*sendBreak*" command resets target MCU on mbed-enabled platform. + +Test execution phase will consist of: +* Opening connection between host computer and DUT, +* DUT will send to host preamble with test runner information such as: + * test environment version, + * test timeout, + * preferred host test script (Python script which is used to supervise/instrument test execution), +* Host will spawn host test script and test execution will be instrumented +* Exchange data (in most cases text) between host and DUT, + + +## Command line overview + +This chapter will present few examples of how you can use `mbedhtrun` command line to execute tests. In most cases test automation tools such as [Greentea](https://github.com/ARMmbed/greentea) will execute `mbedhtrun` implicitly. There are cases when we want to execute `mbedhtrun` independently. Mostly in situation when we want to: +* debug our code and have binary + host test instrumentation on, +* prototype or +* just want to replace test runner in another OS with one compatible with mbed-enabled devices. + +All `mbedhtrun` needs is name of the binary you want to flash and method of flashing! + +## Useful command line end-to-end examples + +### Flashing phase operations + +Flash binary file `/path/to/file/binary.bin` using mount point `D:`. Use serial port `COM4` to communicate with DUT: + +``` +$ mbedhtrun -f /path/to/file/binary.bin -d D: -p COM4 +``` + +Flash (use shell command `copy`) binary file `/path/to/file/binary.bin` using mount point `D:`. Use serial port `COM4` to communicate with DUT: + +``` +$ mbedhtrun -f /path/to/file/binary.bin -d D: -p COM4 -c copy +``` + +Skip flashing phase (e.g. you've already flashed this device with `/path/to/file/binary.bin` binary). Use serial port `COM4` to communicate with DUT: + +``` +$ mbedhtrun -f /path/to/file/binary.bin -d D: -p COM4 --skip-flashing +``` + +### DUT-host communication and reset phase + +Flash binary file `/path/to/file/binary.bin` using mount point `D:`. Use serial port `COM4` with baudrate `115200` to communicate with DUT: + +``` +$ mbedhtrun -f /path/to/file/binary.bin -d D: -p COM4:115200 +``` + +As above but we will skip reset phase (non so common but in some cases can be used to suppress reset phase for some reasons): + +``` +$ mbedhtrun -f /path/to/file/binary.bin -d D: -p COM4:115200 --skip-reset +``` + +Flash binary file `/path/to/file/binary.bin` using mount point `D:`. Use serial port `COM4` with default baudrate to communicate with DUT. Do not send `__sync` key-value protocol synchronization packet to DUT before preamble read: + +``` +$ mbedhtrun -f /path/to/file/binary.bin -d D: -p COM4 --sync=0 +``` + +**Note**: Sync packet management allows you to manipulate the way `htrun` sends `__sync` packet(s) to DUT. With current settings we can force on `htrun` to send `__sync` packets in this manner: +* `--sync=0` - No sync packets will be sent to DUT. +* `--sync=-1`- `__sync` packets will be sent unless we will reach timeout or proper response is sent from DUT. +* `--sync=N` - Where N is integer > 0. Send up to N `__sync` packets to target platform. Response is sent unless we get response from target platform or timeout occurs. + +### Global Resource Manager connection + +Flash local file `/path/to/file/binary.bin` to remote device resource (platform `K64F`) provided by `remote_client` GRM service available on IP address `10.2.203.31` and port: `8000`. Force serial port connection to remote device `9600` with baudrate: + +``` +$ mbedhtrun -p :9600 -f /path/to/file/binary.bin -m K64F --grm remote_client:10.2.203.31:8000 +``` + +Command line switch `--grm` has format: `::`. + * `` - name of Python module to load as remote resource manager. + * `` and `` - IP address and port of remote resource manager. + +**Note**: Switch -m is required to tell Global Resource Management which platform to request. +**Note**: Command line switch `--grm` implicitly forces `--skip-flashing` and `--skip-reset` because both flags are used for locally available DUTs. + +### Fast Model connection + +This option is designed for htrun to use Arm Fast Models. + +The "--fm" option only available when [mbed-fastmodel-agent](https://github.com/ARMmbed/mbed-fastmodel-agent) module is installed : + +Load local file `/path/to/file/binary.elf` to onto fastmodel FVP_MPS2_m3 simulators: + +``` +$ mbedhtrun -f /path/to/file/binary.elf -m FVP_MPS2_M3 --fm DEFAULT +``` + +Command line switch format `--fm `. + * `` - ether pre-defined CONFIG_NAME from mbedfm or a local config file for the Fast Models. + +**Note**: Switch -m is required to tell this fastmodel connection which Fastmodel to request. +**Note**: Command line switch `--fm` implicitly forces `--skip-flashing` and `--skip-reset` because both flags are used for locally available DUTs. + + +### Miscellaneous + +List available host tests names, class names and origin: + +``` +$ mbedhtrun --list +``` + +List available host tests names, class names and origin. Load additional host tests from `/path/to/host_tests` directory: + +``` +$ mbedhtrun --list -e /path/to/host_tests +``` + +List available reset and flashing plugins: + +``` +$ mbedhtrun --plugins +``` + +Flash binary file `/path/to/file/binary.bin` using plugin `stlink`. Use serial port `COM4` with baudrate `115200` to communicate with DUT: + +``` +mbedhtrun -c stlink -f /path/to/file/binary.bin -p COM4:115200 +``` + +# Installation + +`htrun` is redistributed with sources, as Python 2.7 compatible module called `mbed-host-tests` and command line tool called `mbedhtrun`. + +## Installation from PyPI (Python Package Index) +`mbed-host-tests` module is redistributed via PyPI. We recommend you use the [application pip](https://pip.pypa.io/en/latest/installing.html#install-pip). + +**Note:** Python 2.7.9 onwards include ```pip``` by default, so you may have ```pip``` already. +**Note:** `mbed-host-tests` module is redistributed with `mbed-greentea` module as a dependency. So if you've already installed Greentea `mbed-host-tests` should be there! + +To install mbed-ls from [PyPI](https://pypi.python.org/pypi/mbed-host-tests) use command: + +``` +$ pip install mbed-host-tests --upgrade +``` + +## Installation from Python sources +To install the mbed test suite, first clone the `mbed-os-tools` repository: + +``` +$ git clone https://github.com/ARMmbed/mbed-os-tools.git +``` + +Change the directory to the `mbed-os-tools/packages/mbed-host-tests` directory: + +``` +$ cd mbed-os-tools/packages/mbed-host-tests +``` + +Now you are ready to install `htrun`: + +``` +$ python setup.py install +``` + +### Checking installation +To check whether the installation was successful try running the ```mbedgt --help``` command and check that it returns information (you may need to restart your terminal first): + +``` +$ mbedhtrun --help +Usage: mbedgt-script.py [options] + +Flash, reset and perform host supervised tests on mbed platforms + +Options: + -h, --help show this help message and exit +``` + +# mbed-host-tests + +mbed's test suite (codenamed ```Greentea```) supports the *test supervisor* concept. This concept is realized by this module. ```mbed-host-tests``` is a collection of host tests. Host test is script written in Python, which is executed in parallel with the test suite runner (a binary running on the target hardware / device under test) to monitor the test execution's progress or to control the test flow (interaction with the mbed device under test - DUT). The host test is also responsible for grabbing the test result, or deducing it from the test runner's behavior. + +Key-value protocol was developed and is used to provide communication layer between DUT (device under test) and host computer. Key-value protocol defined host computer as master and DUT as slave. +* Slave side APIs and key-value protocol implementation is encapsulated in [greentea-client](https://github.com/ARMmbed/mbed-os/tree/master/features/frameworks/greentea-client) module. +* Master side APIs and key-value protocol is encapsulated in ```mbed-host-tests```. + +```mbed-host-tests``` responsibilities are: +* Flash mbed device with given binary. +* Reset mbed device after flashing to start test suite execution. +* Use key-value protocol to handshake with device and make sure correct host test script is executed to supervise test suite execution. +* Run key-value protocol state machine and execute event callbacks. +* Monitor serial port traffic to parse valid key-value protocol events. +* Make decision if test test suite passed / failed / returned error. +* Provide command line tool interface, command: ```mbedhtrun``` after module installation (on host). +* Provide few basic host test implementations which can be used out of the box for test development. For example the basic host test (called ```default``` or ```default_auto```) just parses events from DUT and finished host test execution when ```end``` event is received. Other included in this module host tests can help you to test timers or RTC. + +## Key-value protocol overview + +* Text based protocol, format ```{{KEY;VALUE}}}```. +* Master-slave mode where host is master and DUT is slave. + +## Design draft +* Simple key-value protocol is introduced. It is used to communicate between DUT and host. Protocol main features: +* Protocol introduced is master-slave protocol, where master is host and slave is device under test. +* Transport layer consist of simple ```{{ KEY ; VALUE }} \n``` text messages sent by slave (DUT). Both key and value are strings with allowed character set limitations (to simplify parsing and protocol parser itself). Message ends with required by DUT K-V parser `\n` character. +* DUT always (except for handshake phase) initializes communication by sending key-value message to host. +* To avoid miscommunication between master and slave simple handshake protocol is introduces: + * Master (host) sends sync packet: ```{{__sync;UUID-STRING}}}``` with message value containing random UUID string. + * DUT waits for ```{{__sync;...}}``` message in input stream and replies with the same packer ```{{__sync;...}}```. + * After correct sync packet is received by master, messages ```{{__timeout;%d}}``` and ```{{__host_test_name}}``` are expected. + * Host parses DUTs tx stream and generates events sent to host test. + * Each event is a tuple of ```(key, value, timestamp)```, where key and value are extracted from message and +* Host tests are now driven by simple async feature. Event state machine on master side is used to process events from DUT. Each host test is capable of registering callbacks, functions which will be executed when event occur. Event name is identical with KEY in key-value pair send as event from/to DUT. +* DUT slave side uses simple parser to parse key-value pairs from stream. All non key-value data will be ignored. Blocking wait for an event API is provided: This implies usage of master-slave exchange between DUT and host where DUT uses non-blocking send event API to send to host (master) event and can wait for response. Master implements corresponding response after receiving event and processing data. + * Message parsing transforms key-value string message to Python event in this order: + * ```{{key;value}}``` string captured on DUT output. + * key-value data becomes a recognizable message with key (string) and value (string) payload. + * Event is formed in host test, a tuple of ```key``` (string), ```value``` (string), ```timestamp``` where ```timestamp``` is time of message reception in Python [time.time()](https://docs.python.org/2/library/time.html#time.time) format (float, time in seconds since the epoch as a floating point number.). +* Each host test registers callbacks for available events. +* Few keys' names in key-value messaging protocol are promoted to be considered "system events". Their names are used by event loop mechanism to communicate between DUT, host and various internal components. Please do not use restricted even names for your own private events. What's more: + * User can't register callbacks to "system events" with few exceptions. + * Reserved event/message keys have leading ```__``` in name: + * ```__sync``` - sync message, used by master and DUT to handshake. + * ```__notify_sync_failed``` - sent by host when sync response not received from DUT. + * ```__timeout``` - timeout in sec, sent by DUT after ```{{sync;UUID}}``` is received. + * ```__version``` - ```greentea-client``` version send from DUT to host. + * ```__host_test_name``` - host test name, sent by DUT after ```{{sync;UUID}}``` is received. + * ```__notify_prn``` - sent by host test to print log message. + * ```__notify_conn_lost``` - sent by host test's connection process to notify serial port connection lost. + * ```__notify_complete``` - sent by DUT, async notificaion about test case result (true, false, none). + * ```__coverage_start``` - sent by DUT, coverage data. + * ```__testcase_start``` - sent by DUT, test case start data. + * ```__testcase_finish``` - sent by DUT, test case result. + * ```__exit``` - sent by DUT, test suite execution finished. + * ```__exit_event_queue``` - sent by host test, indicating no more events expected. + * Non-Reserved event/message keys have leading ```__``` in name: + * ```__rxd_line``` - Event triggered when ```\n``` was found on DUT RXD channel. It can be overridden (```self.register_callback('__rxd_line', )```) and used by user. Event is sent by host test to notify a new line of text was received on RXD channel. ```__rxd_line``` event payload (value) in a line of text received from DUT over RXD. +* Each host test (master side) has four functions used by async framework: + * ```setup()``` used to initialize host test and register callbacks. + * ```result()``` used to return test case result when ```notify_complete()``` is not called. + * ```teardown()``` used to finalize and resource freeing. It is guaranteed that ```teardown()``` will be always called after timeout or async test completion(). + * ```notify_complete(result : bool)``` used by host test to notify test case result. This result will be read after test suite ```TIMEOUT```s or after DUT send ```__exit``` message (test suite execution finished event). + * ```self.send_kv(key : string, value : string)``` - send key-value message to DUT. + * ```self.log(text : string)``` - send event ```__notify_prn``` with text as payload (value). Your message will be printed in log. +* Result returned from host test is a test suite result. Test cases results are reported by DUT, usually using modified ```utest``` framework. + +# Greentea client API + +DUT test API was first introduced in ```mbedmicro/mbed``` project [here](https://github.com/mbedmicro/mbed/tree/master/libraries/tests/mbed/env). After refactoring this functionality was copied and improved in [greentea-client](https://github.com/ARMmbed/mbed-os/tree/master/features/frameworks/greentea-client) module. + +* Slave side key-value protocol API, see [here](https://github.com/ARMmbed/mbed-os/blob/master/features/frameworks/greentea-client/greentea-client/test_env.h) for details. + +```c++ +// Send key-value pairs from slave to master +void greentea_send_kv(const char *, const char *); +void greentea_send_kv(const char *, const int); +void greentea_send_kv(const char *, const int, const int); +void greentea_send_kv(const char *, const char *, const int); +void greentea_send_kv(const char *, const char *, const int, const int); + +// Blocking, receive key-value message from master +int greentea_parse_kv(char *, char *, const int, const int); +``` +Functions are used to send key-string or key-integer value messages to master. This functions should replace typical ```printf()``` calls with payload/control data to host. + +* **Blocking** wait for key-value pair message in input stream: + +```c++ +int greentea_parse_kv(char *out_key, char *out_value, const int out_key_len, const int out_value_len); +``` + +This function should replace ```scanf()``` used to check for incoming messages from master. +Function parses input and if key-value message is found load to ```out_key```, ```out_value``` key-value pair. Use ```out_key_size``` and ```out_value_size``` to define out buffers max size (including trailing zero). + +# Key-value transport protocol sequence + +Key-value protocol has few parts: +* **Handshake** - synchronize master and slave. +* **Preamble exchange** - DUT informs host about test parameters such as client version, test suite timeout, requested host test name etc. After this part is finished master will create requested host test and attach callbacks to user events. +* **Event exchange** - key-value event exchange between slave and master. In this exchange in general slave (DUT) will initialize communication. This part may end with ending pair of events ```end``` and ```__exit``` where ```end``` event carries test suite result returned by DUT and ```__exit``` event marks test suite ended and exited. After ```__exit``` event is received there will be no more communication between DUT and host test. + +## Handshake +Hanshake between DUT and host is a sequence of ```__sync``` events send between host (master) and DUT (slave). This is currently only situation when master initiates communication first. Handshake should provide synchronization point where master and slave are starting the same session. + +After reset: +* DUT calls function ```GREENTEA_SETUP(timeout, "host test name");``` which +* calls immediately ```greentea_parse_kv``` (blocking parse of input serial port for event ```{{__sync;UUID}}```). +* When ```__sync``` packet is parsed in the stream DUT sends back (echoes) ```__sync``` event with the same [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_3_.28MD5_hash_.26_namespace.29) as payload. UUID is a random value e.g. ```5f8dbbd2-199a-449c-b286-343a57da7a37```. + +``` + DUT (slave) host (master) + ----- ----- + | | + DUT reset ---> | | + | | +greentea_parse_kv(key,value) | | +-------[ blocking ]----------->| | + | | + . . + . . + | | self.send_kv("__sync", UUID) + | {{__sync;UUID}} |<----------------------------- + |<------------------| + | | + | | +greentea_parse_kv | {{__sync;UUID}} | +echoes __sync event with |------------------>| +the same UUID to master | | + | | +``` + +Example of handshake from ```htrun``` log: + +DUT code: + +```c +// GREENTEA_SETUP pseudo-code +void GREENTEA_SETUP(const int timeout, const char *host_test_name) { + // Wait for SYNC and echo it back + char _key[8] = {0}; + char _value[48] = {0}; + while (1) { + greentea_parse_kv(_key, _value, sizeof(_key), sizeof(_value)); + if (strcmp(_key, GREENTEA_TEST_ENV_SYNC) == 0) { + // Found correct __sunc message + greentea_send_kv(_key, _value); + break; + } + } + + // Send PREAMBLE: client version, test suite timeout and requested host test + greentea_send_kv(GREENTEA_TEST_ENV_HOST_TEST_VERSION, "0.1.8"); + greentea_send_kv(GREENTEA_TEST_ENV_TIMEOUT, timeout); + greentea_send_kv(GREENTEA_TEST_ENV_HOST_TEST_NAME, host_test_name); +} +``` + +Corresponding log: + +``` +[1458565465.35][SERI][INF] reset device using 'default' plugin... +[1458565465.60][SERI][INF] wait for it... +[1458565466.60][CONN][INF] sending preamble '2f554b1c-bbbf-4b1b-b1f0-f45493282f2c' +[1458565466.60][SERI][TXD] mbedmbedmbedmbedmbedmbedmbedmbedmbedmbed +[1458565466.60][SERI][TXD] {{__sync;2f554b1c-bbbf-4b1b-b1f0-f45493282f2c}} +[1458565466.74][CONN][INF] found SYNC in stream: {{__sync;2f554b1c-bbbf-4b1b-b1f0-f45493282f2c}}, queued... +[1458565466.74][HTST][INF] sync KV found, uuid=2f554b1c-bbbf-4b1b-b1f0-f45493282f2c, timestamp=1458565466.743000 +[1458565466.74][CONN][RXD] {{__sync;2f554b1c-bbbf-4b1b-b1f0-f45493282f2c}} +``` + +## Preamble exchange + +This phase comes just after handshake phase. DUT informs host about test parameters such as client version, timeout, requested host test name etc. After this part is finished master will create requested host test and attach callbacks to user events. +This phase is ended with ```__host_test_name``` being received by host. After ```__host_test_name``` event is received + +``` +DUT (slave) host (master) + ----- ----- + | | + | {{__version;%s}} | + |------------------------>| + | | + | {{__timeout;%d}} | + |------------------------>| + | | + | {{__host_test_name;%s}} | + |------------------------>| + | | +``` + +Example of handshake from ```htrun``` log: + +* DUT code: + +```c +void main() { + GREENTEA_CLIENT(5, "default_auto"); + // ... +} +``` + +* Corresponding log: + +``` +[1458565466.76][CONN][INF] found KV pair in stream: {{__version;0.1.8}}, queued... +[1458565466.76][CONN][RXD] {{__version;0.1.8}} +[1458565466.76][HTST][INF] DUT greentea-client version: 0.1.8 +[1458565466.77][CONN][INF] found KV pair in stream: {{__timeout;5}}, queued... +[1458565466.77][HTST][INF] setting timeout to: 5 sec +[1458565466.78][CONN][RXD] {{__timeout;5}} +[1458565466.81][CONN][INF] found KV pair in stream: {{__host_test_name;default_auto}}, queued... +[1458565466.81][HTST][INF] host test setup() call... +[1458565466.81][HTST][INF] CALLBACKs updated +[1458565466.81][HTST][INF] host test detected: default_auto +[1458565466.81][CONN][RXD] {{__host_test_name;default_auto}} +``` + +## Event exchange + +In this phase DUT and host exchange events and host side is calling callbacks registered to each of the events sent from DUT. DUT can use function ```greentea_parse_kv``` to parse input stream for next incoming key-value event. +After ```__host_test_name``` event is received and before any event is consumed during this stage: +* Host state machine loads host test object by name provided in payload of ```__host_test_name``` event.E.g. event ```{{____host_test_name;default_auto}} will load host test named "*default_auto*". +* Host state machine loads callbacks registered by user in host test setup phase and hooks them to event machine. +Now host is ready to handle test suite test execution. From this moment each event sent from DUT will be handled by corresponding callback registered by user in host test setup. Unknown events will not be handled and warning will be printed in log. + +``` + +DUT (slave) host (master) + ----- ----- + | | + | | Host Test + | | ----- + | | create | + | |---------->| + | | | + | | | + | {{key1;value}} | | + |---------------->| | ht.setup() + | . | |<---[ user register callbacks ]--- + | . | | + | . | | host.callbacks.update(ht.get_callbacks()) + | . | |<---[ host state machine ]------------------ + | {{key2;value}} | | + |---------------->| | + | | | + | | | + | | | ht.callbacks[key1](key, value, timestamp) + | | |<------------------------------------------ + | | | ht.callbacks[key2](key, value, timestamp) + | | |<------------------------------------------ + | | | + | | | + - - - - - - - - - - - - - - - + TEST CASE FLOW CONTINUES + - - - - - - - - - - - - - - - + | | | + | | | ht.notify_complete(true) + | | | (sets test suite 'result' to true + | | |<---------------- + | | | + | | | + | {{end;success}} | | + |---------------->| | + | | | + | {{__exit;%d}} | | + |---------------->| | + | | | + | | | result = ht.result() + | | |<---------------- + | | | + | | | ht.teardown() + | | |<---------------- + | | | + | | | + +``` + +* After DUT send ```__exit``` or after timeout it is guaranteed that host test ```teardown()``` function will be called. This call is blocking, please make sure your tear down function finishes. + +# DUT - host test case workflow +## DUT implementation + +```c++ +int main() { + // 1. Handshake between DUT and host and + // 2. Send test case related data + GREENTEA_SETUP(15, "gimme_auto"); // __timeout, __host_test_name + + // ... + // Send to master {{gimme_something; some_stuff}} + greentea_send_kv("gimme_something", "some_stuff"); + + char key[16] = {0}; + char value[32] = {0}; + // Blocking wait for master response for {{gimme_something; some_stuff}} + greentea_parse_kv(key, value, sizeof(key), sizeof(value)); + // ... + fprintf(stderr, "Received from master %s, %s", key, value); + // ... + + GREENTEA_TESTSUITE_RESULT(true); // __exit +} +``` + +## Example of corresponding host test + +```python +class GimmeAuto(BaseHostTest): + """ Simple, basic host test's test runner waiting for serial port + output from MUT, no supervision over test running in MUT is executed. + """ + + __result = None + name = "gimme_auto" + + def _callback_gimme_something(self, key, value, timestamp): + # You've received {{gimme_something;*}} + + # We will send DUT some data back... + # And now decide about test case result + if value == 'some_stuff': + # Message payload/value was 'some_stuff' + # We can for example return true from test + self.send_kv("print_this", "This is what I wanted %s"% value) + self.notify_complete(True) + else: + self.send_kv("print_this", "This not what I wanted :(") + self.notify_complete(False) + + def setup(self): + # Register callback for message 'gimme_something' from DUT + self.register_callback("gimme_something", self._callback_gimme_something) + + # Initialize your host test here + # ... + + def result(self): + # Define your test result here + # Or use self.notify_complete(bool) to pass result anytime! + return self.__result + + def teardown(self): + # Release resources here after test is completed + pass +``` + +Log: + +``` +[1454926794.22][HTST][INF] copy image onto target... + 1 file(s) copied. +[1454926801.48][HTST][INF] starting host test process... +[1454926802.01][CONN][INF] starting connection process... +[1454926802.01][CONN][INF] initializing serial port listener... +[1454926802.01][SERI][INF] serial(port=COM188, baudrate=9600) +[1454926802.02][SERI][INF] reset device using 'default' plugin... +[1454926802.27][SERI][INF] wait for it... +[1454926803.27][CONN][INF] sending preamble '9caa42a0-28a0-4b80-ba1d-befb4e43a4c1'... +[1454926803.27][SERI][TXD] mbedmbedmbedmbedmbedmbedmbedmbedmbedmbed +[1454926803.27][SERI][TXD] {{__sync;9caa42a0-28a0-4b80-ba1d-befb4e43a4c1}} +[1454926803.40][CONN][RXD] {{__sync;9caa42a0-28a0-4b80-ba1d-befb4e43a4c1}} +[1454926803.40][CONN][INF] found SYNC in stream: {{__sync;9caa42a0-28a0-4b80-ba1d-befb4e43a4c1}}, queued... +[1454926803.40][HTST][INF] sync KV found, uuid=9caa42a0-28a0-4b80-ba1d-befb4e43a4c1, timestamp=1454926803.405000 +[1454926803.42][CONN][RXD] {{__timeout;15}} +[1454926803.42][CONN][INF] found KV pair in stream: {{__timeout;15}}, queued... +[1454926803.42][HTST][INF] setting timeout to: 15 sec +[1454926803.45][CONN][RXD] {{__host_test_name;gimme_auto}} +[1454926803.45][CONN][INF] found KV pair in stream: {{__host_test_name;gimme_auto}}, queued... +[1454926803.45][HTST][INF] host test setup() call... +[1454926803.45][HTST][INF] CALLBACKs updated +[1454926803.45][HTST][INF] host test detected: gimme_auto +[1454926803.48][CONN][RXD] {{gimme_something;some_stuff}} +[1454926803.48][CONN][INF] found KV pair in stream: {{gimme_something;some_stuff}}, queued... +[1454926803.48][SERI][TXD] {{print_this;This is what I wanted some_stuff}} +[1454926803.48][HTST][INF] __notify_complete(True) +[1454926803.62][CONN][RXD] Received from master print_this, This is what I wanted some_stuf +[1454926803.62][CONN][RXD] {{end;success}} +[1454926803.62][CONN][INF] found KV pair in stream: {{end;success}}, queued... +[1454926803.62][HTST][ERR] orphan event in main phase: {{end;success}}, timestamp=1454926803.625000 +[1454926803.63][CONN][RXD] {{__exit;0}} +[1454926803.63][CONN][INF] found KV pair in stream: {{__exit;0}}, queued... +[1454926803.63][HTST][INF] __exit(0) +[1454926803.63][HTST][INF] test suite run finished after 0.21 sec... +[1454926803.63][HTST][INF] exited with code: None +[1454926803.63][HTST][INF] 0 events in queue +[1454926803.63][HTST][INF] stopped consuming events +[1454926803.63][HTST][INF] host test result() skipped, received: True +[1454926803.63][HTST][INF] calling blocking teardown() +[1454926803.63][HTST][INF] teardown() finished +[1454926803.63][HTST][INF] {{result;success}} +mbedgt: mbed-host-test-runner: stopped +mbedgt: mbed-host-test-runner: returned 'OK' +mbedgt: test on hardware with target id: 02400226d94b0e770000000000000000000000002492f3cf +mbedgt: test suite 'mbed-drivers-test-gimme' ......................................................... OK in 10.02 sec +mbedgt: shuffle seed: 0.3631708941 +mbedgt: test suite report: ++---------------+---------------+-------------------------+--------+--------------------+-------------+ +| target | platform_name | test suite | result | elapsed_time (sec) | copy_method | ++---------------+---------------+-------------------------+--------+--------------------+-------------+ +| frdm-k64f-gcc | K64F | mbed-drivers-test-gimme | OK | 10.02 | shell | ++---------------+---------------+-------------------------+--------+--------------------+-------------+ +mbedgt: test suite results: 1 OK +``` + +# Host test examples +## Return result after __exit + +```python +class GimmeAuto(BaseHostTest): + """ Simple, basic host test's test runner waiting for serial port + output from MUT, no supervision over test running in MUT is executed. + """ + + __result = None + name = "gimme_auto" + + def _callback_gimme_something(self, key, value, timestamp): + # You've received {{gimme_something;*}} + + # We will send DUT some data back... + # And now decide about test case result + if value == 'some_stuff': + # Message payload/value was 'some_stuff' + # We can for example return true from test + self.send_kv("print_this", "This is what I wanted %s"% value) + self.__result = True + else: + self.send_kv("print_this", "This not what I wanted :(") + self.__result = False + + def setup(self): + # Register callback for message 'gimme_something' from DUT + self.register_callback("gimme_something", self._callback_gimme_something) + + # Initialize your host test here + # ... + + def result(self): + # Define your test result here + # Or use self.notify_complete(bool) to pass result anytime! + return self.__result + + def teardown(self): + # Release resources here after test is completed + pass +``` + +Corresponding log: + +``` +[1454926627.11][HTST][INF] copy image onto target... + 1 file(s) copied. +[1454926634.38][HTST][INF] starting host test process... +[1454926634.93][CONN][INF] starting connection process... +[1454926634.93][CONN][INF] initializing serial port listener... +[1454926634.93][SERI][INF] serial(port=COM188, baudrate=9600) +[1454926634.94][SERI][INF] reset device using 'default' plugin... +[1454926635.19][SERI][INF] wait for it... +[1454926636.19][CONN][INF] sending preamble '9a743ff3-45e6-44cf-9e2a-9a83e6205184'... +[1454926636.19][SERI][TXD] mbedmbedmbedmbedmbedmbedmbedmbedmbedmbed +[1454926636.19][SERI][TXD] {{__sync;9a743ff3-45e6-44cf-9e2a-9a83e6205184}} +[1454926636.33][CONN][RXD] {{__sync;9a743ff3-45e6-44cf-9e2a-9a83e6205184}} +[1454926636.33][CONN][INF] found SYNC in stream: {{__sync;9a743ff3-45e6-44cf-9e2a-9a83e6205184}}, queued... +[1454926636.33][HTST][INF] sync KV found, uuid=9a743ff3-45e6-44cf-9e2a-9a83e6205184, timestamp=1454926636.331000 +[1454926636.34][CONN][RXD] {{__timeout;15}} +[1454926636.34][CONN][INF] found KV pair in stream: {{__timeout;15}}, queued... +[1454926636.34][HTST][INF] setting timeout to: 15 sec +[1454926636.38][CONN][RXD] {{__host_test_name;gimme_auto}} +[1454926636.38][CONN][INF] found KV pair in stream: {{__host_test_name;gimme_auto}}, queued... +[1454926636.38][HTST][INF] host test setup() call... +[1454926636.38][HTST][INF] CALLBACKs updated +[1454926636.38][HTST][INF] host test detected: gimme_auto +[1454926636.41][CONN][RXD] {{gimme_something;some_stuff}} +[1454926636.41][CONN][INF] found KV pair in stream: {{gimme_something;some_stuff}}, queued... +[1454926636.41][SERI][TXD] {{print_this;This is what I wanted some_stuff}} +[1454926636.54][CONN][RXD] Received from master print_this, This is what I wanted some_stuf +[1454926636.54][CONN][RXD] {{end;success}} +[1454926636.54][CONN][INF] found KV pair in stream: {{end;success}}, queued... +[1454926636.55][HTST][ERR] orphan event in main phase: {{end;success}}, timestamp=1454926636.541000 +[1454926636.56][CONN][RXD] {{__exit;0}} +[1454926636.56][CONN][INF] found KV pair in stream: {{__exit;0}}, queued... +[1454926636.56][HTST][INF] __exit(0) +[1454926636.56][HTST][INF] test suite run finished after 0.22 sec... +[1454926636.56][HTST][INF] exited with code: None +[1454926636.56][HTST][INF] 0 events in queue +[1454926636.56][HTST][INF] stopped consuming events +[1454926636.56][HTST][INF] host test result(): True +[1454926636.56][HTST][INF] calling blocking teardown() +[1454926636.56][HTST][INF] teardown() finished +[1454926636.56][HTST][INF] {{result;success}} +mbedgt: mbed-host-test-runner: stopped +mbedgt: mbed-host-test-runner: returned 'OK' +mbedgt: test on hardware with target id: 02400226d94b0e770000000000000000000000002492f3cf +mbedgt: test suite 'mbed-drivers-test-gimme' ......................................................... OK in 10.04 sec +mbedgt: shuffle seed: 0.3866075474 +mbedgt: test suite report: ++---------------+---------------+-------------------------+--------+--------------------+-------------+ +| target | platform_name | test suite | result | elapsed_time (sec) | copy_method | ++---------------+---------------+-------------------------+--------+--------------------+-------------+ +| frdm-k64f-gcc | K64F | mbed-drivers-test-gimme | OK | 10.04 | shell | ++---------------+---------------+-------------------------+--------+--------------------+-------------+ +mbedgt: test suite results: 1 OK +``` + +# Writing DUT test suite (slave side) + +## DUT test suite with single test case + +We can use few methods to structure out test suite and test cases. Simpliest would be to use ```greentea-client``` API and wrap one test case inside out test suite. This way of creating test suite is useful when you want to: +* write only one test case inside test suite, +* make example application (example as a test) or +* when your test suite is calling blocking forever function. For example all types of UDP/TCP servers which run in forever loop are in this category. In this case we do not expect from DUT ```__exit``` event at all and host test should be designed in such a way that it always return result. + +### DUT always finishes execution + +In this example DUT code uses ```greentea-client``` to sync (```GREENTEA_SETUP```) and pass result (```GREENTEA_TESTSUITE_RESULT```) to ```Greentea```. This is very simple example of how you can write tests. Note that in this example test suite only implements one test case. Actually test suite is test case at the same time. Result passed to ```GREENTEA_TESTSUITE_RESULT``` will be at the same time test case result. + +* DUT implementation: + +```c++ +#include "greentea-client/test_env.h" +#include "unity/unity.h" // Optional: unity ASSERTs + +int app_start(int, char*[]) { + + bool result = true; + GREENTEA_SETUP(15, "default_auto"); + + // test case execution and assertions + + GREENTEA_TESTSUITE_RESULT(result); + return 0; +} +``` + +### DUT test suite never finishes execution + +Test suite is implemented so that it will never exit / finish its execution. For example ```main()``` or ```app_start()``` functions are implemented using infinite (endless) loop. This property have for example UDP/TCP servers (listening forever), all sorts of echo servers etc. + +In this example DUT code uses ```greentea-client``` to sync (```GREENTEA_SETUP```) with ```Greentea```. We are not calling ```GREENTEA_TESTSUITE_RESULT(result)``` at any time. In this example host test is responsible for providing test suite result using ```self.notify_complete()``` API or ```self.result()``` function. + +You need to write and specify by name your custom host test: +* DUT side uses second argument of ```GREENTEA_SETUP(timeout, host_test_name)``` function: + +```c++ +GREENTEA_SETUP(15, "wait_us_auto"); +``` + +* You need to place your custom host test in ```/test/host_tests``` directory. + * Do not forget to name host test accordingly. See below example host test ```name``` class member. + +* DUT implementation using ```my_host_test``` custom host test: + +```c++ +#include "greentea-client/test_env.h" +#include "unity/unity.h" + +void recv() { + // receive from client +} + +int app_start(int, char*[]) { + + Ethernet eth(TCP_SERVER, PORT, recv); + GREENTEA_SETUP(15, "my_host_test"); + + eth.listen(); // Blocking forever + + return 0; +} +``` + +* Example host test template: + +```python +from mbed_host_tests import BaseHostTest + +class YourCustomHostTest(BaseHostTest): + + name = "my_host_test" # Host test names used by GREENTEA_CLIENT(..., host_test_name) + + __result = False # Result in case of timeout! + + def _callback_for_event(self, key, value, timestamp): + # + # Host test API: + # + # self.notify_complete(result : bool) + # + # """! Notify main even loop that host test finished processing + # @param result True for success, False failure. If None - no action in main even loop + # """ + # + # self.send_kv(key : string, value : string) + # + # """! Send Key-Value data to DUT + # @param key Event key + # @param value Event payload + # """ + # + # self.log(text : string) + # + # """! Send log message to main event loop + # @param text log message + # """ + pass + + def setup(self): + # TODO: + # * Initialize your resources + # * Register callbacks: + # + # Host test API: + # + # self.register_callback(event_name, callable, force=False) + # + # """! Register callback for a specific event (key: event name) + # @param key String with name of the event + # @param callback Callable which will be registered for event "key" + # @param force God mode, if set to True you can add callback on any system event + # """ + pass + + def teardown(self): + # Destroy all resources used by host test. + # For example open sockets, open files, auxiliary threads and processes. + pass + + def result(self): + # Returns host test result (True, False or None) + # This function will be called when test suite ends (also timeout). + # Use when you want to pass result after host state machine stops. + return __result +``` + +### DUT test suite with ```utest``` harness + +```utest``` harness allows you to define multiple test cases inside your test suite. This feature is supported by ```Greentea``` test tools. + +* DUT implementation: + +```c++ +#include "greentea-client/test_env.h" +#include "unity/unity.h" +#include "utest/utest.h" + +status_t greentea_failure_handler(const Case *const source, const failure_t reason) { + // Continue with next test case if it fails + greentea_case_failure_abort_handler(source, reason); + return STATUS_CONTINUE; +} + +void test_uninitialised_array() { + // TEst case code... +} + +void test_repeated_init() { + // TEst case code... +} + +void test_data_types() { + // TEst case code... +} + +const Case cases[] = { + Case("Test uninitialised array", test_uninitialised_array, greentea_failure_handler), + Case("Test repeated array initialisation", test_repeated_init, greentea_failure_handler), + Case("Test basic data type arrays", test_data_types, greentea_failure_handler) + // ... +}; + +status_t greentea_setup(const size_t number_of_cases) { + GREENTEA_SETUP(5, "default_auto"); + return greentea_test_setup_handler(number_of_cases); +} + +int app_start(int, char*[]) { + + // Run the test cases + Harness::run(specification); +} +``` + +# Writing host tests (master side) +When writing a new host test for your module please bear in mind that: +* You own the host test and you should write it the way so it can coexist with the same host tests ran by other processes such as Continuous Integration systems or other host users. + * Note: If you work in isolation and your test environment if fully controlled by you (for example you queue all tasks calling host tests, or use global host unique socket port numbers) this rule doesn’t apply to you. +* When writing host test using OS resources such as sockets, files, serial ports, peripheral devices like for example multi-meters / scopes. remember that those resources are indivisible! + * For example if you hardcode in your host test UDP port 32123 and use it for UDP server implementation of your host test bear in mind that this port may be already used. It is your responsibility to react for this event and implement means to overcome it (if possible). + +## Callbacks +You can register callbacks in ```setup()``` phase or decorate callback functions using ```@event_callback``` decorator. + +### Callback registration in setup() method + +```python +from mbed_host_tests import BaseHostTest + +class DetectRuntimeError(BaseHostTest): + + __result = False + + def callback_some_event(self, key, value, timeout): + # Do something with 'some_event' + pass + + def setup(self): + # Reagister call back for 'some_event' event + self.register_callback('some_event', self.callback_some_event) + + def result(self): + # Do some return calculations + return self.__result +``` + +Below the same callback registered using decorator: + +### Callback decorator definition + +```python +from mbed_host_tests.host_tests import BaseHostTest, event_callback + +class DetectRuntimeError(BaseHostTest): + + __result = False + + @event_callback('some_event') + def callback_some_event(self, key, value, timeout): + # Do something with 'some_event' + pass + + def setup(self): + # Do some extra setup if required + # You can also register here callbacks using self.register_callback(...) method + pass + + def result(self): + # Do some return calculations + return self.__result +``` + +### Parsing text received from DUT (line by line) +Example of host test expecting ```Runtime error ... CallbackNode ... ``` string in DUT output. +We will use allowed to override ```__rxd_line``` event to hook to DUT RXD channel lines of text. + +#### Before Greentea v0.2.0 + +```python +from sys import stdout +from mbed_host_tests import BaseHostTest + +class DetectRuntimeError(BaseHostTest): + + name = 'detect_runtime_error' + + def test(self, selftest): + result = selftest.RESULT_FAILURE + try: + while True: + line = selftest.mbed.serial_readline() + + if line is None: + return selftest.RESULT_IO_SERIAL + + stdout.write(line) + stdout.flush() + + line = line.strip() + + if line.startswith("Runtime error") and line.find("CallbackNode") != -1: + result = selftest.RESULT_SUCCESS + break + + except KeyboardInterrupt, _: + selftest.notify("\r\n[CTRL+C] exit") + result = selftest.RESULT_ERROR + + return result +``` + +#### Using __rdx_line event + +```python +from mbed_host_tests import BaseHostTest + +class DetectRuntimeError(BaseHostTest): + """! We _expect_ to detect 'Runtime error' """ + + __result = False + + def callback__rxd_line(self, key, value, timeout): + # + # Parse line of text received over e.g. serial from DUT + # + line = value.strip() + if line.startswith("Runtime error") and "CallbackNode" in line: + # We've found exepcted "Runtime error" string in DUTs output stream + self.notify_complete(True) + + def setup(self): + # Force, we force callback registration even it is a restricted one (starts with '__') + self.register_callback('__rxd_line', self.callback__rxd_line, force=True) + + def result(self): + # We will return here (False) when we reach timeout of the test + return self.__result + + def teardown(self): + pass +``` + +## ```htrun``` new log format: + * ```[timestamp][source][level]``` - new log format, where: + * ```timestamp``` - returned by Python's ```time.time()```. + * ```source``` - log source. + * ```CONN``` - connection process (pooling for connection source e.g. serial port), + * ```SERI``` - serial port wrapper with standard read, write, flush interface, + * ```HTST``` - host test object, HostTestBase derived object, + * ```PLGN``` - host test plugins, type `BasePlugin` of the plugin, + * ```COPY``` - host test plugins, type `CopyMethod` of the plugin, + * ```REST``` - host test plugins, type `ResetMethod` of the plugin, + * ```level``` - logging level: + * ```INF``` (info), + * ```WRN``` (warning), + * ```ERR``` (error). + * ```TXD``` (host's TX channel, to DUT). + * ```RXD``` (host's RX channel, from DUT). + +### Log example +* ```[1455218713.87][CONN][RXD] {{__sync;a7ace3a2-4025-4950-b9fc-a3671103387a}}```: +* Logged from ```CONN``` (connection process). +* ```RXD``` channel emitted ```{{__sync;a7ace3a2-4025-4950-b9fc-a3671103387a}}```. +* Time stamp: ```2016-02-11 19:53:27```, see below: + +# Plugins + +In order to work with platforms for which the hardware is still under development, and hence may not have an mbed interface chip, some "hook" files are required. Operation with these platforms is a matter for the platform development teams involved and is not, in general, supported by ARM. + +## SARA NBIOT EVK +The SARA NBIOT EVK board must be connected to a Windows PC using a Segger JLink box, which is used for downloading code and resetting the board. The USB port on the EVK must also be connected to the same PC. To make use of these hooks you will also require access to some proprietary tools that can be requested from u-blox. + + +# Testing mbed-os examples + +mbed-os examples are essentially sample apps written as inspirational code for developers to understand the mbed-os APIs and coding paradigms. Before every mbed-os release all examples are tested across all supported configs and platforms. There is already a large set examples available and as they grow it is important to automate them. Hence automating examples make sense. Although it is important not to pollute them with test like instrumentation. As that will defeat the purpose of examples being simple and specific. + +Hence the strategy for testing examples is based on observation instead of interaction. An example's serial logging is captured and converted into a templated log. All successive executions of this example should match this log. + +Templated log simply means a log with text that does not change or regular expressions replacing original text. Below is an example of the templated log: + +``` + + > Using Ethernet LWIP + + > Client IP Address is 10.2.203.139 + + > Connecting with developer.mbed.org + +Starting the TLS handshake... Starting the TLS handshake... + + > TLS connection to developer.mbed.org established + +Server certificate: Server certificate: + + > + cert. version : 3 + > + serial number : 11:21:B8:47:9B:21:6C:B1:C6:AF:BC:5D:0 + > + issuer name : C=BE, O=GlobalSign nv-sa, CN=GlobalSi + > + subject name : C=GB, ST=Cambridgeshire, L=Cambridge, + > + issued on : 2016-03-03 12:26:08 + > + expires on : 2017-04-05 10:31:02 + > + signed using : RSA with SHA-256 + > + RSA key size : 2048 bits + > + basic constraints : CA=false + > + subject alt name : *.mbed.com, mbed.org, *.mbed.org, mbe + > + key usage : Digital Signature, Key Encipherment + > + ext key usage : TLS Web Server Authentication, TLS We + +Certificate verification passed Certificate verification passed + + + + + > HTTPS: Received 439 chars from server + + > HTTPS: Received 200 OK status ... [OK] + +HTTPS: Received 'Hello world!' status ... [OK] HTTPS: Received 'Hello world!' status ... [OK] + +HTTPS: Received message: HTTPS: Received message: + + + + + > HTTP/1.1 200 OK + + > Server: nginx/1.7.10 + + > Date: Thu, 01 Dec 2016 13:56:32 GMT + + > Content-Type: text/plain + + > Content-Length: 14 + + > Connection: keep-alive + + > Last-Modified: Fri, 27 Jul 2012 13:30:34 GMT + + > Accept-Ranges: bytes + + > Cache-Control: max-age=36000 + + > Expires: Thu, 01 Dec 2016 23:56:32 GMT + + > X-Upstream-L3: 172.17.0.3:80 + + > X-Upstream-L2: developer-sjc-indigo-2-nginx + + > Strict-Transport-Security: max-age=31536000; includeSubdomain + + + + +Hello world! Hello world! + +``` + +Please observe above that all the lines that have data that changes from execution to execution (on right) have been removed. It makes it possible htrun to compare these logs. htrun matches lines from the compare log (on left) one by one. It keeps on looking for a line until it matches. Once matched it moves on to match the next line. If it finds all lines from the compare log in the target serial output stream. Then it halts and passes the examples. + +Another example with regular examples is shown below: + +``` + + SHA-256 :\s*\d+ Kb/s,\s*\d+ cycles/byte | SHA-256 : 1922 Kb/s, 61 cycl + + SHA-512 :\s*\d+ Kb/s,\s*\d+ cycles/byte | SHA-512 : 614 Kb/s, 191 cycl + + AES-CBC-128 :\s*\d+ Kb/s,\s*\d+ cycles/byte | AES-CBC-128 : 1401 Kb/s, 83 cycl + + AES-CBC-192 :\s*\d+ Kb/s,\s*\d+ cycles/byte | AES-CBC-192 : 1231 Kb/s, 95 cycl + + AES-CBC-256 :\s*\d+ Kb/s,\s*\d+ cycles/byte | AES-CBC-256 : 1097 Kb/s, 106 cycl + + AES-GCM-128 :\s*\d+ Kb/s,\s*\d+ cycles/byte | AES-GCM-128 : 429 Kb/s, 273 cycl + + AES-GCM-192 :\s*\d+ Kb/s,\s*\d+ cycles/byte | AES-GCM-192 : 412 Kb/s, 285 cycl + + AES-GCM-256 :\s*\d+ Kb/s,\s*\d+ cycles/byte | AES-GCM-256 : 395 Kb/s, 297 cycl + + AES-CCM-128 :\s*\d+ Kb/s,\s*\d+ cycles/byte | AES-CCM-128 : 604 Kb/s, 194 cycl + + AES-CCM-192 :\s*\d+ Kb/s,\s*\d+ cycles/byte | AES-CCM-192 : 539 Kb/s, 217 cycl + + AES-CCM-256 :\s*\d+ Kb/s,\s*\d+ cycles/byte | AES-CCM-256 : 487 Kb/s, 241 cycl + + CTR_DRBG \(NOPR\) :\s*\d+ Kb/s,\s*\d+ cycles/byte | CTR_DRBG (NOPR) : 1145 Kb/s, 102 cycl + + CTR_DRBG \(PR\) :\s*\d+ Kb/s,\s*\d+ cycles/byte | CTR_DRBG (PR) : 821 Kb/s, 142 cycl + + HMAC_DRBG SHA-256 \(NOPR\) :\s*\d+ Kb/s,\s*\d+ cycles/byte | HMAC_DRBG SHA-256 (NOPR) : 219 Kb/s, 537 cycl + + HMAC_DRBG SHA-256 \(PR\) :\s*\d+ Kb/s,\s*\d+ cycles/byte | HMAC_DRBG SHA-256 (PR) : 193 Kb/s, 612 cycl + + RSA-2048 :\s*\d+ ms/ public | RSA-2048 : 30 ms/ public + + RSA-2048 :\s*\d+ ms/private | RSA-2048 : 1054 ms/private + + RSA-4096 :\s*\d+ ms/ public | RSA-4096 : 101 ms/ public + + RSA-4096 :\s*\d+ ms/private | RSA-4096 : 5790 ms/private + + ECDHE-secp384r1 :\s*\d+ ms/handshake | ECDHE-secp384r1 : 1023 ms/handshake + + ECDHE-secp256r1 :\s*\d+ ms/handshake | ECDHE-secp256r1 : 678 ms/handshake + + ECDHE-Curve25519 :\s*\d+ ms/handshake | ECDHE-Curve25519 : 580 ms/handshake + + ECDH-secp384r1 :\s*\d+ ms/handshake | ECDH-secp384r1 : 503 ms/handshake + + ECDH-secp256r1 :\s*\d+ ms/handshake | ECDH-secp256r1 : 336 ms/handshake + + ECDH-Curve25519 :\s*\d+ ms/handshake | ECDH-Curve25519 : 300 ms/handshake + +``` + +To capture a log use following option: + +``` +mbedhtrun -d D: -p COM46 -m K64F -f .\BUILD\K64F\GCC_ARM\benchmark.bin --serial-output-file compare.log +``` + +Option ```--serial-output-file``` takes file name as argument and writes the target serial output to the file. Edit the file to remove lines that will change in successive executions. Put regular expressions if needed at places like benchmark numbers in above log. With these edits you are left with a template good for comparison. + +Use following command to test the example and the comparison log: + +``` +mbedhtrun -d D: -p COM46 -m K64F -f .\BUILD\K64F\GCC_ARM\benchmark.bin --compare-log compare.log +``` + +In case an application requires more time to process data and generate results, you can use the option ```--polling-timeout``` to override the default timeout setting. + +A tested comparison log can be checked into GitHub with the examples and can be used in the CI for example verification. diff --git a/tools/python/mbed_host_tests/__init__.py b/tools/python/mbed_host_tests/__init__.py new file mode 100644 index 0000000000..a118cadc61 --- /dev/null +++ b/tools/python/mbed_host_tests/__init__.py @@ -0,0 +1,36 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + + +"""! @package mbed-host-tests + +Flash, reset and perform host supervised tests on mbed platforms. +Write your own programs (import this package) or use 'mbedhtrun' command line tool instead. + +""" + +from mbed_os_tools.test import ( + host_tests_plugins, + HostRegistry, + BaseHostTest, + event_callback, + DEFAULT_BAUD_RATE, + get_plugin_caps, + init_host_test_cli_params, +) diff --git a/tools/python/mbed_host_tests/host_tests/__init__.py b/tools/python/mbed_host_tests/host_tests/__init__.py new file mode 100644 index 0000000000..ebfb2d04d6 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests/__init__.py @@ -0,0 +1,19 @@ +""" +mbed SDK +Copyright (c) 2011-2013 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. +""" + +# base host test class +from .base_host_test import BaseHostTest, event_callback diff --git a/tools/python/mbed_host_tests/host_tests/base_host_test.py b/tools/python/mbed_host_tests/host_tests/base_host_test.py new file mode 100644 index 0000000000..c2eef217cc --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests/base_host_test.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-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. +""" + +from mbed_os_tools.test.host_tests.base_host_test import ( + BaseHostTestAbstract, + event_callback, + HostTestCallbackBase, + BaseHostTest, +) diff --git a/tools/python/mbed_host_tests/host_tests/default_auto.py b/tools/python/mbed_host_tests/host_tests/default_auto.py new file mode 100644 index 0000000000..82d0f3586d --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests/default_auto.py @@ -0,0 +1,18 @@ +""" +mbed SDK +Copyright (c) 2011-2013 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. +""" + +from mbed_os_tools.test.host_tests.default_auto import DefaultAuto diff --git a/tools/python/mbed_host_tests/host_tests/detect_auto.py b/tools/python/mbed_host_tests/host_tests/detect_auto.py new file mode 100644 index 0000000000..719b5913f2 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests/detect_auto.py @@ -0,0 +1,18 @@ +""" +mbed SDK +Copyright (c) 2011-2013 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. +""" + +from mbed_os_tools.test.host_tests.detect_auto import DetectPlatformTest diff --git a/tools/python/mbed_host_tests/host_tests/dev_null_auto.py b/tools/python/mbed_host_tests/host_tests/dev_null_auto.py new file mode 100644 index 0000000000..60b9118a75 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests/dev_null_auto.py @@ -0,0 +1,18 @@ +""" +mbed SDK +Copyright (c) 2011-2013 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. +""" + +from mbed_os_tools.test.host_tests.dev_null_auto import DevNullTest diff --git a/tools/python/mbed_host_tests/host_tests/echo.py b/tools/python/mbed_host_tests/host_tests/echo.py new file mode 100644 index 0000000000..8522b2c310 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests/echo.py @@ -0,0 +1,18 @@ +""" +mbed SDK +Copyright (c) 2011-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. +""" + +from mbed_os_tools.test.host_tests.echo import EchoTest diff --git a/tools/python/mbed_host_tests/host_tests/hello_auto.py b/tools/python/mbed_host_tests/host_tests/hello_auto.py new file mode 100644 index 0000000000..9a112042ce --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests/hello_auto.py @@ -0,0 +1,18 @@ +""" +mbed SDK +Copyright (c) 2011-2013 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. +""" + +from mbed_os_tools.test.host_tests.hello_auto import HelloTest diff --git a/tools/python/mbed_host_tests/host_tests/rtc_auto.py b/tools/python/mbed_host_tests/host_tests/rtc_auto.py new file mode 100644 index 0000000000..a95c2a5d1a --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests/rtc_auto.py @@ -0,0 +1,18 @@ +""" +mbed SDK +Copyright (c) 2011-2013 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. +""" + +from mbed_os_tools.test.host_tests.rtc_auto import RTCTest diff --git a/tools/python/mbed_host_tests/host_tests/wait_us_auto.py b/tools/python/mbed_host_tests/host_tests/wait_us_auto.py new file mode 100644 index 0000000000..999f3d3764 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests/wait_us_auto.py @@ -0,0 +1,18 @@ +""" +mbed SDK +Copyright (c) 2011-2013 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. +""" + +from mbed_os_tools.test.host_tests.wait_us_auto import WaitusTest diff --git a/tools/python/mbed_host_tests/host_tests_conn_proxy/__init__.py b/tools/python/mbed_host_tests/host_tests_conn_proxy/__init__.py new file mode 100644 index 0000000000..736980ca8a --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_conn_proxy/__init__.py @@ -0,0 +1,20 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from .conn_proxy import conn_process diff --git a/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive.py b/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive.py new file mode 100644 index 0000000000..e588520286 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive.py @@ -0,0 +1,21 @@ +""" +mbed SDK +Copyright (c) 2011-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. +""" + +from mbed_os_tools.test.host_tests_conn_proxy.conn_primitive import ( + ConnectorPrimitiveException, + ConnectorPrimitive, +) diff --git a/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive_fastmodel.py b/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive_fastmodel.py new file mode 100644 index 0000000000..fb23808c84 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive_fastmodel.py @@ -0,0 +1,20 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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. +""" + +from mbed_os_tools.test.host_tests_conn_proxy.conn_primitive_fastmodel import ( + FastmodelConnectorPrimitive, +) diff --git a/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive_remote.py b/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive_remote.py new file mode 100644 index 0000000000..729bd60296 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive_remote.py @@ -0,0 +1,20 @@ +""" +mbed SDK +Copyright (c) 2011-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. +""" + +from mbed_os_tools.test.host_tests_conn_proxy.conn_primitive_remote import ( + RemoteConnectorPrimitive, +) diff --git a/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive_serial.py b/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive_serial.py new file mode 100644 index 0000000000..a11cc00e08 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_primitive_serial.py @@ -0,0 +1,21 @@ +""" +mbed SDK +Copyright (c) 2011-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. +""" + + +from mbed_os_tools.test.host_tests_conn_proxy.conn_primitive_serial import ( + SerialConnectorPrimitive, +) diff --git a/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_proxy.py b/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_proxy.py new file mode 100644 index 0000000000..e2ea5c3326 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_conn_proxy/conn_proxy.py @@ -0,0 +1,22 @@ +""" +mbed SDK +Copyright (c) 2011-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. +""" + +from mbed_os_tools.test.host_tests_conn_proxy.conn_proxy import ( + KiViBufferWalker, + conn_primitive_factory, + conn_process, +) diff --git a/tools/python/mbed_host_tests/host_tests_logger/__init__.py b/tools/python/mbed_host_tests/host_tests_logger/__init__.py new file mode 100644 index 0000000000..45833b6d54 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_logger/__init__.py @@ -0,0 +1,20 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from .ht_logger import HtrunLogger diff --git a/tools/python/mbed_host_tests/host_tests_logger/ht_logger.py b/tools/python/mbed_host_tests/host_tests_logger/ht_logger.py new file mode 100644 index 0000000000..75d86d0000 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_logger/ht_logger.py @@ -0,0 +1,18 @@ +""" +mbed SDK +Copyright (c) 2011-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. +""" + +from mbed_os_tools.test.host_tests_logger.ht_logger import HtrunLogger diff --git a/tools/python/mbed_host_tests/host_tests_plugins/__init__.py b/tools/python/mbed_host_tests/host_tests_plugins/__init__.py new file mode 100644 index 0000000000..9a567bb826 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/__init__.py @@ -0,0 +1,33 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +"""! @package mbed-host-test-plugins + +This package contains plugins used by host test to reset, flash devices etc. +This package can be extended with new packages to add more generic functionality + +""" + +from mbed_os_tools.test.host_tests_plugins import ( + HOST_TEST_PLUGIN_REGISTRY, + call_plugin, + get_plugin_caps, + get_plugin_info, + print_plugin_info, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/host_test_plugins.py b/tools/python/mbed_host_tests/host_tests_plugins/host_test_plugins.py new file mode 100644 index 0000000000..d364dd681e --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/host_test_plugins.py @@ -0,0 +1,22 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.host_test_plugins import ( + HostTestPluginBase, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/host_test_registry.py b/tools/python/mbed_host_tests/host_tests_plugins/host_test_registry.py new file mode 100644 index 0000000000..454cd151c2 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/host_test_registry.py @@ -0,0 +1,22 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.host_test_registry import ( + HostTestRegistry, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_copy_jn51xx.py b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_jn51xx.py new file mode 100644 index 0000000000..729250da63 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_jn51xx.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_copy_jn51xx import ( + HostTestPluginCopyMethod_JN51xx, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_copy_mbed.py b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_mbed.py new file mode 100644 index 0000000000..103092532f --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_mbed.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_copy_mbed import ( + HostTestPluginCopyMethod_Mbed, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_copy_mps2.py b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_mps2.py new file mode 100644 index 0000000000..436644f521 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_mps2.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_copy_mps2 import ( + HostTestPluginCopyMethod_MPS2, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_copy_pyocd.py b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_pyocd.py new file mode 100644 index 0000000000..498ee371f8 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_pyocd.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2016-2016,2018 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. + +Author: Russ Butler +""" + +from mbed_os_tools.test.host_tests_plugins.module_copy_pyocd import ( + HostTestPluginCopyMethod_pyOCD, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_copy_shell.py b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_shell.py new file mode 100644 index 0000000000..02644e2a83 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_shell.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_copy_shell import ( + HostTestPluginCopyMethod_Shell, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_copy_silabs.py b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_silabs.py new file mode 100644 index 0000000000..7917926cda --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_silabs.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_copy_silabs import ( + HostTestPluginCopyMethod_Silabs, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_copy_stlink.py b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_stlink.py new file mode 100644 index 0000000000..696594f344 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_stlink.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_copy_stlink import ( + HostTestPluginCopyMethod_Stlink, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_copy_ublox.py b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_ublox.py new file mode 100644 index 0000000000..d9d7e94e1b --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_copy_ublox.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_copy_ublox import ( + HostTestPluginCopyMethod_ublox, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_power_cycle_mbed.py b/tools/python/mbed_host_tests/host_tests_plugins/module_power_cycle_mbed.py new file mode 100644 index 0000000000..f861dcfb58 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_power_cycle_mbed.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_power_cycle_mbed import ( + HostTestPluginPowerCycleResetMethod, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_reset_jn51xx.py b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_jn51xx.py new file mode 100644 index 0000000000..a296c961a9 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_jn51xx.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_reset_jn51xx import ( + HostTestPluginResetMethod_JN51xx, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_reset_mbed.py b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_mbed.py new file mode 100644 index 0000000000..13024110fe --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_mbed.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_reset_mbed import ( + HostTestPluginResetMethod_Mbed, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_reset_mps2.py b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_mps2.py new file mode 100644 index 0000000000..1708c75757 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_mps2.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_reset_mps2 import ( + HostTestPluginResetMethod_MPS2, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_reset_pyocd.py b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_pyocd.py new file mode 100644 index 0000000000..189140d5d5 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_pyocd.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2016-2016,2018 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. + +Author: Russ Butler +""" + +from mbed_os_tools.test.host_tests_plugins.module_reset_pyocd import ( + HostTestPluginResetMethod_pyOCD, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_reset_silabs.py b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_silabs.py new file mode 100644 index 0000000000..e33c7a6bad --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_silabs.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_reset_silabs import ( + HostTestPluginResetMethod_SiLabs, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_reset_stlink.py b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_stlink.py new file mode 100644 index 0000000000..a87ed479f1 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_stlink.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_reset_stlink import ( + HostTestPluginResetMethod_Stlink, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_plugins/module_reset_ublox.py b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_ublox.py new file mode 100644 index 0000000000..2d36c251a1 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_plugins/module_reset_ublox.py @@ -0,0 +1,23 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_plugins.module_reset_ublox import ( + HostTestPluginResetMethod_ublox, + load_plugin, +) diff --git a/tools/python/mbed_host_tests/host_tests_registry/__init__.py b/tools/python/mbed_host_tests/host_tests_registry/__init__.py new file mode 100644 index 0000000000..4500af0a45 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_registry/__init__.py @@ -0,0 +1,26 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +"""! @package host_registry + +Host registry is used to store all host tests (by id) which can be called from test framework + +""" + +from .host_registry import HostRegistry diff --git a/tools/python/mbed_host_tests/host_tests_registry/host_registry.py b/tools/python/mbed_host_tests/host_tests_registry/host_registry.py new file mode 100644 index 0000000000..418c4889e5 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_registry/host_registry.py @@ -0,0 +1,20 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_registry.host_registry import HostRegistry diff --git a/tools/python/mbed_host_tests/host_tests_runner/__init__.py b/tools/python/mbed_host_tests/host_tests_runner/__init__.py new file mode 100644 index 0000000000..8fa0c750a4 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_runner/__init__.py @@ -0,0 +1,25 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. + +Author: Przemyslaw Wirkus +""" + +"""! @package mbed-host-test-runner + +This package contains basic host test implementation with algorithms to flash and reset device. +Functionality can be overridden by set of plugins which can provide specialised flashing and reset implementations. + +""" diff --git a/tools/python/mbed_host_tests/host_tests_runner/host_test.py b/tools/python/mbed_host_tests/host_tests_runner/host_test.py new file mode 100644 index 0000000000..ca9dcf7557 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_runner/host_test.py @@ -0,0 +1,24 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_runner.host_test import ( + HostTestResults, + Test, + DefaultTestSelectorBase, +) diff --git a/tools/python/mbed_host_tests/host_tests_runner/host_test_default.py b/tools/python/mbed_host_tests/host_tests_runner/host_test_default.py new file mode 100644 index 0000000000..9fe28f511b --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_runner/host_test_default.py @@ -0,0 +1,22 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_runner.host_test_default import ( + DefaultTestSelector, +) diff --git a/tools/python/mbed_host_tests/host_tests_runner/mbed_base.py b/tools/python/mbed_host_tests/host_tests_runner/mbed_base.py new file mode 100644 index 0000000000..9c76720080 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_runner/mbed_base.py @@ -0,0 +1,20 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_runner.mbed_base import Mbed diff --git a/tools/python/mbed_host_tests/host_tests_toolbox/__init__.py b/tools/python/mbed_host_tests/host_tests_toolbox/__init__.py new file mode 100644 index 0000000000..c4b8b8cc56 --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_toolbox/__init__.py @@ -0,0 +1,22 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from .host_functional import reset_dev +from .host_functional import flash_dev +from .host_functional import handle_send_break_cmd diff --git a/tools/python/mbed_host_tests/host_tests_toolbox/host_functional.py b/tools/python/mbed_host_tests/host_tests_toolbox/host_functional.py new file mode 100644 index 0000000000..ab700444ad --- /dev/null +++ b/tools/python/mbed_host_tests/host_tests_toolbox/host_functional.py @@ -0,0 +1,24 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from mbed_os_tools.test.host_tests_toolbox.host_functional import ( + flash_dev, + reset_dev, + handle_send_break_cmd, +) diff --git a/tools/python/mbed_host_tests/mbedflsh.py b/tools/python/mbed_host_tests/mbedflsh.py new file mode 100644 index 0000000000..a9c8792c32 --- /dev/null +++ b/tools/python/mbed_host_tests/mbedflsh.py @@ -0,0 +1,99 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +import sys +import optparse + +### Flashing/Reset API provided by mbed--host-tests (mbedhtrun) +from mbed_os_tools.test import host_tests_plugins + + +def cmd_parser_setup(): + """! Creates simple command line parser + """ + parser = optparse.OptionParser() + + parser.add_option('-f', '--file', + dest='filename', + help='File to flash onto mbed device') + + parser.add_option("-d", "--disk", + dest="disk", + help="Target disk (mount point) path. Example: F:, /mnt/MBED", + metavar="DISK_PATH") + + copy_methods_str = "Plugin support: " + ', '.join(host_tests_plugins.get_plugin_caps('CopyMethod')) + + parser.add_option("-c", "--copy", + dest="copy_method", + default='shell', + help="Copy (flash the target) method selector. " + copy_methods_str, + metavar="COPY_METHOD") + + parser.add_option('', '--plugins', + dest='list_plugins', + default=False, + action="store_true", + help='Prints registered plugins and exits') + + parser.add_option('', '--version', + dest='version', + default=False, + action="store_true", + help='Prints package version and exits') + + parser.description = """Flash mbed devices from command line.""" \ + """This module is using build in to mbed-host-tests plugins used for flashing mbed devices""" + parser.epilog = """Example: mbedflsh -d E: -f /path/to/file.bin""" + + (opts, args) = parser.parse_args() + return (opts, args) + + +def main(): + """! Function wrapping flashing (copy) plugin + @details USers can use mbedflsh command to flash mbeds from command line + """ + errorlevel_flag = 0 + (opts, args) = cmd_parser_setup() + + if opts.version: + import pkg_resources # part of setuptools + version = pkg_resources.require("mbed-host-tests")[0].version + print(version) + sys.exit(0) + elif opts.list_plugins: # --plugins option + host_tests_plugins.print_plugin_info() + sys.exit(0) + else: + pass + + if opts.filename: + print("mbedflsh: opening file %s..."% opts.filename) + result = host_tests_plugins.call_plugin('CopyMethod', + opts.copy_method, + image_path=opts.filename, + destination_disk=opts.disk) + errorlevel_flag = result == True + + return errorlevel_flag + + +if __name__ == '__main__': + exit(main()) diff --git a/tools/python/mbed_host_tests/mbedhtrun.py b/tools/python/mbed_host_tests/mbedhtrun.py new file mode 100644 index 0000000000..dd1f2e3856 --- /dev/null +++ b/tools/python/mbed_host_tests/mbedhtrun.py @@ -0,0 +1,66 @@ +""" +mbed SDK +Copyright (c) 2011-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. + +Author: Przemyslaw Wirkus +""" + +from __future__ import print_function +from multiprocessing import freeze_support +from mbed_os_tools.test import init_host_test_cli_params +from mbed_os_tools.test.host_tests_runner.host_test_default import DefaultTestSelector +from mbed_os_tools.test.host_tests_toolbox.host_functional import handle_send_break_cmd + + +def main(): + """! This function drives command line tool 'mbedhtrun' which is using DefaultTestSelector + @details 1. Create DefaultTestSelector object and pass command line parameters + 2. Call default test execution function run() to start test instrumentation + """ + freeze_support() + result = 0 + cli_params = init_host_test_cli_params() + + if cli_params.version: # --version + import pkg_resources # part of setuptools + version = pkg_resources.require("mbed-host-tests")[0].version + print(version) + elif cli_params.send_break_cmd: # -b with -p PORT (and optional -r RESET_TYPE) + handle_send_break_cmd( + port=cli_params.port, + disk=cli_params.disk, + reset_type=cli_params.forced_reset_type, + baudrate=cli_params.baud_rate, + verbose=cli_params.verbose + ) + else: + test_selector = DefaultTestSelector(cli_params) + try: + result = test_selector.execute() + # Ensure we don't return a negative value + if result < 0 or result > 255: + result = 1 + except (KeyboardInterrupt, SystemExit): + test_selector.finish() + result = 1 + raise + else: + test_selector.finish() + + return result + + +if __name__ == '__main__': + exit(main()) \ No newline at end of file diff --git a/tools/python/mbed_lstools/README.md b/tools/python/mbed_lstools/README.md new file mode 100644 index 0000000000..f2c59ae0d0 --- /dev/null +++ b/tools/python/mbed_lstools/README.md @@ -0,0 +1,454 @@ +# Development moved + +The development of Mbed LS has been moved into the [mbed-os-tools](../../src/mbed_os_tools) package. You can continue to use this module for legacy reasons, however all further development should be continued in the new package. + +------------- + +[![PyPI version](https://badge.fury.io/py/mbed-ls.svg)](https://badge.fury.io/py/mbed-ls) + +# Mbed LS + +Mbed LS is a Python (2 and 3) module that detects and lists Mbed Enabled devices connected to the host computer. The Mbed OS team publishes Mbed LS on PyPI. It works on all major operating systems (Windows, Linux and Mac OS X). + +It provides the following information for all connected boards in a console (terminal) output: + +- Mbed OS platform name. +- Mount point (MSD or disk). +- Serial port. + +# Installation + +## Installation from PyPI (Python Package Index) + +To install Mbed LS from [PyPI](https://pypi.python.org/pypi/mbed-ls), run the following command: + +```bash +$ pip install mbed-ls --upgrade +``` + +## Installation from Python sources + +**Prerequisites:** You need to have [Python 2.7.x](https://www.python.org/download/releases/2.7/) or [Python 3.6.x](https://www.python.org/downloads/release/python-362/) installed on your system. + +**Note:** If your OS is Windows, please follow the installation instructions [for the serial port driver](https://os.mbed.com/docs/latest/tutorials/windows-serial-driver.html). + +Install Mbed LS from sources with the following commands: + +```bash +$ git clone https://github.com/ARMmbed/mbed-os-tools.git +$ cd mbed-os-tools/packages/mbed-ls +$ python setup.py install +``` + +# Command-line + +The command-line tool is available with the command `mbedls`. + +**Note:** [Mbed CLI](https://github.com/armmbed/mbed-cli) has a similarly-named command `mbed ls`; however, the commands are different. Be sure to omit the space when using the Mbed LS command-line tool. + +```bash +$ mbedls ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +| platform_name | platform_name_unique | mount_point | serial_port | target_id | daplink_version | ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +| K64F | K64F[0] | D: | COM18 | 0240000032044e4500257009997b00386781000097969900 | 0244 | ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +``` + +## Result formats + +The Mbed LS command-line accepts a few arguments to change the format of the results. The default format is a table. You may pass `--simple` to simplify this table format, and `--json` to print the table as a json list of the rows. + +### Simple (no table formatting) + +``` +$ mbedls --simple + K64F K64F[0] D: COM18 0240000032044e4500257009997b00386781000097969900 0244 +``` + +### JSON + +```bash +$ mbedls --json +[ + { + "daplink_auto_reset": "0", + "daplink_automation_allowed": "1", + "daplink_bootloader_crc": "0xa65218eb", + "daplink_bootloader_version": "0242", + "daplink_daplink_mode": "Interface", + "daplink_git_sha": "67f8727a030bcc585e982d899fb6382db56d673b", + "daplink_hic_id": "97969900", + "daplink_interface_crc": "0xe4422294", + "daplink_interface_version": "0244", + "daplink_local_mods": "0", + "daplink_overflow_detection": "1", + "daplink_remount_count": "0", + "daplink_unique_id": "0240000032044e4500257009997b00386781000097969900", + "daplink_usb_interfaces": "MSD, CDC, HID", + "daplink_version": "0244", + "mount_point": "D:", + "platform_name": "K64F", + "platform_name_unique": "K64F[0]", + "product_id": "0204", + "serial_port": "COM18", + "target_id": "0240000032044e4500257009997b00386781000097969900", + "target_id_mbed_htm": "0240000032044e4500257009997b00386781000097969900", + "target_id_usb_id": "0240000032044e4500257009997b00386781000097969900", + "vendor_id": "0d28" + } +] +``` + +## Mocking (renaming) platforms + +Override a platform's name using the `--mock` parameter: + +``` +$ mbedls --mock 0240:MY_NEW_PLATFORM +$ mbedls ++-----------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +| platform_name | platform_name_unique | mount_point | serial_port | target_id | daplink_version | ++-----------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +| MY_NEW_PLATFORM | MY_NEW_PLATFORM[0] | D: | COM18 | 0240000032044e4500257009997b00386781000097969900 | 0244 | ++-----------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +``` + +The `--mock` parameter accepts a platform ID and a platform name, separated by the `:` character. The platform ID is the first 4 characters of the `target_id`. The platform name is the name you are temporarily assigning to this platform. + +To remove a mocked platform, use the `--mock` parameter again. Continuing from the previous example, use `-` as the value: + +``` +$ mbedls --mock -0240 +$ mbedls ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +| platform_name | platform_name_unique | mount_point | serial_port | target_id | daplink_version | ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +| K64F | K64F[0] | D: | COM18 | 0240000032044e4500257009997b00386781000097969900 | 0244 | ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +``` + +You can also remove all mocked platforms by supplying `*` as the `target_id`: + +``` +$ mbedls --mock="-*" +``` + +**NOTE:** Due to a quirk in the parameter formatting, the command-line can interpret `-*` as another parameter instead of a value. It is necessary to use the complete `--mock="-*"` syntax, so the command-line interprets each part of the command correctly. + +## Retargeting platforms + +It is possible to change the returned results for certain platforms depending on the current directory. This is especially useful when developing new platforms. + +The command-line tool and Python API check the current directory for a file named `mbedls.json`. When it is present, it overrides the returned values. The format of the `mbedls.json` file is: + +```json +{ + "": { + "": "" + } +} +``` + +For example, to change the `serial_port` of the K64F with a `target_id` of `0240000032044e4500257009997b00386781000097969900`, the `mbedls.json` file contains the following: + +```json +{ + "0240000032044e4500257009997b00386781000097969900": { + "serial_port": "COM99" + } +} +``` + +This results in the following output from the command-line tool: + +```bash +$ mbedls ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +| platform_name | platform_name_unique | mount_point | serial_port | target_id | daplink_version | ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +| K64F | K64F[0] | D: | COM99 | 0240000032044e4500257009997b00386781000097969900 | 0244 | ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +``` + +Note how the `serial_port` value changed from `COM18` to `COM99`. Deleting the `mbedls.json` or using the `--skip-retarget` parameter removes these changes. + +# Python API + +The Python API is available through the `mbed_lstools` module. + +## `mbed_lstools.create(...)` + +```python +>>> import mbed_lstools +>>> mbeds = mbed_lstools.create() +>>> mbeds + +``` + +This returns an instance that provides access to the rest of the API. + +### Arguments + +#### `skip_retarget` + +**Default:** `False` + +When set to `True`, this skips the retargetting step, and the results are unmodified. This enables the same behavior as the `--skip-retarget` command-line flag. + +#### `list_unmounted` + +**Default:** `False` + +When set to `True`, this includes unmounted platforms in the results. This enables the same behavior as the `-u` command-line flag. + +## `mbeds.list_mbeds(...)` + +```python +>>> import mbed_lstools +>>> mbeds = mbed_lstools.create() +>>> mbeds.list_mbeds(fs_interaction=FSInteraction.BeforeFilter, + filter_function=None, + unique_names=False, + read_details_txt=False) +[{'target_id_mbed_htm': u'0240000032044e4500257009997b00386781000097969900', 'mount_point': 'D:', 'target_id': u'0240000032044e4500257009997b00386781000097969900', 'serial_port': u'COM18', 'target_id_usb_id': u'0240000032044e4500257009997b00386781000097969900', 'platform_name': u'K64F'}] +``` + +### Arguments + +#### `filter_function` + +**Default:** `None` + +This function allows you to filter results based on platform data. This can hasten the execution of the `list_mbeds` function. + +As a normal function definition: + +```python +def filter_func(mbed): + return m['platform_name'] == 'K64F' + +mbeds.list_mbeds(filter_function=filter_func) +``` + +As a lambda function: + +```python +platforms = mbeds.list_mbeds(filter_function=lambda m: m['platform_name'] == 'K64F') +``` + +#### `fs_interaction` + +**Default:** `FSInteraction.BeforeFilter` + +This argument controls the accuracy and speed of this function. There are three choices (in ascending order of accuracy and decreasing order of speed): + +- `FSInteraction.NEVER` - This is the fastest option but also potentially the least accurate. It never touches the file system of the devices. It uses only the information available through the USB descriptors. This is appropriate for use in a highly controlled environment (such as an automated Continuous Integration setup). **This has the potential to provide incorrect names and data. It may also lead to devices not being detected at all.** +- `FSInterfaction.AfterFilter` - This accesses the file system but only after application of the `filter_function`. This can lead to speed increases but at the risk of filtering on inaccurate information. +- `FSInteraction.BeforeFilter` - This accesses the file system before doing any filtering. It is the most accurate option and is recommended for most uses. This is the default behavior of the command-line tool and the API. + +#### `unique_names` + +**Default:** `False`. + +Mbed LS assigns a unique name to each platform when this is set to `True`. The unique name takes the form of `K64F[0]`, where the number between the brackets is an incrementing value. This name is accessible through the dictionary member `platform_unique_name` in the returned platform data. + +#### `read_details_txt` + +**Default:** `False` + +Mbed LS reads more data from the file system on each device when this is set to `True`. It can provide useful management data but also takes more time to execute. + +## `mbeds.mock_manufacture_id(...)` + +```python +>>> import mbed_lstools +>>> mbeds = mbed_lstools.create() +>>> mbeds.mock_manufacture_id('0240', 'CUSTOM_PLATFORM', oper='+') +>>> mbeds.list_mbeds() +[{'target_id': u'0240000032044e4500257009997b00386781000097969900', ... 'platform_name': u'CUSTOM_PLATFORM'}] +>>> mbeds.mock_manufacture_id('0240', '', oper='-') +>>> mbeds.list_mbeds() +[{'target_id': u'0240000032044e4500257009997b00386781000097969900', ... 'platform_name': u'K64F'}] +``` + +### Arguments + +#### `mid` + +**Required** + +The first four characters of the `target_id` that you want to mock. + +#### `platform_name` + +**Required** + +Overrides the `platform_name` for any platform with a `target_id` that starts with `mid`. + +#### `oper` + +**Default:** `'+'` + +If set to `'+'`, the mocked platform is enabled. If `'-'`, the mocked platform is disabled. + + +## `mbeds.get_supported_platforms(...)` + +```python +>>> import mbed_lstools +>>> mbeds = mbed_lstools.create() +>>> mbeds.get_supported_platforms(device_type='daplink') +{'0240': 'K64F', '0311': 'K66F'} +``` + +### Arguments + +#### `device_type` + +**Default:** `'daplink'` + +Chooses which device type entries are retrieved from the platform database. + +## Logging + +Mbed LS uses the Python `logging` module for all of its logging needs. Mbed LS uses the logger `"mbedls"` as its root, and all other loggers start with `"mbedls."`. Configuring the Python root logger automatically redirects all of the Mbed LS logs to the configured endpoint. When using the Python API, configure logging, such as by calling `logging.basicConfig()`. + +# Testing + +The `/test` directory contains all tests. You can run the tests with the following command: + +``` +$ python setup.py test +``` + +## Code coverage + +The `coverage` Python package measures code coverage. You can install it with following command: + +``` +$ pip install coverage --upgrade +``` + +To run the tests while measuring code coverage, use the following command: + +``` +$ coverage run setup.py test +``` + +You can then generate a report: + +``` +$ coverage report +Name Stmts Miss Cover +------------------------------------------------------- +mbed_lstools\__init__.py 2 0 100% +mbed_lstools\darwin.py 85 7 92% +mbed_lstools\linux.py 45 3 93% +mbed_lstools\lstools_base.py 299 124 59% +mbed_lstools\main.py 134 44 67% +mbed_lstools\platform_database.py 114 4 96% +mbed_lstools\windows.py 98 21 79% +------------------------------------------------------- +TOTAL 777 203 74% +``` + +# OS-specific behavior + +## Windows + +The Mbed serial port works by default on Mac and Linux, but Windows needs a driver. Check [here](https://os.mbed.com/docs/latest/tutorials/windows-serial-driver.html) for more details. + +## Linux + +Mbed LS requires you to mount a platform before it shows up in the results. Many Linux systems do not automatically mount USB devices. We recommend you use an automounter to manage this for you. + +There are many automounters available, and it is ultimately up to you to determine which is the best one for your use case. However, the `usbmount` package on Ubuntu makes it easy to start. If you need more control over your automounter, you can build and run an open source project called [ldm](https://github.com/LemonBoy/ldm). + +# Mbed Enabled technical requirements overview + +This tool relies on board interfaces conforming to certain standards, so it can detect platforms properly. The [Mbed Enabled](https://www.mbed.com/en/about-mbed/mbed-enabled/) program sets these standards. Please see the [Technical Requirements](https://www.mbed.com/en/about-mbed/mbed-enabled/mbed-enabled-program-requirements/) for more information. + +## Device unique identifier + +Each device must have a unique identifier. This identifier has two parts: a **platform ID** and a **platform unique string**. + +The **platform ID** contains four ASCII characters containing only hexadecimal values (A-F and 0-9). This platform ID is the same for all platforms of the same type. For example, all `K64F` platforms have a platform ID of `0240`. `mbedls` uses this to identify the platform. + +The **platform unique string** can be any length of characters (a-z, A-Z and 0-9) that you can use to uniquely identify platforms of the same type on the same machine. For example, two FRDM-K64F platforms attached to the same machine could have the following attributes: + +``` +$ mbedls ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +| platform_name | platform_name_unique | mount_point | serial_port | target_id | daplink_version | ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +| K64F | K64F[0] | D: | COM18 | 0240000032044e4500257009997b00386781000097969900 | 0244 | +| K64F | K64F[1] | E: | COM19 | 0240000032044e4500257009997b00386781000097840023 | 0244 | ++---------------+----------------------+-------------+-------------+--------------------------------------------------+-----------------+ +``` + +Note how both platforms share the same platform ID (`0240`) but have a unique ending string. + +# Adding platform support + +If a platform meets the Mbed Enabled technical requirements (stated above), it can be added to Mbed LS. + +## Adding a new platform with a supported debugger + +Mbed LS currently supports the following types of debuggers: + +- [DAPLink](https://github.com/ARMmbed/DAPLink) + - As well as the related but legacy [CMSIS-DAP](https://github.com/mbedmicro/CMSIS-DAP) firmware +- ST-LINK +- J-Link + +### Adding support for DAPLink-compatible platforms (DAPLink, ST-LINK, and CMSIS-DAP) + +Add an entry to the `daplink` section of the [`DEFAULT_PLATFORM_DB`](../../src/mbed_os_tools/detect/platform_database.py). + +If your platform's name is `NEW_PLATFORM` and it has platform ID of `9999`, the new entry should be: + +``` +DEFAULT_PLATFORM_DB = { + u'daplink': { + ... + u'9999': u'NEW_PLATFORM', + ... + } +} +``` + +Please order the entries by the platform ID when adding new platforms. + +### Adding support for J-Link platforms + +J-Link detection works differently due to the information present on the platform's filesystem. All new entries should be added to the `jlink` section of the [`DEFAULT_PLATFORM_DB`](../../src/mbed_os_tools/detect/platform_database.py). + +The following is an example `jlink` platform entry: + +``` +DEFAULT_PLATFORM_DB = { + ... + u'jlink': { + u'X729475D28G': { + u'platform_name': u'NRF51_DK', + u'jlink_device_name': u'nRF51422_xxAC' + }, + ... + } +} +``` + +Instead of a platform ID, there is a target-unique string (`X729475D28G` in this case). This should correspond with the unique part of the link present in the `Board.html` or `User Guide.html`. This seems to vary among the platforms. In general, try following the links in each file. You want to use the url that links to a product page that references the platform. The J-Link logic in Mbed LS assumes that the url has the target-unique string on the end (after the last `/` character). In the above example, the expected url structure would be `http://www.nordicsemi.com/X729475D28G`. + +If your J-Link platform does not follow this convention, please raise an issue with the following information: + +- The name of the platform +- The file **names and contents** present on the platform's filesystem +- A link to the J-Link firmware binary if possible + +## Adding a new type of debugger + +The type of debugger present on the platform affects how it is detected. The USB Vendor ID is used to detect which type of debugger is present on the platform. + +If a new type of debugger is being introduced to Mbed LS with the platform, you will need to add the Vendor ID to the [identification map](../../src/mbed_os_tools/detect/lstools_base.py). You will also need to assign the correct "update from the filesystem" logic [here](../../src/mbed_os_tools/detect/lstools_base.py). If the debugger is compatible with the files presented by DAPLink, you may reuse that implementation when updating the device information from the filesystem. If it is not, you may need to write your own update logic. If you need guidance on this, please ask for it when you submit an issue or a pull request. diff --git a/tools/python/mbed_lstools/__init__.py b/tools/python/mbed_lstools/__init__.py new file mode 100644 index 0000000000..72c9a4fb5d --- /dev/null +++ b/tools/python/mbed_lstools/__init__.py @@ -0,0 +1,38 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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. +""" + +"""! @package mbed-ls + +mbed-ls package is a set of tools inherited from mbed-lmtools used to detect +mbed enabled devices on host (Windows, Linux and MacOS). + +mbed-lstools is a Python module that detects and lists mbed-enabled devices +connected to the host computer. It will be delivered as a redistributable +Python module (package) and command line tool. + +Currently supported operating system: +* Windows 7. +* Ubuntu. +* Mac OS X (Darwin). + +The stand-alone mbed-lstools Python package is still under development, +but it's already delivered as part of the mbed SDK's test suite and a command +line tool (see below). +""" + +from .main import mbedls_main +from .main import create diff --git a/tools/python/mbed_lstools/darwin.py b/tools/python/mbed_lstools/darwin.py new file mode 100644 index 0000000000..44448aa8fc --- /dev/null +++ b/tools/python/mbed_lstools/darwin.py @@ -0,0 +1,18 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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. +""" + +from mbed_os_tools.detect.darwin import MbedLsToolsDarwin diff --git a/tools/python/mbed_lstools/linux.py b/tools/python/mbed_lstools/linux.py new file mode 100644 index 0000000000..6e66650174 --- /dev/null +++ b/tools/python/mbed_lstools/linux.py @@ -0,0 +1,18 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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. +""" + +from mbed_os_tools.detect.linux import MbedLsToolsLinuxGeneric diff --git a/tools/python/mbed_lstools/lstools_base.py b/tools/python/mbed_lstools/lstools_base.py new file mode 100644 index 0000000000..04bd2e8917 --- /dev/null +++ b/tools/python/mbed_lstools/lstools_base.py @@ -0,0 +1,25 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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 functools +import json +import logging + +from mbed_os_tools.detect.lstools_base import ( + FSInteraction, + MbedLsToolsBase, +) diff --git a/tools/python/mbed_lstools/main.py b/tools/python/mbed_lstools/main.py new file mode 100644 index 0000000000..f25f98383b --- /dev/null +++ b/tools/python/mbed_lstools/main.py @@ -0,0 +1,202 @@ + +""" +mbed SDK +Copyright (c) 2011-2018 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 sys +import json +import argparse +from collections import defaultdict + +# Make sure that any global generic setup is run +from . import lstools_base +from mbed_os_tools.detect.main import ( + create, + mbed_os_support, + mbed_lstools_os_info, + mock_platform +) + +import logging +logger = logging.getLogger("mbedls.main") +logger.addHandler(logging.NullHandler()) +del logging + +def get_version(): + """! Get mbed-ls Python module version string """ + import mbed_os_tools + return mbed_os_tools.VERSION + +def print_version(mbeds, args): + print(get_version()) + +def print_mbeds(mbeds, args, simple): + devices = mbeds.list_mbeds(unique_names=True, read_details_txt=True) + if devices: + from prettytable import PrettyTable, HEADER + columns = ['platform_name', 'platform_name_unique', 'mount_point', + 'serial_port', 'target_id', 'daplink_version'] + columns_header = ['platform_name', 'platform_name_unique', 'mount_point', + 'serial_port', 'target_id', 'interface_version'] + pt = PrettyTable(columns_header, junction_char="|", hrules=HEADER) + pt.align = 'l' + for d in devices: + pt.add_row([d.get(col, None) or 'unknown' for col in columns]) + print(pt.get_string(border=not simple, header=not simple, + padding_width=1, sortby='platform_name_unique')) + +def print_table(mbeds, args): + return print_mbeds(mbeds, args, False) + +def print_simple(mbeds, args): + return print_mbeds(mbeds, args, True) + +def list_platforms(mbeds, args): + print(mbeds.list_manufacture_ids()) + +def mbeds_as_json(mbeds, args): + print(json.dumps(mbeds.list_mbeds(unique_names=True, + read_details_txt=True), + indent=4, sort_keys=True)) + +def json_by_target_id(mbeds, args): + print(json.dumps({m['target_id']: m for m + in mbeds.list_mbeds(unique_names=True, + read_details_txt=True)}, + indent=4, sort_keys=True)) + +def json_platforms(mbeds, args): + platforms = set() + for d in mbeds.list_mbeds(): + platforms |= set([d['platform_name']]) + print(json.dumps(list(platforms), indent=4, sort_keys=True)) + +def json_platforms_ext(mbeds, args): + platforms = defaultdict(lambda: 0) + for d in mbeds.list_mbeds(): + platforms[d['platform_name']] += 1 + print(json.dumps(platforms, indent=4, sort_keys=True)) + +def parse_cli(to_parse): + """! Parse the command line + + @return Retrun a namespace that contains: + * command - python function to run + * skip_retarget - bool indicting to skip retargeting + * list_unmounted - list boards that are not mounted + * debug - turn on debug logging + """ + parser = argparse.ArgumentParser() + parser.set_defaults(command=print_table) + + commands = parser.add_argument_group('sub commands')\ + .add_mutually_exclusive_group() + commands.add_argument( + '-s', '--simple', dest='command', action='store_const', + const=print_simple, + help='list attached targets without column headers and borders') + commands.add_argument( + '-j', '--json', dest='command', action='store_const', + const=mbeds_as_json, + help='list attached targets with detailed information in JSON format') + commands.add_argument( + '-J', '--json-by-target-id', dest='command', action='store_const', + const=json_by_target_id, + help='map attached targets from their target ID to their detailed ' + 'information in JSON format') + commands.add_argument( + '-p', '--json-platforms', dest='command', action='store_const', + const=json_platforms, + help='list attached platform names in JSON format.') + commands.add_argument( + '-P', '--json-platforms-ext', dest='command', action='store_const', + const=json_platforms_ext, + help='map attached platform names to the number of attached boards in ' + 'JSON format') + commands.add_argument( + '-l', '--list', dest='command', action='store_const', + const=list_platforms, + help='list all target IDs and their corresponding platform names ' + 'understood by mbed-ls') + commands.add_argument( + '--version', dest='command', action='store_const', const=print_version, + help='print package version and exit') + commands.add_argument( + '-m', '--mock', metavar='ID:NAME', + help='substitute or create a target ID to platform name mapping used' + 'when invoking mbedls in the current directory') + + parser.add_argument( + '--skip-retarget', dest='skip_retarget', default=False, + action="store_true", + help='skip parsing and interpretation of the re-target file,' + ' `./mbedls.json`') + parser.add_argument( + '-u', '--list-unmounted', dest='list_unmounted', default=False, + action='store_true', + help='list mbeds, regardless of whether they are mounted or not') + parser.add_argument( + '-d', '--debug', dest='debug', default=False, action="store_true", + help='outputs extra debug information useful when creating issues!') + + args = parser.parse_args(to_parse) + if args.mock: + args.command = mock_platform + return args + +def start_logging(): + try: + import colorlog + colorlog.basicConfig( + format='%(log_color)s%(levelname)s%(reset)s:%(name)s:%(message)s') + except ImportError: + import logging + logging.basicConfig() + del logging + +def mbedls_main(): + """! Function used to drive CLI (command line interface) application + @return Function exits with success code + """ + start_logging() + + args = parse_cli(sys.argv[1:]) + + import logging + root_logger = logging.getLogger("mbedls") + if args.debug: + root_logger.setLevel(logging.DEBUG) + else: + root_logger.setLevel(logging.INFO) + del logging + logger.debug("mbed-ls ver. %s", get_version()) + logger.debug("host: %s", str(mbed_lstools_os_info())) + + mbeds = create(skip_retarget=args.skip_retarget, + list_unmounted=args.list_unmounted, + force_mock=args.command is mock_platform) + + if mbeds is None: + logger.critical('This platform is not supported! Pull requests welcome at github.com/ARMmbed/mbed-ls') + sys.exit(1) + + ret_code = args.command(mbeds, args) + if not ret_code: + ret_code = 0 + + logger.debug("Return code: %d", ret_code) + + sys.exit(ret_code) diff --git a/tools/python/mbed_lstools/platform_database.py b/tools/python/mbed_lstools/platform_database.py new file mode 100644 index 0000000000..335dfb9d46 --- /dev/null +++ b/tools/python/mbed_lstools/platform_database.py @@ -0,0 +1,31 @@ +""" +mbed SDK +Copyright (c) 2017-2018 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. +""" + +"""Functions that manage a platform database""" + +from mbed_os_tools.detect.platform_database import ( + LOCAL_PLATFORM_DATABASE, + LOCAL_MOCKS_DATABASE, + DEFAULT_PLATFORM_DB, + PlatformDatabase +) + +""" +NOTE: The platform database is now in the mbed-os-tools package. +You can find it in the following file: mbed_os_tools/detect/platform_database.py +Please make all further contributions to the new package. +""" diff --git a/tools/python/mbed_lstools/windows.py b/tools/python/mbed_lstools/windows.py new file mode 100644 index 0000000000..521ed09a0b --- /dev/null +++ b/tools/python/mbed_lstools/windows.py @@ -0,0 +1,21 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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. +""" + +from mbed_os_tools.detect.windows import ( + MbedLsToolsWin7, + CompatibleIDsNotFoundException +) diff --git a/tools/python/mbed_os_tools/__init__.py b/tools/python/mbed_os_tools/__init__.py new file mode 100644 index 0000000000..7a65c9dafe --- /dev/null +++ b/tools/python/mbed_os_tools/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +# Mbed CE: added hardcoded version +VERSION = "0.0.16" diff --git a/tools/python/mbed_os_tools/detect/__init__.py b/tools/python/mbed_os_tools/detect/__init__.py new file mode 100644 index 0000000000..e7fcadcb1f --- /dev/null +++ b/tools/python/mbed_os_tools/detect/__init__.py @@ -0,0 +1,37 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +"""! @package mbed-ls + +mbed-ls package is a set of tools inherited from mbed-lmtools used to detect +mbed enabled devices on host (Windows, Linux and MacOS). + +mbed-lstools is a Python module that detects and lists mbed-enabled devices +connected to the host computer. It will be delivered as a redistributable +Python module (package) and command line tool. + +Currently supported operating system: +* Windows 7. +* Ubuntu. +* Mac OS X (Darwin). + +The stand-alone mbed-lstools Python package is still under development, +but it's already delivered as part of the mbed SDK's test suite and a command +line tool (see below). +""" + +from .main import create + +create = create diff --git a/tools/python/mbed_os_tools/detect/darwin.py b/tools/python/mbed_os_tools/detect/darwin.py new file mode 100644 index 0000000000..701ac5cbf8 --- /dev/null +++ b/tools/python/mbed_os_tools/detect/darwin.py @@ -0,0 +1,226 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +import subprocess +import platform + +from bs4 import BeautifulSoup + +try: + from plistlib import loads +except ImportError: + from plistlib import readPlistFromString as loads +from xml.parsers.expat import ExpatError + +from .lstools_base import MbedLsToolsBase + +import logging + +logger = logging.getLogger("mbedls.lstools_darwin") +logger.addHandler(logging.NullHandler()) +DEBUG = logging.DEBUG +del logging + +mbed_volume_name_match = re.compile(r"\b(mbed|SEGGER MSD|ATMEL EDBG Media)\b", re.I) + + +def _plist_from_popen(popen): + out, _ = popen.communicate() + if not out: + return [] + try: + try: + # Try simple and fast first if this fails fall back to the slower but + # more robust process. + return loads(out) + except ExpatError: + # Beautiful soup ensures the XML is properly formed after it is parsed + # so that it can be used by other less lenient commands without problems + xml_representation = BeautifulSoup(out.decode('utf8'), 'xml') + + if not xml_representation.get_text(): + # The output is not in the XML format + return loads(out) + return loads(xml_representation.decode().encode('utf8')) + except ExpatError: + return [] + + +def _find_TTY(obj): + """ Find the first tty (AKA IODialinDevice) that we can find in the + children of the specified object, or None if no tty is present. + """ + try: + return obj["IODialinDevice"] + except KeyError: + for child in obj.get("IORegistryEntryChildren", []): + found = _find_TTY(child) + if found: + return found + return None + + +def _prune(current, keys): + """ Reduce the amount of data we have to sift through to only + include the specified keys, and children that contain the + specified keys + """ + pruned_current = {k: current[k] for k in keys if k in current} + pruned_children = list( + filter( + None, [_prune(c, keys) for c in current.get("IORegistryEntryChildren", [])] + ) + ) + keep_current = any(k in current for k in keys) or pruned_children + if keep_current: + if pruned_children: + pruned_current["IORegistryEntryChildren"] = pruned_children + return pruned_current + else: + return {} + + +def _dfs_usb_info(obj, parents): + """ Find all of the usb info that we can from this particular IORegistry + tree with depth first search (and searching the parent stack....) + """ + output = {} + if ( + "BSD Name" in obj + and obj["BSD Name"].startswith("disk") + and mbed_volume_name_match.search(obj["IORegistryEntryName"]) + ): + disk_id = obj["BSD Name"] + usb_info = {"serial": None, "vendor_id": None, "product_id": None, "tty": None} + for parent in [obj] + parents: + if "USB Serial Number" in parent: + usb_info["serial"] = parent["USB Serial Number"] + if "idVendor" in parent and "idProduct" in parent: + usb_info["vendor_id"] = format(parent["idVendor"], "04x") + usb_info["product_id"] = format(parent["idProduct"], "04x") + if usb_info["serial"]: + usb_info["tty"] = _find_TTY(parent) + if all(usb_info.values()): + break + logger.debug("found usb info %r", usb_info) + output[disk_id] = usb_info + for child in obj.get("IORegistryEntryChildren", []): + output.update(_dfs_usb_info(child, [obj] + parents)) + return output + + +class MbedLsToolsDarwin(MbedLsToolsBase): + """ mbed-enabled platform detection on Mac OS X + """ + + def __init__(self, **kwargs): + MbedLsToolsBase.__init__(self, **kwargs) + self.mac_version = float(".".join(platform.mac_ver()[0].split(".")[:2])) + + def find_candidates(self): + # {volume_id: {serial:, vendor_id:, product_id:, tty:}} + volumes = self._volumes() + + # {volume_id: mount_point} + mounts = self._mount_points() + return [ + { + "mount_point": mounts[v], + "serial_port": volumes[v]["tty"], + "target_id_usb_id": volumes[v].get("serial"), + "vendor_id": volumes[v].get("vendor_id"), + "product_id": volumes[v].get("product_id"), + } + for v in set(volumes.keys()) and set(mounts.keys()) + if v in mounts and v in volumes + ] + + def _mount_points(self): + """ Returns map {volume_id: mount_point} """ + diskutil_ls = subprocess.Popen( + ["diskutil", "list", "-plist"], stdout=subprocess.PIPE + ) + disks = _plist_from_popen(diskutil_ls) + + if logger.isEnabledFor(DEBUG): + import pprint + + logger.debug( + "disks dict \n%s", pprint.PrettyPrinter(indent=2).pformat(disks) + ) + return { + disk["DeviceIdentifier"]: disk.get("MountPoint", None) + for disk in disks["AllDisksAndPartitions"] + } + + def _volumes(self): + """ returns a map {volume_id: {serial:, vendor_id:, product_id:, tty:}""" + + # to find all the possible mbed volumes, we look for registry entries + # under all possible USB tree which have a "BSD Name" that starts with + # "disk" # (i.e. this is a USB disk), and have a IORegistryEntryName that + # matches /\cmbed/ + # Once we've found a disk, we can search up for a parent with a valid + # serial number, and then search down again to find a tty that's part + # of the same composite device + # ioreg -a -r -n -l + usb_controllers = [ + "AppleUSBXHCI", + "AppleUSBUHCI", + "AppleUSBEHCI", + "AppleUSBOHCI", + "IOUSBHostDevice", + ] + + cmp_par = "-n" + # For El Captain we need to list all the instances of (-c) rather than + # compare names (-n) + if self.mac_version >= 10.11: + cmp_par = "-c" + + usb_tree = [] + for usb_controller in usb_controllers: + ioreg_usb = subprocess.Popen( + ["ioreg", "-a", "-r", cmp_par, usb_controller, "-l"], + stdout=subprocess.PIPE, + ) + usb_tree.extend(_plist_from_popen(ioreg_usb)) + + r = {} + + for name, obj in enumerate(usb_tree): + pruned_obj = _prune( + obj, + [ + "USB Serial Number", + "idVendor", + "BSD Name", + "IORegistryEntryName", + "idProduct", + "IODialinDevice", + ], + ) + if logger.isEnabledFor(DEBUG): + import pprint + + logger.debug( + "finding in \n%s", + pprint.PrettyPrinter(indent=2).pformat(pruned_obj), + ) + r.update(_dfs_usb_info(pruned_obj, [])) + + logger.debug("_volumes return %r", r) + return r diff --git a/tools/python/mbed_os_tools/detect/linux.py b/tools/python/mbed_os_tools/detect/linux.py new file mode 100644 index 0000000000..2800c4501c --- /dev/null +++ b/tools/python/mbed_os_tools/detect/linux.py @@ -0,0 +1,177 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +import os + +from .lstools_base import MbedLsToolsBase + +import logging + +logger = logging.getLogger("mbedls.lstools_linux") +logger.addHandler(logging.NullHandler()) +del logging + +SYSFS_BLOCK_DEVICE_PATH = "/sys/class/block" + + +def _readlink(link): + content = os.readlink(link) + if content.startswith(".."): + return os.path.abspath(os.path.join(os.path.dirname(link), content)) + else: + return content + + +class MbedLsToolsLinuxGeneric(MbedLsToolsBase): + """ mbed-enabled platform for Linux with udev + """ + + def __init__(self, **kwargs): + """! ctor + """ + MbedLsToolsBase.__init__(self, **kwargs) + self.nlp = re.compile(r"(pci|usb)-[0-9a-zA-Z:_-]*_(?P[0-9a-zA-Z]*)-.*$") + self.mmp = re.compile(r"(?P(/[^/ ]*)+) on (?P(/[^/ ]*)+) ") + self.udp = re.compile(r"^[0-9]+-[0-9]+[^:\s]*$") + + def find_candidates(self): + disk_ids = self._dev_by_id("disk") + serial_ids = self._dev_by_id("serial") + mount_ids = dict(self._fat_mounts()) + usb_info = self._sysfs_block_devices(disk_ids.values()) + logger.debug("Mount mapping %r", mount_ids) + + return [ + { + "mount_point": mount_ids.get(disk_dev), + "serial_port": serial_ids.get(disk_uuid), + "target_id_usb_id": disk_uuid, + "vendor_id": usb_info.get(disk_dev, {}).get("vendor_id"), + "product_id": usb_info.get(disk_dev, {}).get("product_id"), + } + for disk_uuid, disk_dev in disk_ids.items() + ] + + def _dev_by_id(self, device_type): + """! Get a dict, USBID -> device, for a device class + @param device_type The type of devices to search. For exmaple, "serial" + looks for all serial devices connected to this computer + @return A dict: Device USBID -> device file in /dev + """ + dir = os.path.join("/dev", device_type, "by-id") + if os.path.isdir(dir): + to_ret = dict( + self._hex_ids([os.path.join(dir, f) for f in os.listdir(dir)]) + ) + logger.debug("Found %s devices by id %r", device_type, to_ret) + return to_ret + else: + logger.error( + "Could not get %s devices by id. " + "This could be because your Linux distribution " + "does not use udev, or does not create /dev/%s/by-id " + "symlinks. Please submit an issue to github.com/" + "armmbed/mbed-ls.", + device_type, + device_type, + ) + return {} + + def _fat_mounts(self): + """! Lists mounted devices with vfat file system (potential mbeds) + @result Returns list of all mounted vfat devices + @details Uses Linux shell command: 'mount' + """ + _stdout, _, retval = self._run_cli_process("mount") + if not retval: + for line in _stdout.splitlines(): + if b"vfat" in line: + match = self.mmp.search(line.decode("utf-8")) + if match: + yield match.group("dev"), match.group("dir") + + def _hex_ids(self, dev_list): + """! Build a USBID map for a device list + @param disk_list List of disks in a system with USBID decoration + @return Returns map USBID -> device file in /dev + @details Uses regular expressions to get a USBID (TargeTIDs) a "by-id" + symbolic link + """ + logger.debug("Converting device list %r", dev_list) + for dl in dev_list: + match = self.nlp.search(dl) + if match: + yield match.group("usbid"), _readlink(dl) + + def _sysfs_block_devices(self, block_devices): + device_names = {os.path.basename(d): d for d in block_devices} + sysfs_block_devices = set(os.listdir(SYSFS_BLOCK_DEVICE_PATH)) + common_device_names = sysfs_block_devices.intersection(set(device_names.keys())) + result = {} + + for common_device_name in common_device_names: + sysfs_path = os.path.join(SYSFS_BLOCK_DEVICE_PATH, common_device_name) + full_sysfs_path = os.readlink(sysfs_path) + path_parts = full_sysfs_path.split("/") + + end_index = None + for index, part in enumerate(path_parts): + if self.udp.search(part): + end_index = index + + if end_index is None: + logger.debug( + "Did not find suitable usb folder for usb info: %s", full_sysfs_path + ) + continue + + usb_info_rel_path = path_parts[: end_index + 1] + usb_info_path = os.path.join( + SYSFS_BLOCK_DEVICE_PATH, os.sep.join(usb_info_rel_path) + ) + + vendor_id = None + product_id = None + + vendor_id_file_paths = os.path.join(usb_info_path, "idVendor") + product_id_file_paths = os.path.join(usb_info_path, "idProduct") + + try: + with open(vendor_id_file_paths, "r") as vendor_file: + vendor_id = vendor_file.read().strip() + except OSError as e: + logger.debug( + "Failed to read vendor id file %s weith error:", + vendor_id_file_paths, + e, + ) + + try: + with open(product_id_file_paths, "r") as product_file: + product_id = product_file.read().strip() + except OSError as e: + logger.debug( + "Failed to read product id file %s weith error:", + product_id_file_paths, + e, + ) + + result[device_names[common_device_name]] = { + "vendor_id": vendor_id, + "product_id": product_id, + } + + return result diff --git a/tools/python/mbed_os_tools/detect/lstools_base.py b/tools/python/mbed_os_tools/detect/lstools_base.py new file mode 100644 index 0000000000..e7852870ec --- /dev/null +++ b/tools/python/mbed_os_tools/detect/lstools_base.py @@ -0,0 +1,805 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +from abc import ABCMeta, abstractmethod +from io import open +from json import load +from os import listdir +from os.path import expanduser, isfile, join, exists, isdir +import logging +import functools +import json + +from .platform_database import ( + PlatformDatabase, + LOCAL_PLATFORM_DATABASE, + LOCAL_MOCKS_DATABASE, +) +from future.utils import with_metaclass + +mbedls_root_logger = logging.getLogger("mbedls") +mbedls_root_logger.setLevel(logging.WARNING) + +logger = logging.getLogger("mbedls.lstools_base") +logger.addHandler(logging.NullHandler()) + + +def deprecated(reason): + """Deprecate a function/method with a decorator""" + + def actual_decorator(func): + @functools.wraps(func) + def new_func(*args, **kwargs): + logger.warning("Call to deprecated function %s. %s", func.__name__, reason) + return func(*args, **kwargs) + + return new_func + + return actual_decorator + + +class FSInteraction(object): + BeforeFilter = 1 + AfterFilter = 2 + Never = 3 + + +class MbedLsToolsBase(with_metaclass(ABCMeta, object)): + """ Base class for mbed-lstools, defines mbed-ls tools interface for + mbed-enabled devices detection for various hosts + """ + + # Which OSs are supported by this module + # Note: more than one OS can be supported by mbed-lstools_* module + os_supported = [] + + # Directory where we will store global (OS user specific mocking) + HOME_DIR = expanduser("~") + MOCK_FILE_NAME = ".mbedls-mock" + RETARGET_FILE_NAME = "mbedls.json" + DETAILS_TXT_NAME = "DETAILS.TXT" + MBED_HTM_NAME = "mbed.htm" + + VENDOR_ID_DEVICE_TYPE_MAP = { + "0483": "stlink", + "0d28": "daplink", + "1366": "jlink", + "03eb": "atmel", + } + + def __init__(self, list_unmounted=False, **kwargs): + """ ctor + """ + self.retarget_data = {} # Used to retarget mbed-enabled platform properties + + platform_dbs = [] + if isfile(self.MOCK_FILE_NAME) or ( + "force_mock" in kwargs and kwargs["force_mock"] + ): + platform_dbs.append(self.MOCK_FILE_NAME) + elif isfile(LOCAL_MOCKS_DATABASE): + platform_dbs.append(LOCAL_MOCKS_DATABASE) + platform_dbs.append(LOCAL_PLATFORM_DATABASE) + self.plat_db = PlatformDatabase(platform_dbs, primary_database=platform_dbs[0]) + self.list_unmounted = list_unmounted + + if "skip_retarget" not in kwargs or not kwargs["skip_retarget"]: + self.retarget() + + @abstractmethod + def find_candidates(self): + """Find all candidate devices connected to this computer + + Note: Should not open any files + + @return A dict with the keys 'mount_point', 'serial_port' and 'target_id_usb_id' + """ + raise NotImplementedError + + def list_mbeds( + self, + fs_interaction=FSInteraction.BeforeFilter, + filter_function=None, + unique_names=False, + read_details_txt=False, + ): + """ List details of connected devices + @return Returns list of structures with detailed info about each mbed + @param fs_interaction A member of the FSInteraction class that picks the + trade of between quality of service and speed + @param filter_function Function that is passed each mbed candidate, + should return True if it should be included in the result + Ex. mbeds = list_mbeds(filter_function=lambda m: m['platform_name'] == 'K64F') + @param unique_names A boolean controlling the presence of the + 'platform_unique_name' member of the output dict + @param read_details_txt A boolean controlling the presense of the + output dict attributes read from other files present on the 'mount_point' + @details Function returns list of dictionaries with mbed attributes + 'mount_point', TargetID name etc. + Function returns mbed list with platform names if possible + """ + platform_count = {} + candidates = list(self.find_candidates()) + logger.debug("Candidates for display %r", candidates) + result = [] + for device in candidates: + device["device_type"] = self._detect_device_type(device) + if ( + not device["mount_point"] + or not self.mount_point_ready(device["mount_point"]) + ) and not self.list_unmounted: + if device["target_id_usb_id"] and device["serial_port"]: + logger.warning( + "MBED with target id '%s' is connected, but not mounted. " + "Use the '-u' flag to include it in the list.", + device["target_id_usb_id"], + ) + else: + platform_data = self.plat_db.get( + device["target_id_usb_id"][0:4], + device_type=device["device_type"] or "daplink", + verbose_data=True, + ) + device.update(platform_data or {"platform_name": None}) + maybe_device = { + FSInteraction.BeforeFilter: self._fs_before_id_check, + FSInteraction.AfterFilter: self._fs_after_id_check, + FSInteraction.Never: self._fs_never, + }[fs_interaction](device, filter_function, read_details_txt) + if maybe_device and ( + maybe_device["mount_point"] or self.list_unmounted + ): + if unique_names: + name = device["platform_name"] + platform_count.setdefault(name, -1) + platform_count[name] += 1 + device["platform_name_unique"] = "%s[%d]" % ( + name, + platform_count[name], + ) + try: + device.update(self.retarget_data[device["target_id"]]) + logger.debug( + "retargeting %s with %r", + device["target_id"], + self.retarget_data[device["target_id"]], + ) + except KeyError: + pass + + # This is done for API compatibility, would prefer for this to + # just be None + device["device_type"] = ( + device["device_type"] if device["device_type"] else "unknown" + ) + result.append(maybe_device) + + return result + + def _fs_never(self, device, filter_function, read_details_txt): + """Filter device without touching the file system of the device""" + device["target_id"] = device["target_id_usb_id"] + device["target_id_mbed_htm"] = None + if not filter_function or filter_function(device): + return device + else: + return None + + def _fs_before_id_check(self, device, filter_function, read_details_txt): + """Filter device after touching the file system of the device. + Said another way: Touch the file system before filtering + """ + + device["target_id"] = device["target_id_usb_id"] + self._update_device_from_fs(device, read_details_txt) + if not filter_function or filter_function(device): + return device + else: + return None + + def _fs_after_id_check(self, device, filter_function, read_details_txt): + """Filter device before touching the file system of the device. + Said another way: Touch the file system after filtering + """ + device["target_id"] = device["target_id_usb_id"] + device["target_id_mbed_htm"] = None + if not filter_function or filter_function(device): + self._update_device_from_fs(device, read_details_txt) + return device + else: + return None + + def _update_device_from_fs(self, device, read_details_txt): + """ Updates the device information based on files from its 'mount_point' + @param device Dictionary containing device information + @param read_details_txt A boolean controlling the presense of the + output dict attributes read from other files present on the 'mount_point' + """ + if not device.get("mount_point", None): + return + + try: + directory_entries = listdir(device["mount_point"]) + device["directory_entries"] = directory_entries + device["target_id"] = device["target_id_usb_id"] + + # Always try to update using daplink compatible boards processself. + # This is done for backwards compatibility. + self._update_device_details_daplink_compatible(device, read_details_txt) + + if device.get("device_type") == "jlink": + self._update_device_details_jlink(device, read_details_txt) + + if device.get("device_type") == "atmel": + self._update_device_details_atmel(device, read_details_txt) + + except (OSError, IOError) as e: + logger.warning( + 'Marking device with mount point "%s" as unmounted due to the ' + "following error: %s", + device["mount_point"], + e, + ) + device["mount_point"] = None + + def _detect_device_type(self, device): + """ Returns a string of the device type + @param device Dictionary containing device information + @return Device type located in VENDOR_ID_DEVICE_TYPE_MAP or None if unknown + """ + + return self.VENDOR_ID_DEVICE_TYPE_MAP.get(device.get("vendor_id")) + + def _update_device_details_daplink_compatible(self, device, read_details_txt): + """ Updates the daplink-specific device information based on files from its + 'mount_point' + @param device Dictionary containing device information + @param read_details_txt A boolean controlling the presense of the + output dict attributes read from other files present on the 'mount_point' + """ + lowercase_directory_entries = [e.lower() for e in device["directory_entries"]] + if self.MBED_HTM_NAME.lower() in lowercase_directory_entries: + self._update_device_from_htm(device) + elif not read_details_txt: + logger.debug( + "Since mbed.htm is not present, attempting to use " + "details.txt for the target id" + ) + read_details_txt = True + + if ( + read_details_txt + and self.DETAILS_TXT_NAME.lower() in lowercase_directory_entries + ): + details_txt = self._details_txt(device["mount_point"]) or {} + device.update( + { + "daplink_%s" % f.lower().replace(" ", "_"): v + for f, v in details_txt.items() + } + ) + + # If details.txt contains the target id, this is the most trusted source + if device.get("daplink_unique_id", None): + device["target_id"] = device["daplink_unique_id"] + + if device["target_id"]: + identifier = device["target_id"][0:4] + platform_data = self.plat_db.get( + identifier, device_type="daplink", verbose_data=True + ) + if not platform_data: + logger.warning( + 'daplink entry: "%s" not found in platform database', identifier + ) + else: + device.update(platform_data) + else: + device["platform_name"] = None + + def _update_device_details_jlink(self, device, _): + """ Updates the jlink-specific device information based on files from its 'mount_point' + @param device Dictionary containing device information + """ + lower_case_map = {e.lower(): e for e in device["directory_entries"]} + + if "board.html" in lower_case_map: + board_file_key = "board.html" + elif "user guide.html" in lower_case_map: + board_file_key = "user guide.html" + else: + logger.warning("No valid file found to update JLink device details") + return + + board_file_path = join(device["mount_point"], lower_case_map[board_file_key]) + with open(board_file_path, "r") as board_file: + board_file_lines = board_file.readlines() + + for line in board_file_lines: + m = re.search(r"url=([\w\d\:\-/\\\?\.=-_]+)", line) + if m: + device["url"] = m.group(1).strip() + identifier = device["url"].split("/")[-1] + platform_data = self.plat_db.get( + identifier, device_type="jlink", verbose_data=True + ) + if not platform_data: + logger.warning( + 'jlink entry: "%s", not found in platform database', identifier + ) + else: + device.update(platform_data) + break + + def _update_device_from_htm(self, device): + """Set the 'target_id', 'target_id_mbed_htm', 'platform_name' and + 'daplink_*' attributes by reading from mbed.htm on the device + """ + htm_target_id, daplink_info = self._read_htm_ids(device["mount_point"]) + if daplink_info: + device.update( + { + "daplink_%s" % f.lower().replace(" ", "_"): v + for f, v in daplink_info.items() + } + ) + if htm_target_id: + logger.debug( + "Found htm target id, %s, for usb target id %s", + htm_target_id, + device["target_id_usb_id"], + ) + device["target_id"] = htm_target_id + else: + logger.debug( + "Could not read htm on from usb id %s. Falling back to usb id", + device["target_id_usb_id"], + ) + device["target_id"] = device["target_id_usb_id"] + device["target_id_mbed_htm"] = htm_target_id + + def _update_device_details_atmel(self, device, _): + """ Updates the Atmel device information based on files from its 'mount_point' + @param device Dictionary containing device information + @param read_details_txt A boolean controlling the presense of the + output dict attributes read from other files present on the 'mount_point' + """ + + # Atmel uses a system similar to DAPLink, but there's no details.txt with a + # target ID to identify device we can use the serial, which is ATMLXXXXYYYYYYY + # where XXXX is the board identifier. + # This can be verified by looking at readme.htm, which also uses the board ID + # to redirect to platform page + + device["target_id"] = device["target_id_usb_id"][4:8] + platform_data = self.plat_db.get( + device["target_id"], device_type="atmel", verbose_data=True + ) + + device.update(platform_data or {"platform_name": None}) + + def mock_manufacture_id(self, mid, platform_name, oper="+"): + """! Replace (or add if manufacture id doesn't exist) entry in self.manufacture_ids + @param oper '+' add new mock / override existing entry + '-' remove mid from mocking entry + @return Mocked structure (json format) + """ + if oper == "+": + self.plat_db.add(mid, platform_name, permanent=True) + elif oper == "-": + self.plat_db.remove(mid, permanent=True) + else: + raise ValueError("oper can only be [+-]") + + def retarget_read(self): + """! Load retarget data from local file + @return Curent retarget configuration (dictionary) + """ + if isfile(self.RETARGET_FILE_NAME): + logger.debug("reading retarget file %s", self.RETARGET_FILE_NAME) + try: + with open(self.RETARGET_FILE_NAME, "r", encoding="utf-8") as f: + return load(f) + except IOError as e: + logger.exception(e) + except ValueError as e: + logger.exception(e) + return {} + + def retarget(self): + """! Enable retargeting + @details Read data from local retarget configuration file + @return Retarget data structure read from configuration file + """ + self.retarget_data = self.retarget_read() + return self.retarget_data + + def get_dummy_platform(self, platform_name): + """! Returns simple dummy platform """ + if not hasattr(self, "dummy_counter"): + self.dummy_counter = {} # platform: counter + + if platform_name not in self.dummy_counter: + self.dummy_counter[platform_name] = 0 + + platform = { + "platform_name": platform_name, + "platform_name_unique": "%s[%d]" + % (platform_name, self.dummy_counter[platform_name]), + "mount_point": "DUMMY", + "serial_port": "DUMMY", + "target_id": "DUMMY", + "target_id_mbed_htm": "DUMMY", + "target_id_usb_id": "DUMMY", + "daplink_version": "DUMMY", + } + self.dummy_counter[platform_name] += 1 + return platform + + def get_supported_platforms(self, device_type=None): + """! Return a dictionary of supported target ids and the corresponding platform name + @param device_type Filter which device entries are returned from the platform + database + @return Dictionary of { 'target_id': 'platform_name', ... } + """ + kwargs = {} + if device_type is not None: + kwargs["device_type"] = device_type + + items = self.plat_db.items(**kwargs) + return {i[0]: i[1] for i in items} + + # Private functions supporting API + def _read_htm_ids(self, mount_point): + """! Function scans mbed.htm to get information about TargetID. + @param mount_point mbed mount point (disk / drive letter) + @return Function returns targetID, in case of failure returns None. + @details Note: This function should be improved to scan variety of boards' + mbed.htm files + """ + result = {} + target_id = None + for line in self._htm_lines(mount_point): + target_id = target_id or self._target_id_from_htm(line) + ver_bld = self._mbed_htm_comment_section_ver_build(line) + if ver_bld: + result["version"], result["build"] = ver_bld + + m = re.search(r"url=([\w\d\:/\\\?\.=-_]+)", line) + if m: + result["url"] = m.group(1).strip() + return target_id, result + + def _mbed_htm_comment_section_ver_build(self, line): + """! Check for Version and Build date of interface chip firmware im mbed.htm file + @return (version, build) tuple if successful, None if no info found + """ + # + m = re.search(r"^", line) + if m: + version_str, build_str = m.groups() + return (version_str.strip(), build_str.strip()) + + # + m = re.search(r"^ + m = re.search(r"^", line) + if m: + version_str, build_str = m.groups() + return (version_str.strip(), build_str.strip()) + return None + + def _htm_lines(self, mount_point): + if mount_point: + mbed_htm_path = join(mount_point, self.MBED_HTM_NAME) + with open(mbed_htm_path, "r") as f: + return f.readlines() + + def _details_txt(self, mount_point): + """! Load DETAILS.TXT to dictionary: + DETAILS.TXT example: + Version: 0226 + Build: Aug 24 2015 17:06:30 + Git Commit SHA: 27a236b9fe39c674a703c5c89655fbd26b8e27e1 + Git Local mods: Yes + + or: + + # DAPLink Firmware - see https://mbed.com/daplink + Unique ID: 0240000029164e45002f0012706e0006f301000097969900 + HIF ID: 97969900 + Auto Reset: 0 + Automation allowed: 0 + Daplink Mode: Interface + Interface Version: 0240 + Git SHA: c765cbb590f57598756683254ca38b211693ae5e + Local Mods: 0 + USB Interfaces: MSD, CDC, HID + Interface CRC: 0x26764ebf + """ + + if mount_point: + path_to_details_txt = join(mount_point, self.DETAILS_TXT_NAME) + with open(path_to_details_txt, "r") as f: + return self._parse_details(f.readlines()) + return None + + def _parse_details(self, lines): + result = {} + for line in lines: + if not line.startswith("#"): + key, _, value = line.partition(":") + if value: + result[key] = value.strip() + if "Interface Version" in result: + result["Version"] = result["Interface Version"] + return result + + def _target_id_from_htm(self, line): + """! Extract Target id from htm line. + @return Target id or None + """ + # Detecting modern mbed.htm file format + m = re.search("\\?code=([a-fA-F0-9]+)", line) + if m: + result = m.groups()[0] + logger.debug("Found target id %s in htm line %s", result, line) + return result + # Last resort, we can try to see if old mbed.htm format is there + m = re.search("\\?auth=([a-fA-F0-9]+)", line) + if m: + result = m.groups()[0] + logger.debug("Found target id %s in htm line %s", result, line) + return result + + return None + + def mount_point_ready(self, path): + """! Check if a mount point is ready for file operations + """ + return exists(path) and isdir(path) + + @staticmethod + def _run_cli_process(cmd, shell=True): + """! Runs command as a process and return stdout, stderr and ret code + @param cmd Command to execute + @return Tuple of (stdout, stderr, returncode) + """ + from subprocess import Popen, PIPE + + p = Popen(cmd, shell=shell, stdout=PIPE, stderr=PIPE) + _stdout, _stderr = p.communicate() + return _stdout, _stderr, p.returncode + + @deprecated( + "Functionality has been moved into 'list_mbeds'. " + "Please use list_mbeds with 'unique_names=True' and " + "'read_details_txt=True'" + ) + def list_mbeds_ext(self): + """! Function adds extra information for each mbed device + @return Returns list of mbed devices plus extended data like + 'platform_name_unique' + @details Get information about mbeds with extended parameters/info included + """ + + return self.list_mbeds(unique_names=True, read_details_txt=True) + + @deprecated( + "List formatting methods are deprecated for a simpler API. " + "Please use 'list_mbeds' instead." + ) + def list_manufacture_ids(self): + """! Creates list of all available mappings for target_id -> Platform + @return String with table formatted output + """ + from prettytable import PrettyTable, HEADER + + columns = ["target_id_prefix", "platform_name"] + pt = PrettyTable(columns, junction_char="|", hrules=HEADER) + for col in columns: + pt.align[col] = "l" + + for target_id_prefix, platform_name in sorted(self.plat_db.items()): + pt.add_row([target_id_prefix, platform_name]) + + return pt.get_string() + + @deprecated( + "List formatting methods are deprecated to simplify the API. " + "Please use 'list_mbeds' instead." + ) + def list_platforms(self): + """! Useful if you just want to know which platforms are currently + available on the system + @return List of (unique values) available platforms + """ + result = [] + mbeds = self.list_mbeds() + for i, val in enumerate(mbeds): + platform_name = str(val["platform_name"]) + if platform_name not in result: + result.append(platform_name) + return result + + @deprecated( + "List formatting methods are deprecated to simplify the API. " + "Please use 'list_mbeds' instead." + ) + def list_platforms_ext(self): + """! Useful if you just want to know how many platforms of each type are + currently available on the system + @return Dict of platform: platform_count + """ + result = {} + mbeds = self.list_mbeds() + for i, val in enumerate(mbeds): + platform_name = str(val["platform_name"]) + if platform_name not in result: + result[platform_name] = 1 + else: + result[platform_name] += 1 + return result + + @deprecated( + "List formatting methods are deprecated to simplify the API. " + "Please use 'list_mbeds' instead." + ) + def list_mbeds_by_targetid(self): + """! Get information about mbeds with extended parameters/info included + @return Returns dictionary where keys are TargetIDs and values are mbed + structures + @details Ordered by target id (key: target_id). + """ + result = {} + mbed_list = self.list_mbeds_ext() + for mbed in mbed_list: + target_id = mbed["target_id"] + result[target_id] = mbed + return result + + @deprecated( + "List formatting methods are deprecated to simplify the API. " + "Please use 'list_mbeds' instead." + ) + def get_string( + self, border=False, header=True, padding_width=1, sortby="platform_name" + ): + """! Printing with some sql table like decorators + @param border Table border visibility + @param header Table header visibility + @param padding_width Table padding + @param sortby Column used to sort results + @return Returns string which can be printed on console + """ + from prettytable import PrettyTable, HEADER + + result = "" + mbeds = self.list_mbeds(unique_names=True, read_details_txt=True) + if mbeds: + """ ['platform_name', 'mount_point', 'serial_port', 'target_id'] - + columns generated from USB auto-detection + ['platform_name_unique', ...] - + columns generated outside detection subsystem (OS dependent detection) + """ + columns = [ + "platform_name", + "platform_name_unique", + "mount_point", + "serial_port", + "target_id", + "daplink_version", + ] + pt = PrettyTable(columns, junction_char="|", hrules=HEADER) + for col in columns: + pt.align[col] = "l" + + for mbed in mbeds: + row = [] + for col in columns: + row.append(mbed[col] if col in mbed and mbed[col] else "unknown") + pt.add_row(row) + result = pt.get_string( + border=border, header=header, padding_width=padding_width, sortby=sortby + ) + return result + + # Private functions supporting API + + @deprecated( + "This method will be removed from the public API. " + "Please use 'list_mbeds' instead" + ) + def get_json_data_from_file(self, json_spec_filename, verbose=False): + """! Loads from file JSON formatted string to data structure + @return None if JSON can be loaded + """ + try: + with open(json_spec_filename) as data_file: + try: + return json.load(data_file) + except ValueError as json_error_msg: + logger.error( + "Parsing file(%s): %s", json_spec_filename, json_error_msg + ) + return None + except IOError as fileopen_error_msg: + logger.warning(fileopen_error_msg) + return None + + @deprecated( + "This method will be removed from the public API. " + "Please use 'list_mbeds' instead" + ) + def get_htm_target_id(self, mount_point): + target_id, _ = self._read_htm_ids(mount_point) + return target_id + + @deprecated( + "This method will be removed from the public API. " + "Please use 'list_mbeds' instead" + ) + def get_mbed_htm(self, mount_point): + _, build_info = self._read_htm_ids(mount_point) + return build_info + + @deprecated( + "This method will be removed from the public API. " + "Please use 'list_mbeds' instead" + ) + def get_mbed_htm_comment_section_ver_build(self, line): + return self._mbed_htm_comment_section_ver_build(line) + + @deprecated( + "This method will be removed from the public API. " + "Please use 'list_mbeds' instead" + ) + def get_mbed_htm_lines(self, mount_point): + return self._htm_lines(mount_point) + + @deprecated( + "This method will be removed from the public API. " + "Please use 'list_mbeds' instead" + ) + def get_details_txt(self, mount_point): + return self._details_txt(mount_point) + + @deprecated( + "This method will be removed from the public API. " + "Please use 'list_mbeds' instead" + ) + def parse_details_txt(self, lines): + return self._parse_details(lines) + + @deprecated( + "This method will be removed from the public API. " + "Please use 'list_mbeds' instead" + ) + def scan_html_line_for_target_id(self, line): + return self._target_id_from_htm(line) + + @staticmethod + @deprecated( + "This method will be removed from the public API. " + "Please use 'list_mbeds' instead" + ) + def run_cli_process(cmd, shell=True): + return MbedLsToolsBase._run_cli_process(cmd, shell) diff --git a/tools/python/mbed_os_tools/detect/main.py b/tools/python/mbed_os_tools/detect/main.py new file mode 100644 index 0000000000..031205198f --- /dev/null +++ b/tools/python/mbed_os_tools/detect/main.py @@ -0,0 +1,104 @@ + +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 sys +import platform + +# Make sure that any global generic setup is run +from . import lstools_base # noqa: F401 + +import logging + +logger = logging.getLogger("mbedls.main") +logger.addHandler(logging.NullHandler()) +del logging + + +def create(**kwargs): + """! Factory used to create host OS specific mbed-lstools object + + :param kwargs: keyword arguments to pass along to the constructors + @return Returns MbedLsTools object or None if host OS is not supported + + """ + result = None + mbed_os = mbed_os_support() + if mbed_os is not None: + if mbed_os == "Windows7": + from .windows import MbedLsToolsWin7 + + result = MbedLsToolsWin7(**kwargs) + elif mbed_os == "LinuxGeneric": + from .linux import MbedLsToolsLinuxGeneric + + result = MbedLsToolsLinuxGeneric(**kwargs) + elif mbed_os == "Darwin": + from .darwin import MbedLsToolsDarwin + + result = MbedLsToolsDarwin(**kwargs) + return result + + +def mbed_os_support(): + """! Function used to determine if host OS is supported by mbed-lstools + + @return Returns None if host OS is not supported else return OS short name + + @details This function should be ported for new OS support + """ + result = None + os_info = mbed_lstools_os_info() + if os_info[0] == "nt" and os_info[1] == "Windows": + result = "Windows7" + elif os_info[0] == "posix" and os_info[1] == "Linux": + result = "LinuxGeneric" + elif os_info[0] == "posix" and os_info[1] == "Darwin": + result = "Darwin" + return result + + +def mbed_lstools_os_info(): + """! Returns information about host OS + + @return Returns tuple with information about OS and host platform + """ + result = ( + os.name, + platform.system(), + platform.release(), + platform.version(), + sys.platform, + ) + return result + + +def mock_platform(mbeds, args): + for token in args.mock.split(","): + if ":" in token: + oper = "+" # Default + mid, platform_name = token.split(":") + if mid and mid[0] in ["+", "-"]: + oper = mid[0] # Operation (character) + mid = mid[1:] # We remove operation character + mbeds.mock_manufacture_id(mid, platform_name, oper=oper) + elif token and token[0] in ["-", "!"]: + # Operations where do not specify data after colon: --mock=-1234,-7678 + oper = token[0] + mid = token[1:] + mbeds.mock_manufacture_id(mid, "dummy", oper=oper) + else: + logger.error("Could not parse mock from token: '%s'", token) diff --git a/tools/python/mbed_os_tools/detect/platform_database.py b/tools/python/mbed_os_tools/detect/platform_database.py new file mode 100644 index 0000000000..0d0c35e777 --- /dev/null +++ b/tools/python/mbed_os_tools/detect/platform_database.py @@ -0,0 +1,571 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +"""Functions that manage a platform database""" + +import datetime +import json +import re +from collections import OrderedDict, defaultdict +from copy import copy +from io import open +from os import makedirs +from os.path import join, dirname, getmtime +from appdirs import user_data_dir +from fasteners import InterProcessLock + +import logging + +logger = logging.getLogger("mbedls.platform_database") +logger.addHandler(logging.NullHandler()) +del logging + +try: + unicode +except NameError: + unicode = str + +LOCAL_PLATFORM_DATABASE = join(user_data_dir("mbedls"), "platforms.json") +LOCAL_MOCKS_DATABASE = join(user_data_dir("mbedls"), "mock.json") + +DEFAULT_PLATFORM_DB = { + u"daplink": { + u"0200": u"KL25Z", + u"0201": u"KW41Z", + u"0210": u"KL05Z", + u"0214": u"HEXIWEAR", + u"0217": u"K82F", + u"0218": u"KL82Z", + u"0220": u"KL46Z", + u"0227": u"MIMXRT1050_EVK", + u"0228": u"RAPIDIOT_K64F", + u"0230": u"K20D50M", + u"0231": u"K22F", + u"0234": u"RAPIDIOT_KW41Z", + u"0236": u"LPC55S69", + u"0240": u"K64F", + u"0245": u"K64F", + u"0250": u"KW24D", + u"0261": u"KL27Z", + u"0262": u"KL43Z", + u"0300": u"MTS_GAMBIT", + u"0305": u"MTS_MDOT_F405RG", + u"0310": u"MTS_DRAGONFLY_F411RE", + u"0311": u"K66F", + u"0312": u"MTS_DRAGONFLY_L471QG", + u"0313": u"MTS_DRAGONFLY_L496VG", + u"0315": u"MTS_MDOT_F411RE", + u"0316": u"MTS_DRAGONFLY_F413RH", + u"0350": u"XDOT_L151CC", + u"0360": u"HANI_IOT", + u"0400": u"MAXWSNENV", + u"0405": u"MAX32600MBED", + u"0407": u"MAX32620HSP", + u"0408": u"MAX32625NEXPAQ", + u"0409": u"MAX32630FTHR", + u"0410": u"ETTEPLAN_LORA", + u"0415": u"MAX32625MBED", + u"0416": u"MAX32625PICO", + u"0418": u"MAX32620FTHR", + u"0419": u"MAX35103EVKIT2", + u"0421": u"MAX32660EVSYS", + u"0424": u"MAX32670EVKIT", + u"0450": u"MTB_UBLOX_ODIN_W2", + u"0451": u"MTB_MXCHIP_EMW3166", + u"0452": u"MTB_LAIRD_BL600", + u"0453": u"MTB_MTS_XDOT", + u"0454": u"MTB_MTS_DRAGONFLY", + u"0455": u"MTB_UBLOX_NINA_B1", + u"0456": u"MTB_MURATA_ABZ", + u"0457": u"MTB_RAK811", + u"0458": u"ADV_WISE_1510", + u"0459": u"ADV_WISE_1530", + u"0460": u"ADV_WISE_1570", + u"0461": u"MTB_LAIRD_BL652", + u"0462": u"MTB_USI_WM_BN_BM_22", + u"0465": u"MTB_LAIRD_BL654", + u"0466": u"MTB_MURATA_WSM_BL241", + u"0467": u"MTB_STM_S2LP", + u"0468": u"MTB_STM_L475", + u"0469": u"MTB_STM32_F439", + u"0472": u"MTB_ACONNO_ACN52832", + u"0602": u"EV_COG_AD3029LZ", + u"0603": u"EV_COG_AD4050LZ", + u"0604": u"SDP_K1", + u"0700": u"NUCLEO_F103RB", + u"0705": u"NUCLEO_F302R8", + u"0710": u"NUCLEO_L152RE", + u"0715": u"NUCLEO_L053R8", + u"0720": u"NUCLEO_F401RE", + u"0725": u"NUCLEO_F030R8", + u"0729": u"NUCLEO_G071RB", + u"0730": u"NUCLEO_F072RB", + u"0735": u"NUCLEO_F334R8", + u"0740": u"NUCLEO_F411RE", + u"0742": u"NUCLEO_F413ZH", + u"0743": u"DISCO_F413ZH", + u"0744": u"NUCLEO_F410RB", + u"0745": u"NUCLEO_F303RE", + u"0746": u"DISCO_F303VC", + u"0747": u"NUCLEO_F303ZE", + u"0750": u"NUCLEO_F091RC", + u"0755": u"NUCLEO_F070RB", + u"0760": u"NUCLEO_L073RZ", + u"0764": u"DISCO_L475VG_IOT01A", + u"0765": u"NUCLEO_L476RG", + u"0766": u"SILICA_SENSOR_NODE", + u"0770": u"NUCLEO_L432KC", + u"0774": u"DISCO_L4R9I", + u"0775": u"NUCLEO_F303K8", + u"0776": u"NUCLEO_L4R5ZI", + u"0777": u"NUCLEO_F446RE", + u"0778": u"NUCLEO_F446ZE", + u"0779": u"NUCLEO_L433RC_P", + u"0780": u"NUCLEO_L011K4", + u"0781": u"NUCLEO_L4R5ZI_P", + u"0783": u"NUCLEO_L010RB", + u"0785": u"NUCLEO_F042K6", + u"0788": u"DISCO_F469NI", + u"0790": u"NUCLEO_L031K6", + u"0791": u"NUCLEO_F031K6", + u"0795": u"DISCO_F429ZI", + u"0796": u"NUCLEO_F429ZI", + u"0797": u"NUCLEO_F439ZI", + u"0805": u"DISCO_L053C8", + u"0810": u"DISCO_F334C8", + u"0812": u"NUCLEO_F722ZE", + u"0813": u"NUCLEO_H743ZI", + u"0814": u"DISCO_H747I", + u"0815": u"DISCO_F746NG", + u"0816": u"NUCLEO_F746ZG", + u"0817": u"DISCO_F769NI", + u"0818": u"NUCLEO_F767ZI", + u"0820": u"DISCO_L476VG", + u"0821": u"NUCLEO_L452RE", + u"0822": u"DISCO_L496AG", + u"0823": u"NUCLEO_L496ZG", + u"0824": u"LPC824", + u"0825": u"DISCO_F412ZG", + u"0826": u"NUCLEO_F412ZG", + u"0827": u"NUCLEO_L486RG", + u"0828": u"NUCLEO_L496ZG_P", + u"0829": u"NUCLEO_L452RE_P", + u"0830": u"DISCO_F407VG", + u"0833": u"DISCO_L072CZ_LRWAN1", + u"0835": u"NUCLEO_F207ZG", + u"0836": u"NUCLEO_H743ZI2", + u"0839": u"NUCLEO_WB55RG", + u"0840": u"B96B_F446VE", + u"0841": u"NUCLEO_G474RE", + u"0842": u"NUCLEO_H753ZI", + u"0843": u"NUCLEO_H745ZI_Q", + u"0844": u"NUCLEO_H755ZI_Q", + u"0847": u"DISCO_H745I", + u"0849": u"NUCLEO_G070RB", + u"0850": u"NUCLEO_G431RB", + u"0851": u"NUCLEO_G431KB", + u"0852": u"NUCLEO_G031K8", + u"0853": u"NUCLEO_F301K8", + u"0854": u"NUCLEO_L552ZE_Q", + u"0855": u"DISCO_L562QE", + u"0858": u"DISCO_H750B", + u"0859": u"DISCO_H7B3I", + u"0860": u"NUCLEO_H7A3ZI_Q", + u"0863": u"DISCO_L4P5G", + u"0865": u"NUCLEO_L4P5ZG", + u"0866": u"NUCLEO_WL55JC", + u"0871": u"NUCLEO_H723ZG", + u"0872": u"NUCLEO_G0B1RE", + u"0875": u"DISCO_H735G", + u"0879": u"NUCLEO_F756ZG", + u"0882": u"NUCLEO_G491RE", + u"0883": u"NUCLEO_WB15CC", + u"0884": u"DISCO_WB5MMG", + u"0885": u"B_L4S5I_IOT01A", + u"0886": u"NUCLEO_U575ZI_Q", + u"0887": u"B_U585I_IOT02A", + u"0900": u"SAMR21G18A", + u"0905": u"SAMD21G18A", + u"0910": u"SAML21J18A", + u"0915": u"SAMD21J18A", + u"1000": u"LPC2368", + u"1010": u"LPC1768", + u"1017": u"HRM1017", + u"1018": u"SSCI824", + u"1019": u"TY51822R3", + u"1022": u"RO359B", + u"1034": u"LPC11U34", + u"1040": u"LPC11U24", + u"1045": u"LPC11U24", + u"1050": u"LPC812", + u"1054": u"LPC54114", + u"1056": u"LPC546XX", + u"1060": u"LPC4088", + u"1061": u"LPC11U35_401", + u"1062": u"LPC4088_DM", + u"1070": u"NRF51822", + u"1075": u"NRF51822_OTA", + u"1080": u"OC_MBUINO", + u"1090": u"RBLAB_NRF51822", + u"1093": u"RBLAB_BLENANO2", + u"1095": u"RBLAB_BLENANO", + u"1100": u"NRF51_DK", + u"1101": u"NRF52_DK", + u"1102": u"NRF52840_DK", + u"1105": u"NRF51_DK_OTA", + u"1114": u"LPC1114", + u"1120": u"NRF51_DONGLE", + u"1130": u"NRF51822_SBK", + u"1140": u"WALLBOT_BLE", + u"1168": u"LPC11U68", + u"1200": u"NCS36510", + u"1234": u"UBLOX_C027", + u"1236": u"UBLOX_EVK_ODIN_W2", + u"1237": u"UBLOX_EVK_NINA_B1", + u"1280": u"OKDO_ODIN_W2", + u"1300": u"NUC472-NUTINY", + u"1301": u"NUMBED", + u"1302": u"NUMAKER_PFM_NUC472", + u"1303": u"NUMAKER_PFM_M453", + u"1304": u"NUMAKER_PFM_M487", + u"1305": u"NU_PFM_M2351", + u"1306": u"NUMAKER_PFM_NANO130", + u"1307": u"NUMAKER_PFM_NUC240", + u"1308": u"NUMAKER_IOT_M487", + u"1309": u"NUMAKER_IOT_M252", + u"1310": u"NUMAKER_IOT_M263A", + u"1312": u"NU_M2354", + u"1313": u"NUMAKER_IOT_M467", + u"1500": u"RHOMBIO_L476DMW1K", + u"1549": u"LPC1549", + u"1600": u"LPC4330_M4", + u"1605": u"LPC4330_M4", + u"1701": u"GD32_F307VG", + u"1702": u"GD32_F450ZI", + u"1703": u"GD32_E103VB", + u'1900': u'CY8CKIT_062_WIFI_BT', + u'1901': u'CY8CPROTO_062_4343W', + u'1902': u'CY8CKIT_062_BLE', + u'1903': u'CYW9P62S1_43012EVB_01', + u'1904': u'CY8CPROTO_063_BLE', + u'1905': u'CY8CKIT_062S2_4343W', + u'1906': u'CYW943012P6EVB_01', + u'1907': u'CY8CPROTO_064_SB', + u'1908': u'CYW9P62S1_43438EVB_01', + u'1909': u'CY8CPROTO_062S2_43012', + u'190A': u'CY8CKIT_064S2_4343W', + u'190B': u'CY8CKIT_062S2_43012', + u'190C': u'CY8CPROTO_064B0S3', + u'190E': u'CY8CPROTO_062S3_4343W', + u'190F': u'CY8CPROTO_064B0S1_BLE', + u'1910': u'CY8CKIT064B0S2_4343W', + u'1911': u'CY8CKIT064S0S2_4343W', + u'1912': u'CYSBSYSKIT_01', + u"2000": u"EFM32_G8XX_STK", + u"2005": u"EFM32HG_STK3400", + u"2010": u"EFM32WG_STK3800", + u"2015": u"EFM32GG_STK3700", + u"2020": u"EFM32LG_STK3600", + u"2025": u"EFM32TG_STK3300", + u"2030": u"EFM32ZG_STK3200", + u"2035": u"EFM32PG_STK3401", + u"2040": u"EFM32PG12_STK3402", + u"2041": u"TB_SENSE_12", + u"2042": u"EFM32GG11_STK3701", + u"2043": u"EFM32TG11_STK3301", + u"2045": u"TB_SENSE_1", + u"2100": u"XBED_LPC1768", + u"2201": u"WIZWIKI_W7500", + u"2202": u"WIZWIKI_W7500ECO", + u"2203": u"WIZWIKI_W7500P", + u"2600": u"EP_AGORA", + u"3001": u"LPC11U24", + u"3101": u"SDT32620B", + u"3102": u"SDT32625B", + u"3103": u"SDT51822B", + u"3104": u"SDT52832B", + u"3105": u"SDT64B", + u"3701": u"S5JS100", + u"3702": u"S3JT100", + u"3703": u"S1SBP6A", + u"4000": u"LPC11U35_Y5_MBUG", + u"4005": u"NRF51822_Y5_MBUG", + u"4100": u"MOTE_L152RC", + u"4337": u"LPC4337", + u"4500": u"DELTA_DFCM_NNN40", + u"4501": u"DELTA_DFBM_NQ620", + u"4502": u"DELTA_DFCM_NNN50", + u"4600": u"REALTEK_RTL8195AM", + u"5000": u"ARM_MPS2", + u"5001": u"ARM_IOTSS_BEID", + u"5002": u"ARM_BEETLE_SOC", + u"5003": u"ARM_MPS2_M0P", + u"5004": u"ARM_CM3DS_MPS2", + u"5005": u"ARM_MPS2_M0DS", + u"5006": u"ARM_MUSCA_A1", + u"5007": u"ARM_MUSCA_B1", + u"5009": u"ARM_MUSCA_S1", + u"5020": u"HOME_GATEWAY_6LOWPAN", + u"5500": u"RZ_A1H", + u"5501": u"GR_LYCHEE", + u"5502": u"GR_MANGO", + u"6000": u"FUTURE_SEQUANA", + u"6660": u"NZ32_SC151", + u"7011": u"TMPM066", + u"7012": u"TMPM3H6", + u"7013": u"TMPM46B", + u"7014": u"TMPM3HQ", + u"7015": u"TMPM4G9", + u"7020": u"TMPM4KN", + u"7402": u"MBED_BR_HAT", + u"7778": u"TEENSY3_1", + u"8001": u"UNO_91H", + u"8012": u"TT_M3HQ", + u"8013": u"TT_M4G9", + u"8080": u"FF1705_L151CC", + u"8081": u"FF_LPC546XX", + u"9001": u"LPC1347", + u"9002": u"LPC11U24", + u"9003": u"LPC1347", + u"9004": u"ARCH_PRO", + u"9006": u"LPC11U24", + u"9007": u"LPC11U35_501", + u"9008": u"XADOW_M0", + u"9009": u"ARCH_BLE", + u"9010": u"ARCH_GPRS", + u"9011": u"ARCH_MAX", + u"9012": u"SEEED_TINY_BLE", + u"9014": u"WIO_3G", + u"9015": u"WIO_BG96", + u"9017": u"WIO_EMW3166", + u"9020": u"UHURU_RAVEN", + u"9900": u"NRF51_MICROBIT", + u"C002": u"VK_RZ_A1H", + u"C005": u"MTM_MTCONNECT04S", + u"C006": u"VBLUNO51", + u"C008": u"SAKURAIO_EVB_01", + u"C030": u"UBLOX_C030_U201", + u"C031": u"UBLOX_C030_N211", + u"C032": u"UBLOX_C030_R404M", + u"C033": u"UBLOX_C030_R410M", + u"C034": u"UBLOX_C030_S200", + u"C035": u"UBLOX_C030_R3121", + u"C036": u"UBLOX_C030_R412M", + u"RIOT": u"RIOT", + }, + u"jlink": { + u"X729475D28G": { + u"platform_name": u"NRF51_DK", + u"jlink_device_name": u"nRF51422_xxAC", + }, + u"X349858SLYN": { + u"platform_name": u"NRF52_DK", + u"jlink_device_name": u"nRF52832_xxaa", + }, + u"FRDM-KL25Z": { + u"platform_name": u"KL25Z", + u"jlink_device_name": u"MKL25Z128xxx4", + }, + u"FRDM-KL27Z": { + u"platform_name": u"KL27Z", + u"jlink_device_name": u"MKL27Z64xxx4", + }, + u"FRDM-KL43Z": { + u"platform_name": u"KL43Z", + u"jlink_device_name": u"MKL43Z256xxx4", + }, + }, + u"atmel": {u"2241": "SAML21J18A"}, +} + + +def _get_modified_time(path): + try: + mtime = getmtime(path) + except OSError: + mtime = 0 + return datetime.datetime.fromtimestamp(mtime) + + +def _older_than_me(path): + return _get_modified_time(path) < _get_modified_time(__file__) + + +def _modify_data_format(data, verbose_data, simple_data_key="platform_name"): + if isinstance(data, dict): + if verbose_data: + return data + + return data[simple_data_key] + else: + if verbose_data: + return {simple_data_key: data} + + return data + + +def _overwrite_or_open(db): + try: + if db is LOCAL_PLATFORM_DATABASE and _older_than_me(db): + raise ValueError("Platform Database is out of date") + with open(db, encoding="utf-8") as db_in: + return json.load(db_in) + except (IOError, ValueError) as exc: + if db is LOCAL_PLATFORM_DATABASE: + logger.warning("Error loading database %s: %s; Recreating", db, str(exc)) + try: + makedirs(dirname(db)) + except OSError: + pass + try: + with open(db, "w", encoding="utf-8") as out: + out.write(unicode(json.dumps(DEFAULT_PLATFORM_DB))) + except IOError: + pass + return copy(DEFAULT_PLATFORM_DB) + else: + return {} + + +class PlatformDatabase(object): + """Represents a union of multiple platform database files. + Handles inter-process synchronization of database files. + """ + + target_id_pattern = re.compile(r"^[a-fA-F0-9]{4}$") + + def __init__(self, database_files, primary_database=None): + """Construct a PlatformDatabase object from a series of platform database + files + """ + self._prim_db = primary_database + if not self._prim_db and len(database_files) == 1: + self._prim_db = database_files[0] + self._dbs = OrderedDict() + self._keys = defaultdict(set) + for db in database_files: + new_db = _overwrite_or_open(db) + first_value = None + if new_db.values(): + first_value = next(iter(new_db.values())) + if not isinstance(first_value, dict): + new_db = {"daplink": new_db} + + if new_db: + for device_type in new_db: + duplicates = self._keys[device_type].intersection( + set(new_db[device_type].keys()) + ) + duplicates = set(["%s.%s" % (device_type, k) for k in duplicates]) + if duplicates: + logger.warning( + "Duplicate platform ids found: %s," + " ignoring the definitions from %s", + " ".join(duplicates), + db, + ) + self._dbs[db] = new_db + self._keys[device_type] = self._keys[device_type].union( + new_db[device_type].keys() + ) + else: + self._dbs[db] = new_db + + def items(self, device_type="daplink"): + for db in self._dbs.values(): + for entry in db.get(device_type, {}).items(): + yield entry + + def all_ids(self, device_type="daplink"): + return iter(self._keys[device_type]) + + def get(self, index, default=None, device_type="daplink", verbose_data=False): + """Standard lookup function. Works exactly like a dict. If 'verbose_data' + is True, all data for the platform is returned as a dict.""" + for db in self._dbs.values(): + if device_type in db: + maybe_answer = db[device_type].get(index, None) + if maybe_answer: + return _modify_data_format(maybe_answer, verbose_data) + + return default + + def _update_db(self): + if self._prim_db: + lock = InterProcessLock("%s.lock" % self._prim_db) + acquired = lock.acquire(blocking=False) + if not acquired: + logger.debug("Waiting 60 seconds for file lock") + acquired = lock.acquire(blocking=True, timeout=60) + if acquired: + try: + with open(self._prim_db, "w", encoding="utf-8") as out: + out.write(unicode(json.dumps(self._dbs[self._prim_db]))) + return True + finally: + lock.release() + else: + logger.error( + "Could not update platform database: " + "Lock acquire failed after 60 seconds" + ) + return False + else: + logger.error( + "Can't update platform database: destination database is ambiguous" + ) + return False + + def add(self, id, platform_name, permanent=False, device_type="daplink"): + """Add a platform to this database, optionally updating an origin + database + """ + if self.target_id_pattern.match(id): + if self._prim_db: + if device_type not in self._dbs[self._prim_db]: + self._dbs[self._prim_db][device_type] = {} + self._dbs[self._prim_db][device_type][id] = platform_name + else: + cur_db = next(iter(self._dbs.values())) + if device_type not in cur_db: + cur_db[device_type] = {} + cur_db[device_type][id] = platform_name + self._keys[device_type].add(id) + if permanent: + self._update_db() + else: + raise ValueError("Invald target id: %s" % id) + + def remove(self, id, permanent=False, device_type="daplink", verbose_data=False): + """Remove a platform from this database, optionally updating an origin + database. If 'verbose_data' is True, all data for the platform is returned + as a dict. + """ + logger.debug("Trying remove of %s", id) + if id == "*" and device_type in self._dbs[self._prim_db]: + self._dbs[self._prim_db][device_type] = {} + if permanent: + self._update_db() + else: + for db in self._dbs.values(): + if device_type in db and id in db[device_type]: + logger.debug("Removing id...") + removed = db[device_type][id] + del db[device_type][id] + self._keys[device_type].remove(id) + if permanent: + self._update_db() + + return _modify_data_format(removed, verbose_data) diff --git a/tools/python/mbed_os_tools/detect/windows.py b/tools/python/mbed_os_tools/detect/windows.py new file mode 100644 index 0000000000..fae8472a5e --- /dev/null +++ b/tools/python/mbed_os_tools/detect/windows.py @@ -0,0 +1,517 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +import sys +from collections import defaultdict +from copy import copy + +from .lstools_base import MbedLsToolsBase + +import logging + +logger = logging.getLogger("mbedls.lstools_win7") +logger.addHandler(logging.NullHandler()) +DEBUG = logging.DEBUG +del logging + +if sys.version_info[0] < 3: + import _winreg as winreg +else: + import winreg + + +MAX_COMPOSITE_DEVICE_SUBDEVICES = 8 +MBED_STORAGE_DEVICE_VENDOR_STRINGS = [ + "ven_mbed", + "ven_segger", + "ven_arm_v2m", + "ven_nxp", + "ven_atmel", +] + + +def _get_values_with_numeric_keys(reg_key): + result = [] + try: + for v in _iter_vals(reg_key): + try: + # The only values we care about are ones that have an integer key. + # The other values are metadata for the registry + int(v[0]) + result.append(v[1]) + except ValueError: + continue + except OSError: + logger.debug("Failed to iterate over all keys") + + return result + + +def _is_mbed_volume(volume_string): + for vendor_string in MBED_STORAGE_DEVICE_VENDOR_STRINGS: + if vendor_string.lower() in volume_string.lower(): + return True + + return False + + +def _get_cached_mounted_points(): + """! Get the volumes present on the system + @return List of mount points and their associated volume string + Ex. [{ 'mount_point': 'D:', 'volume_string': 'xxxx'}, ...] + """ + result = [] + try: + # Open the registry key for mounted devices + mounted_devices_key = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, "SYSTEM\\MountedDevices" + ) + for v in _iter_vals(mounted_devices_key): + # Valid entries have the following format: \\DosDevices\\D: + if "DosDevices" not in v[0]: + continue + + volume_string = v[1].decode("utf-16le", "ignore") + if not _is_mbed_volume(volume_string): + continue + + mount_point_match = re.match(".*\\\\(.:)$", v[0]) + + if not mount_point_match: + logger.debug("Invalid disk pattern for entry %s, skipping", v[0]) + continue + + mount_point = mount_point_match.group(1) + logger.debug( + "Mount point %s found for volume %s", mount_point, volume_string + ) + + result.append({"mount_point": mount_point, "volume_string": volume_string}) + except OSError: + logger.error('Failed to open "MountedDevices" in registry') + + return result + + +def _get_disks(): + logger.debug("Fetching mounted devices from disk service registry entry") + try: + disks_key = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum" + ) + disk_strings = _get_values_with_numeric_keys(disks_key) + return [v for v in disk_strings if _is_mbed_volume(v)] + except OSError: + logger.debug("No disk service found, no device can be detected") + return [] + + +def _get_usb_storage_devices(): + logger.debug("Fetching usb storage devices from USBSTOR service registry entry") + try: + usbstor_key = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + "SYSTEM\\CurrentControlSet\\Services\\USBSTOR\\Enum", + ) + return _get_values_with_numeric_keys(usbstor_key) + except OSError: + logger.debug("No USBSTOR service found, no device can be detected") + return [] + + +def _determine_valid_non_composite_devices(devices, target_id_usb_id_mount_point_map): + # Some Mbed devices do not expose a composite USB device. This is typical for + # DAPLink devices in bootloader mode. Since we only have to check one endpoint + # (specifically, the mass storage device), we handle this case separately + candidates = {} + for device in devices: + device_key_string = "SYSTEM\\CurrentControlSet\\Enum\\" + device["full_path"] + try: + device_key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, device_key_string) + except OSError: + logger.debug('Key "%s" not found', device_key_string) + continue + + try: + capability = _determine_subdevice_capability(device_key) + except CompatibleIDsNotFoundException: + logger.debug( + 'Expected %s to have subkey "CompatibleIDs". Skipping.', + device_key_string, + ) + continue + + if capability != "msd": + logger.debug( + "Expected msd device but got %s, skipping %s", + capability, + device["full_path"], + ) + continue + + target_id_usb_id = device["entry_key_string"] + try: + candidates[target_id_usb_id] = { + "target_id_usb_id": target_id_usb_id, + "mount_point": target_id_usb_id_mount_point_map[target_id_usb_id], + } + + candidates[target_id_usb_id].update( + _vid_pid_path_to_usb_info(device["vid_pid_path"]) + ) + except KeyError: + pass + + return candidates + + +def _determine_subdevice_capability(key): + try: + vals = winreg.QueryValueEx(key, "CompatibleIDs") + compatible_ids = [x.lower() for x in vals[0]] + except OSError: + raise CompatibleIDsNotFoundException() + + if "usb\\class_00" in compatible_ids or "usb\\devclass_00" in compatible_ids: + return "composite" + elif "usb\\class_08" in compatible_ids: + return "msd" + elif "usb\\class_02" in compatible_ids: + return "serial" + else: + logger.debug("Unknown capabilities from the following ids: %s", compatible_ids) + return None + + +def _vid_pid_path_to_usb_info(vid_pid_path): + """! Provide the vendor ID and product ID of a device based on its entry in the registry + @return Returns {'vendor_id': '', 'product': ''} + @details If the vendor ID or product ID can't be determined, they will be returned + as None. + """ + result = {"vendor_id": None, "product_id": None} + + for component in vid_pid_path.split("&"): + component_part = component.lower().split("_") + + if len(component_part) != 2: + logger.debug("Unexpected VID/PID string structure %s", component) + break + + if component_part[0] == "vid": + result["vendor_id"] = component_part[1] + elif component_part[0] == "pid": + result["product_id"] = component_part[1] + + return result + + +def _iter_keys_as_str(key): + """! Iterate over subkeys of a key returning subkey as string + """ + for i in range(winreg.QueryInfoKey(key)[0]): + yield winreg.EnumKey(key, i) + + +def _iter_keys(key): + """! Iterate over subkeys of a key + """ + for i in range(winreg.QueryInfoKey(key)[0]): + yield winreg.OpenKey(key, winreg.EnumKey(key, i)) + + +def _iter_vals(key): + """! Iterate over values of a key + """ + logger.debug("_iter_vals %r", key) + for i in range(winreg.QueryInfoKey(key)[1]): + yield winreg.EnumValue(key, i) + + +class CompatibleIDsNotFoundException(Exception): + pass + + +class MbedLsToolsWin7(MbedLsToolsBase): + """ mbed-enabled platform detection for Windows + """ + + def __init__(self, **kwargs): + MbedLsToolsBase.__init__(self, **kwargs) + self.os_supported.append("Windows7") + + def find_candidates(self): + cached_mount_points = _get_cached_mounted_points() + disks = _get_disks() + usb_storage_devices = _get_usb_storage_devices() + + target_id_usb_id_mount_point_map = {} + for cached_mount_point_info in cached_mount_points: + for index, disk in enumerate(copy(disks)): + match_string = disk.split("\\")[-1] + if match_string in cached_mount_point_info["volume_string"]: + # TargetID is a hex string with 10-48 chars + target_id_usb_id_match = re.search( + "[&#]([0-9A-Za-z]{10,48})[&#]", + cached_mount_point_info["volume_string"], + ) + if not target_id_usb_id_match: + logger.debug( + "Entry %s has invalid target id pattern %s, skipping", + cached_mount_point_info["mount_point"], + cached_mount_point_info["volume_string"], + ) + continue + + target_id_usb_id_mount_point_map[ + target_id_usb_id_match.group(1) + ] = cached_mount_point_info["mount_point"] + disks.pop(index) + break + + logger.debug( + "target_id_usb_id -> mount_point mapping: %s ", + target_id_usb_id_mount_point_map, + ) + non_composite_devices = [] + composite_devices = [] + for vid_pid_path in usb_storage_devices: + # Split paths like "USB\VID_0483&PID_374B&MI_01\7&25b4dc8e&0&0001" by "\" + vid_pid_path_componets = vid_pid_path.split("\\") + + vid_pid_components = vid_pid_path_componets[1].split("&") + + if len(vid_pid_components) != 2 and len(vid_pid_components) != 3: + logger.debug( + "Skipping USBSTOR device with unusual VID/PID string format '%s'", + vid_pid_path, + ) + continue + + device = { + "full_path": vid_pid_path, + "vid_pid_path": "&".join(vid_pid_components[:2]), + "entry_key_string": vid_pid_path_componets[2], + } + + # A composite device's vid/pid path always has a third component + if len(vid_pid_components) == 3: + composite_devices.append(device) + else: + non_composite_devices.append(device) + + candidates = defaultdict(dict) + candidates.update( + _determine_valid_non_composite_devices( + non_composite_devices, target_id_usb_id_mount_point_map + ) + ) + # Now we'll find all valid VID/PID and target ID combinations + target_id_usb_ids = set(target_id_usb_id_mount_point_map.keys()) - set( + candidates.keys() + ) + vid_pid_entry_key_string_map = defaultdict(set) + + for device in composite_devices: + vid_pid_entry_key_string_map[device["vid_pid_path"]].add( + device["entry_key_string"] + ) + + vid_pid_target_id_usb_id_map = defaultdict(dict) + usb_key_string = "SYSTEM\\CurrentControlSet\\Enum\\USB" + for vid_pid_path, entry_key_strings in vid_pid_entry_key_string_map.items(): + vid_pid_key_string = "%s\\%s" % (usb_key_string, vid_pid_path) + try: + vid_pid_key = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, vid_pid_key_string + ) + target_id_usb_id_sub_keys = set( + [k for k in _iter_keys_as_str(vid_pid_key)] + ) + except OSError: + logger.debug('VID/PID "%s" not found', vid_pid_key_string) + continue + + overlapping_target_id_usb_ids = target_id_usb_id_sub_keys.intersection( + set(target_id_usb_ids) + ) + for target_id_usb_id in overlapping_target_id_usb_ids: + composite_device_key_string = "%s\\%s" % ( + vid_pid_key_string, + target_id_usb_id, + ) + composite_device_key = winreg.OpenKey(vid_pid_key, target_id_usb_id) + + entry_key_string = target_id_usb_id + is_prefix = False + + try: + new_entry_key_string, _ = winreg.QueryValueEx( + composite_device_key, "ParentIdPrefix" + ) + + if any( + e.startswith(new_entry_key_string) for e in entry_key_strings + ): + logger.debug( + "Assigning new entry key string of %s to device %s, " + "as found in ParentIdPrefix", + new_entry_key_string, + target_id_usb_id, + ) + entry_key_string = new_entry_key_string + is_prefix = True + except OSError: + logger.debug( + 'Device %s did not have a "ParentIdPrefix" key, ' + "sticking with %s as entry key string", + composite_device_key_string, + target_id_usb_id, + ) + + vid_pid_target_id_usb_id_map[vid_pid_path][entry_key_string] = { + "target_id_usb_id": target_id_usb_id, + "is_prefix": is_prefix, + } + + for ( + vid_pid_path, + entry_key_string_target_id_usb_id_map, + ) in vid_pid_target_id_usb_id_map.items(): + for composite_device_subdevice_number in range( + MAX_COMPOSITE_DEVICE_SUBDEVICES + ): + subdevice_type_key_string = "%s\\%s&MI_0%d" % ( + usb_key_string, + vid_pid_path, + composite_device_subdevice_number, + ) + try: + subdevice_type_key = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, subdevice_type_key_string + ) + except OSError: + logger.debug( + "Composite device subdevice key %s was not found, skipping", + subdevice_type_key_string, + ) + continue + + for ( + entry_key_string, + entry_data, + ) in entry_key_string_target_id_usb_id_map.items(): + if entry_data["is_prefix"]: + prepared_entry_key_string = "%s&000%d" % ( + entry_key_string, + composite_device_subdevice_number, + ) + else: + prepared_entry_key_string = entry_key_string + subdevice_key_string = "%s\\%s" % ( + subdevice_type_key_string, + prepared_entry_key_string, + ) + try: + subdevice_key = winreg.OpenKey( + subdevice_type_key, prepared_entry_key_string + ) + except OSError: + logger.debug( + "Sub-device %s not found, skipping", subdevice_key_string + ) + continue + + try: + capability = _determine_subdevice_capability(subdevice_key) + except CompatibleIDsNotFoundException: + logger.debug( + 'Expected %s to have subkey "CompatibleIDs". Skipping.', + subdevice_key_string, + ) + continue + + if capability == "msd": + candidates[entry_data["target_id_usb_id"]][ + "mount_point" + ] = target_id_usb_id_mount_point_map[ + entry_data["target_id_usb_id"] + ] + candidates[entry_data["target_id_usb_id"]].update( + _vid_pid_path_to_usb_info(vid_pid_path) + ) + elif capability == "serial": + try: + device_parameters_key = winreg.OpenKey( + subdevice_key, "Device Parameters" + ) + except OSError: + logger.debug( + 'Key "Device Parameters" not under serial device entry' + ) + continue + + try: + candidates[entry_data["target_id_usb_id"]][ + "serial_port" + ], _ = winreg.QueryValueEx( + device_parameters_key, "PortName" + ) + candidates[entry_data["target_id_usb_id"]].update( + _vid_pid_path_to_usb_info(vid_pid_path) + ) + except OSError: + logger.debug( + '"PortName" value not found under serial device entry' + ) + continue + + final_candidates = [] + for target_id_usb_id, candidate in candidates.items(): + candidate["target_id_usb_id"] = target_id_usb_id + + if "serial_port" not in candidate: + candidate["serial_port"] = None + + if "mount_point" not in candidate: + candidate["mount_point"] = None + + final_candidates.append(candidate) + + return final_candidates + + def mount_point_ready(self, path): + """! Check if a mount point is ready for file operations + @return Returns True if the given path exists, False otherwise + @details Calling the Windows command `dir` instead of using the python + `os.path.exists`. The latter causes a Python error box to appear claiming + there is "No Disk" for some devices that are in the ejected state. Calling + `dir` prevents this since it uses the Windows API to determine if the + device is ready before accessing the file system. + """ + stdout, stderr, retcode = self._run_cli_process("dir %s" % path) + result = True if retcode == 0 else False + + if result: + logger.debug("Mount point %s is ready", path) + else: + logger.debug( + "Mount point %s reported not ready with error '%s'", + path, + stderr.strip(), + ) + + return result diff --git a/tools/python/mbed_os_tools/test/__init__.py b/tools/python/mbed_os_tools/test/__init__.py new file mode 100644 index 0000000000..eca1fec65d --- /dev/null +++ b/tools/python/mbed_os_tools/test/__init__.py @@ -0,0 +1,382 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + + +"""! @package mbed-host-tests + +Flash, reset and perform host supervised tests on mbed platforms. +Write your own programs (import this package) or use 'mbedhtrun' +command line tool instead. + +""" + +import imp +import sys +from optparse import OptionParser +from optparse import SUPPRESS_HELP +from . import host_tests_plugins +from .host_tests_registry import HostRegistry # noqa: F401 +from .host_tests import BaseHostTest, event_callback # noqa: F401 + +# Set the default baud rate +DEFAULT_BAUD_RATE = 9600 + +############################################################################### +# Functional interface for test supervisor registry +############################################################################### + + +def get_plugin_caps(methods=None): + if not methods: + methods = ["CopyMethod", "ResetMethod"] + result = {} + for method in methods: + result[method] = host_tests_plugins.get_plugin_caps(method) + return result + + +def init_host_test_cli_params(): + """! Function creates CLI parser object and returns populated options object. + @return Function returns 'options' object returned from OptionParser class + @details Options object later can be used to populate host test selector script. + """ + parser = OptionParser() + + parser.add_option( + "-m", + "--micro", + dest="micro", + help="Target microcontroller name", + metavar="MICRO", + ) + + parser.add_option( + "-p", "--port", dest="port", help="Serial port of the target", metavar="PORT" + ) + + parser.add_option( + "-d", + "--disk", + dest="disk", + help="Target disk (mount point) path", + metavar="DISK_PATH", + ) + + parser.add_option( + "-t", + "--target-id", + dest="target_id", + help="Unique Target Id or mbed platform", + metavar="TARGET_ID", + ) + + parser.add_option( + "", + "--sync", + dest="sync_behavior", + default=2, + type=int, + help=( + "Define how many times __sync packet will be sent to device: 0: " + "none; -1: forever; 1,2,3... - number of times (Default 2 time)" + ), + metavar="SYNC_BEHAVIOR", + ) + + parser.add_option( + "", + "--sync-timeout", + dest="sync_timeout", + default=5, + type=int, + help="Define delay in seconds between __sync packet (Default is 5 seconds)", + metavar="SYNC_TIMEOUT", + ) + + parser.add_option( + "-f", + "--image-path", + dest="image_path", + help="Path with target's binary image", + metavar="IMAGE_PATH", + ) + + copy_methods_str = "Plugin support: " + ", ".join( + host_tests_plugins.get_plugin_caps("CopyMethod") + ) + + parser.add_option( + "-c", + "--copy", + dest="copy_method", + help="Copy (flash the target) method selector. " + copy_methods_str, + metavar="COPY_METHOD", + ) + + parser.add_option( + "", + "--retry-copy", + dest="retry_copy", + default=3, + type=int, + help="Number of attempts to flash the target", + metavar="RETRY_COPY", + ) + + parser.add_option( + "", + "--tag-filters", + dest="tag_filters", + default="", + type=str, + help=( + "Comma seperated list of device tags used when allocating a target " + "to specify required hardware or attributes [--tag-filters tag1,tag2]" + ), + metavar="TAG_FILTERS", + ) + + reset_methods_str = "Plugin support: " + ", ".join( + host_tests_plugins.get_plugin_caps("ResetMethod") + ) + + parser.add_option( + "-r", + "--reset", + dest="forced_reset_type", + help="Forces different type of reset. " + reset_methods_str, + ) + + parser.add_option( + "-C", + "--program_cycle_s", + dest="program_cycle_s", + default=4, + help=( + "Program cycle sleep. Define how many seconds you want wait after " + "copying binary onto target (Default is 4 second)" + ), + type="float", + metavar="PROGRAM_CYCLE_S", + ) + + parser.add_option( + "-R", + "--reset-timeout", + dest="forced_reset_timeout", + default=1, + metavar="NUMBER", + type="float", + help=( + "When forcing a reset using option -r you can set up after reset " + "idle delay in seconds (Default is 1 second)" + ), + ) + + parser.add_option( + "--process-start-timeout", + dest="process_start_timeout", + default=60, + metavar="NUMBER", + type="float", + help=( + "This sets the maximum time in seconds to wait for an internal " + "process to start. This mostly only affects machines under heavy " + "load (Default is 60 seconds)" + ), + ) + + parser.add_option( + "-e", + "--enum-host-tests", + dest="enum_host_tests", + action="append", + default=["./test/host_tests"], + help="Define directory with local host tests", + ) + + parser.add_option( + "", + "--test-cfg", + dest="json_test_configuration", + help="Pass to host test class data about host test configuration", + ) + + parser.add_option( + "", + "--list", + dest="list_reg_hts", + default=False, + action="store_true", + help="Prints registered host test and exits", + ) + + parser.add_option( + "", + "--plugins", + dest="list_plugins", + default=False, + action="store_true", + help="Prints registered plugins and exits", + ) + + parser.add_option( + "-g", + "--grm", + dest="global_resource_mgr", + help=( + 'Global resource manager: ":' + '[:]", Ex. "module_name:10.2.123.43:3334", ' + 'module_name:https://example.com"' + ), + ) + + # Show --fm option only if "fm_agent" module installed + try: + imp.find_module("fm_agent") + except ImportError: + fm_help = SUPPRESS_HELP + else: + fm_help = ( + 'Fast Model connection, This option requires mbed-fastmodel-agent ' + 'module installed, list CONFIGs via "mbedfm"' + ) + parser.add_option( + "", + "--fm", + dest="fast_model_connection", + metavar="CONFIG", + default=None, + help=fm_help, + ) + + parser.add_option( + "", + "--run", + dest="run_binary", + default=False, + action="store_true", + help="Runs binary image on target (workflow: flash, reset, output console)", + ) + + parser.add_option( + "", + "--skip-flashing", + dest="skip_flashing", + default=False, + action="store_true", + help="Skips use of copy/flash plugin. Note: target will not be reflashed", + ) + + parser.add_option( + "", + "--skip-reset", + dest="skip_reset", + default=False, + action="store_true", + help="Skips use of reset plugin. Note: target will not be reset", + ) + + parser.add_option( + "-P", + "--polling-timeout", + dest="polling_timeout", + default=60, + metavar="NUMBER", + type="int", + help=( + "Timeout in sec for readiness of mount point and serial port of " + "local or remote device. Default 60 sec" + ), + ) + + parser.add_option( + "-b", + "--send-break", + dest="send_break_cmd", + default=False, + action="store_true", + help=( + "Send reset signal to board on specified port (-p PORT) and print " + "serial output. You can combine this with (-r RESET_TYPE) switch" + ), + ) + + parser.add_option( + "", + "--baud-rate", + dest="baud_rate", + help=( + "Baud rate of target, overrides values from mbed-ls, disk/mount " + "point (-d, --disk-path), and serial port -p :" + ), + metavar="BAUD_RATE", + ) + + parser.add_option( + "-v", + "--verbose", + dest="verbose", + default=False, + action="store_true", + help="More verbose mode", + ) + + parser.add_option( + "", + "--serial-output-file", + dest="serial_output_file", + default=None, + help="Save target serial output to this file.", + ) + + parser.add_option( + "", + "--compare-log", + dest="compare_log", + default=None, + help="Log file to compare with the serial output from target.", + ) + + parser.add_option( + "", + "--version", + dest="version", + default=False, + action="store_true", + help="Prints package version and exits", + ) + + parser.add_option( + "", + "--format", + dest="format", + help="Image file format passed to pyocd (elf, bin, hex, axf...).", + ) + + parser.description = ( + """Flash, reset and perform host supervised tests on mbed platforms""" + ) + parser.epilog = ( + """Example: mbedhtrun -d E: -p COM5 -f "test.bin" -C 4 -c shell -m K64F""" + ) + + (options, _) = parser.parse_args() + + if len(sys.argv) == 1: + parser.print_help() + sys.exit() + + return options diff --git a/tools/python/mbed_os_tools/test/__main__.py b/tools/python/mbed_os_tools/test/__main__.py new file mode 100644 index 0000000000..83e0eb5744 --- /dev/null +++ b/tools/python/mbed_os_tools/test/__main__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from .mbed_greentea_cli import main +main() diff --git a/tools/python/mbed_os_tools/test/cmake_handlers.py b/tools/python/mbed_os_tools/test/cmake_handlers.py new file mode 100644 index 0000000000..b4c15ac5a2 --- /dev/null +++ b/tools/python/mbed_os_tools/test/cmake_handlers.py @@ -0,0 +1,144 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +import os +import os.path + +from .mbed_greentea_log import gt_logger + + +def load_ctest_testsuite(link_target, binary_type=".bin", verbose=False): + """! Loads CMake.CTest formatted data about tests from test directory + @return Dictionary of { test_case : test_case_path } pairs + """ + result = {} + if link_target is not None: + ctest_path = os.path.join(link_target, "test", "CTestTestfile.cmake") + try: + with open(ctest_path) as ctest_file: + for line in ctest_file: + line_parse = parse_ctesttestfile_line( + link_target, binary_type, line, verbose=verbose + ) + if line_parse: + test_case, test_case_path = line_parse + result[test_case] = test_case_path + except: # noqa: E722 + pass # Return empty list if path is not found + return result + + +def parse_ctesttestfile_line(link_target, binary_type, line, verbose=False): + """! Parse lines of CTestTestFile.cmake file and searches for 'add_test' + @return Dictionary of { test_case : test_case_path } pairs or None if + failed to parse 'add_test' line + @details Example path with CTestTestFile.cmake: + c:/temp/xxx/mbed-sdk-private/build/frdm-k64f-gcc/test/ + + Example format of CTestTestFile.cmake: + # CMake generated Testfile for + # Source directory: c:/temp/xxx/mbed-sdk-private/build/frdm-k64f-gcc/test + # Build directory: c:/temp/xxx/mbed-sdk-private/build/frdm-k64f-gcc/test + # + # This file includes the relevant testing commands required for + # testing this directory and lists subdirectories to be tested as well. + add_test(mbed-test-stdio "mbed-test-stdio") + add_test(mbed-test-call_before_main "mbed-test-call_before_main") + add_test(mbed-test-dev_null "mbed-test-dev_null") + add_test(mbed-test-div "mbed-test-div") + add_test(mbed-test-echo "mbed-test-echo") + add_test(mbed-test-ticker "mbed-test-ticker") + add_test(mbed-test-hello "mbed-test-hello") + """ + add_test_pattern = r'[adtesADTES_]{8}\([\w\d_-]+ \"([\w\d_-]+)\"' + re_ptrn = re.compile(add_test_pattern) + if line.lower().startswith("add_test"): + m = re_ptrn.search(line) + if m and len(m.groups()) > 0: + if verbose: + print(m.group(1) + binary_type) + test_case = m.group(1) + test_case_path = os.path.join(link_target, "test", m.group(1) + binary_type) + return test_case, test_case_path + return None + + +def list_binaries_for_targets(build_dir="./build", verbose_footer=False): + """! Prints tests in target directories, only if tests exist. + @param build_dir Yotta default build directory where tests will be + @param verbose_footer Prints additional "how to use" Greentea footer + @details Skips empty / no tests for target directories. + """ + dir = build_dir + sub_dirs = ( + [ + os.path.join(dir, o) + for o in os.listdir(dir) + if os.path.isdir(os.path.join(dir, o)) + ] + if os.path.exists(dir) + else [] + ) + + def count_tests(): + result = 0 + for sub_dir in sub_dirs: + test_list = load_ctest_testsuite(sub_dir, binary_type="") + if len(test_list): + for test in test_list: + result += 1 + return result + + if count_tests(): + for sub_dir in sub_dirs: + target_name = sub_dir.split(os.sep)[-1] + gt_logger.gt_log( + "available tests for target '%s', location '%s'" + % (target_name, os.path.abspath(os.path.join(build_dir, sub_dir))) + ) + test_list = load_ctest_testsuite(sub_dir, binary_type="") + if len(test_list): + for test in sorted(test_list): + gt_logger.gt_log_tab("test '%s'" % test) + else: + gt_logger.gt_log_warn("no tests found in current location") + + if verbose_footer: + print( + "\nExample: execute 'mbedgt -t TARGET_NAME -n TEST_NAME' to run " + "test TEST_NAME for target TARGET_NAME" + ) + + +def list_binaries_for_builds(test_spec, verbose_footer=False): + """! Parse test spec and list binaries (BOOTABLE) in lexicographical order + @param test_spec Test specification object + @param verbose_footer Prints additional "how to use" Greentea footer + """ + test_builds = test_spec.get_test_builds() + for tb in test_builds: + gt_logger.gt_log( + "available tests for build '%s', location '%s'" + % (tb.get_name(), tb.get_path()) + ) + for tc in sorted(tb.get_tests().keys()): + gt_logger.gt_log_tab("test '%s'" % tc) + + if verbose_footer: + print( + "\nExample: execute 'mbedgt -t BUILD_NAME -n TEST_NAME' to run test " + "TEST_NAME for build TARGET_NAME in current test specification" + ) diff --git a/tools/python/mbed_os_tools/test/host_tests/__init__.py b/tools/python/mbed_os_tools/test/host_tests/__init__.py new file mode 100644 index 0000000000..7be6faa1a6 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +# base host test class +from .base_host_test import BaseHostTest, event_callback diff --git a/tools/python/mbed_os_tools/test/host_tests/base_host_test.py b/tools/python/mbed_os_tools/test/host_tests/base_host_test.py new file mode 100644 index 0000000000..b512196e06 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests/base_host_test.py @@ -0,0 +1,269 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 inspect +import six +from time import time +from inspect import isfunction, ismethod + + +class BaseHostTestAbstract(object): + """ Base class for each host-test test cases with standard + setup, test and teardown set of functions + """ + + name = '' # name of the host test (used for local registration) + __event_queue = None # To main even loop + __dut_event_queue = None # To DUT + script_location = None # Path to source file used to load host test + __config = {} + + def __notify_prn(self, text): + if self.__event_queue: + self.__event_queue.put(('__notify_prn', text, time())) + + def __notify_conn_lost(self, text): + if self.__event_queue: + self.__event_queue.put(('__notify_conn_lost', text, time())) + + def __notify_sync_failed(self, text): + if self.__event_queue: + self.__event_queue.put(('__notify_sync_failed', text, time())) + + def __notify_dut(self, key, value): + """! Send data over serial to DUT """ + if self.__dut_event_queue: + self.__dut_event_queue.put((key, value, time())) + + def notify_complete(self, result=None): + """! Notify main even loop that host test finished processing + @param result True for success, False failure. If None - no action in main even loop + """ + if self.__event_queue: + self.__event_queue.put(('__notify_complete', result, time())) + + def reset_dut(self, value): + """ + Reset device under test + :return: + """ + if self.__event_queue: + self.__event_queue.put(('__reset_dut', value, time())) + + def reset(self): + """ + Reset the device under test and continue running the host test + :return: + """ + if self.__event_queue: + self.__event_queue.put(("__reset", "0", time())) + + def notify_conn_lost(self, text): + """! Notify main even loop that there was a DUT-host test connection error + @param consume If True htrun will process (consume) all remaining events + """ + self.__notify_conn_lost(text) + + def log(self, text): + """! Send log message to main event loop """ + self.__notify_prn(text) + + def send_kv(self, key, value): + """! Send Key-Value data to DUT """ + self.__notify_dut(key, value) + + def setup_communication(self, event_queue, dut_event_queue, config={}): + """! Setup queues used for IPC """ + self.__event_queue = event_queue # To main even loop + self.__dut_event_queue = dut_event_queue # To DUT + self.__config = config + + def get_config_item(self, name): + """ + Return test config + + :param name: + :return: + """ + return self.__config.get(name, None) + + def setup(self): + """! Setup your tests and callbacks """ + raise NotImplementedError + + def result(self): + """! Returns host test result (True, False or None) """ + raise NotImplementedError + + def teardown(self): + """! Blocking always guaranteed test teardown """ + raise NotImplementedError + + +def event_callback(key): + """ + Decorator for defining a event callback method. Adds a property attribute "event_key" with value as the passed key. + + :param key: + :return: + """ + def decorator(func): + func.event_key = key + return func + return decorator + + +class HostTestCallbackBase(BaseHostTestAbstract): + + def __init__(self): + BaseHostTestAbstract.__init__(self) + self.__callbacks = {} + self.__restricted_callbacks = [ + '__coverage_start', + '__testcase_start', + '__testcase_finish', + '__testcase_summary', + '__exit', + '__exit_event_queue' + ] + + self.__consume_by_default = [ + '__coverage_start', + '__testcase_start', + '__testcase_finish', + '__testcase_count', + '__testcase_name', + '__testcase_summary', + '__rxd_line', + ] + + self.__assign_default_callbacks() + self.__assign_decorated_callbacks() + + def __callback_default(self, key, value, timestamp): + """! Default callback """ + #self.log("CALLBACK: key=%s, value=%s, timestamp=%f"% (key, value, timestamp)) + pass + + def __default_end_callback(self, key, value, timestamp): + """ + Default handler for event 'end' that gives test result from target. + This callback is not decorated as we don't know then in what order this + callback would be registered. We want to let users over write this callback. + Hence it should be registered before registering user defined callbacks. + + :param key: + :param value: + :param timestamp: + :return: + """ + self.notify_complete(value == 'success') + + def __assign_default_callbacks(self): + """! Assigns default callback handlers """ + for key in self.__consume_by_default: + self.__callbacks[key] = self.__callback_default + # Register default handler for event 'end' before assigning user defined callbacks to let users over write it. + self.register_callback('end', self.__default_end_callback) + + def __assign_decorated_callbacks(self): + """ + It looks for any callback methods decorated with @event_callback + + Example: + Define a method with @event_callback decorator like: + + @event_callback('') + def event_handler(self, key, value, timestamp): + do something.. + + :return: + """ + for name, method in inspect.getmembers(self, inspect.ismethod): + key = getattr(method, 'event_key', None) + if key: + self.register_callback(key, method) + + def register_callback(self, key, callback, force=False): + """! Register callback for a specific event (key: event name) + @param key String with name of the event + @param callback Callable which will be registstered for event "key" + @param force God mode + """ + + # Non-string keys are not allowed + if type(key) is not str: + raise TypeError("event non-string keys are not allowed") + + # And finally callback should be callable + if not callable(callback): + raise TypeError("event callback should be callable") + + # Check if callback has all three required parameters (key, value, timestamp) + # When callback is class method should have 4 arguments (self, key, value, timestamp) + if ismethod(callback): + arg_count = six.get_function_code(callback).co_argcount + if arg_count != 4: + err_msg = "callback 'self.%s('%s', ...)' defined with %d arguments"% (callback.__name__, key, arg_count) + err_msg += ", should have 4 arguments: self.%s(self, key, value, timestamp)"% callback.__name__ + raise TypeError(err_msg) + + # When callback is just a function should have 3 arguments func(key, value, timestamp) + if isfunction(callback): + arg_count = six.get_function_code(callback).co_argcount + if arg_count != 3: + err_msg = "callback '%s('%s', ...)' defined with %d arguments"% (callback.__name__, key, arg_count) + err_msg += ", should have 3 arguments: %s(key, value, timestamp)"% callback.__name__ + raise TypeError(err_msg) + + if not force: + # Event starting with '__' are reserved + if key.startswith('__'): + raise ValueError("event key starting with '__' are reserved") + + # We predefined few callbacks you can't use + if key in self.__restricted_callbacks: + raise ValueError("we predefined few callbacks you can't use e.g. '%s'"% key) + + self.__callbacks[key] = callback + + def get_callbacks(self): + return self.__callbacks + + def setup(self): + pass + + def result(self): + pass + + def teardown(self): + pass + + +class BaseHostTest(HostTestCallbackBase): + + __BaseHostTest_Called = False + + def base_host_test_inited(self): + """ This function will check if BaseHostTest ctor was called + Call to BaseHostTest is required in order to force required + interfaces implementation. + @return Returns True if ctor was called (ok behaviour) + """ + return self.__BaseHostTest_Called + + def __init__(self): + HostTestCallbackBase.__init__(self) + self.__BaseHostTest_Called = True diff --git a/tools/python/mbed_os_tools/test/host_tests/default_auto.py b/tools/python/mbed_os_tools/test/host_tests/default_auto.py new file mode 100644 index 0000000000..2b3781a993 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests/default_auto.py @@ -0,0 +1,24 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + + +from .. import BaseHostTest + + +class DefaultAuto(BaseHostTest): + """ Simple, basic host test's test runner waiting for serial port + output from MUT, no supervision over test running in MUT is executed. + """ + pass diff --git a/tools/python/mbed_os_tools/test/host_tests/detect_auto.py b/tools/python/mbed_os_tools/test/host_tests/detect_auto.py new file mode 100644 index 0000000000..4c998cb935 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests/detect_auto.py @@ -0,0 +1,57 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +from .. import BaseHostTest + +class DetectPlatformTest(BaseHostTest): + PATTERN_MICRO_NAME = "Target '(\w+)'" + re_detect_micro_name = re.compile(PATTERN_MICRO_NAME) + + def result(self): + raise NotImplementedError + + def test(self, selftest): + result = True + + c = selftest.mbed.serial_readline() # {{start}} preamble + if c is None: + return selftest.RESULT_IO_SERIAL + + selftest.notify(c.strip()) + selftest.notify("HOST: Detecting target name...") + + c = selftest.mbed.serial_readline() + if c is None: + return selftest.RESULT_IO_SERIAL + selftest.notify(c.strip()) + + # Check for target name + m = self.re_detect_micro_name.search(c) + if m and len(m.groups()): + micro_name = m.groups()[0] + micro_cmp = selftest.mbed.options.micro == micro_name + result = result and micro_cmp + selftest.notify("HOST: MUT Target name '%s', expected '%s'... [%s]"% (micro_name, + selftest.mbed.options.micro, + "OK" if micro_cmp else "FAIL")) + + for i in range(0, 2): + c = selftest.mbed.serial_readline() + if c is None: + return selftest.RESULT_IO_SERIAL + selftest.notify(c.strip()) + + return selftest.RESULT_SUCCESS if result else selftest.RESULT_FAILURE diff --git a/tools/python/mbed_os_tools/test/host_tests/dev_null_auto.py b/tools/python/mbed_os_tools/test/host_tests/dev_null_auto.py new file mode 100644 index 0000000000..6b173fdb37 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests/dev_null_auto.py @@ -0,0 +1,36 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from .. import BaseHostTest + +class DevNullTest(BaseHostTest): + + __result = None + + def _callback_result(self, key, value, timestamp): + # We should not see result data in this test + self.__result = False + + def _callback_to_stdout(self, key, value, timestamp): + self.__result = True + self.log("_callback_to_stdout !") + + def setup(self): + self.register_callback("end", self._callback_result) + self.register_callback("to_null", self._callback_result) + self.register_callback("to_stdout", self._callback_to_stdout) + + def result(self): + return self.__result diff --git a/tools/python/mbed_os_tools/test/host_tests/echo.py b/tools/python/mbed_os_tools/test/host_tests/echo.py new file mode 100644 index 0000000000..81245349c3 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests/echo.py @@ -0,0 +1,55 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 uuid +from .. import BaseHostTest + +class EchoTest(BaseHostTest): + + __result = None + echo_count = 0 + count = 0 + uuid_sent = [] + uuid_recv = [] + + def __send_echo_uuid(self): + if self.echo_count: + str_uuid = str(uuid.uuid4()) + self.send_kv("echo", str_uuid) + self.uuid_sent.append(str_uuid) + self.echo_count -= 1 + + def _callback_echo(self, key, value, timestamp): + self.uuid_recv.append(value) + self.__send_echo_uuid() + + def _callback_echo_count(self, key, value, timestamp): + # Handshake + self.echo_count = int(value) + self.send_kv(key, value) + # Send first echo to echo server on DUT + self.__send_echo_uuid() + + def setup(self): + self.register_callback("echo", self._callback_echo) + self.register_callback("echo_count", self._callback_echo_count) + + def result(self): + self.__result = self.uuid_sent == self.uuid_recv + return self.__result + + def teardown(self): + pass diff --git a/tools/python/mbed_os_tools/test/host_tests/hello_auto.py b/tools/python/mbed_os_tools/test/host_tests/hello_auto.py new file mode 100644 index 0000000000..587bcbc9e0 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests/hello_auto.py @@ -0,0 +1,34 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from .. import BaseHostTest + +class HelloTest(BaseHostTest): + HELLO_WORLD = "Hello World" + + __result = None + + def _callback_hello_world(self, key, value, timestamp): + self.__result = value == self.HELLO_WORLD + self.notify_complete() + + def setup(self): + self.register_callback("hello_world", self._callback_hello_world) + + def result(self): + return self.__result + + def teardown(self): + pass diff --git a/tools/python/mbed_os_tools/test/host_tests/rtc_auto.py b/tools/python/mbed_os_tools/test/host_tests/rtc_auto.py new file mode 100644 index 0000000000..8cf2848b58 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests/rtc_auto.py @@ -0,0 +1,56 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +from time import time, strftime, gmtime +from .. import BaseHostTest + +class RTCTest(BaseHostTest): + PATTERN_RTC_VALUE = "\[(\d+)\] \[(\d+-\d+-\d+ \d+:\d+:\d+ [AaPpMm]{2})\]" + re_detect_rtc_value = re.compile(PATTERN_RTC_VALUE) + + __result = None + timestamp = None + rtc_reads = [] + + def _callback_timestamp(self, key, value, timestamp): + self.timestamp = int(value) + + def _callback_rtc(self, key, value, timestamp): + self.rtc_reads.append((key, value, timestamp)) + + def _callback_end(self, key, value, timestamp): + self.notify_complete() + + def setup(self): + self.register_callback('timestamp', self._callback_timestamp) + self.register_callback('rtc', self._callback_rtc) + self.register_callback('end', self._callback_end) + + def result(self): + def check_strftimes_format(t): + m = self.re_detect_rtc_value.search(t) + if m and len(m.groups()): + sec, time_str = int(m.groups()[0]), m.groups()[1] + correct_time_str = strftime("%Y-%m-%d %H:%M:%S", gmtime(float(sec))) + return time_str == correct_time_str + return False + + ts = [t for _, t, _ in self.rtc_reads] + self.__result = all(filter(check_strftimes_format, ts)) + return self.__result + + def teardown(self): + pass diff --git a/tools/python/mbed_os_tools/test/host_tests/wait_us_auto.py b/tools/python/mbed_os_tools/test/host_tests/wait_us_auto.py new file mode 100644 index 0000000000..80eef24022 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests/wait_us_auto.py @@ -0,0 +1,60 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from time import time +from .. import BaseHostTest + + +class WaitusTest(BaseHostTest): + """ This test is reading single characters from stdio + and measures time between their occurrences. + """ + __result = None + DEVIATION = 0.10 # +/-10% + ticks = [] + + def _callback_exit(self, key, value, timeout): + self.notify_complete() + + def _callback_tick(self, key, value, timestamp): + """ {{tick;%d}}} """ + self.log("tick! " + str(timestamp)) + self.ticks.append((key, value, timestamp)) + + def setup(self): + self.register_callback('exit', self._callback_exit) + self.register_callback('tick', self._callback_tick) + + def result(self): + def sub_timestamps(t1, t2): + delta = t1 - t2 + deviation = abs(delta - 1.0) + #return True if delta > 0 and deviation <= self.DEVIATION else False + return deviation <= self.DEVIATION + + # Check if time between ticks was accurate + if self.ticks: + # If any ticks were recorded + timestamps = [timestamp for _, _, timestamp in self.ticks] + self.log(str(timestamps)) + m = map(sub_timestamps, timestamps[1:], timestamps[:-1]) + self.log(str(m)) + self.__result = all(m) + else: + self.__result = False + return self.__result + + def teardown(self): + pass diff --git a/tools/python/mbed_os_tools/test/host_tests_conn_proxy/__init__.py b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/__init__.py new file mode 100644 index 0000000000..7e40d72aa3 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from .conn_proxy import conn_process diff --git a/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive.py b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive.py new file mode 100644 index 0000000000..7513596e83 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive.py @@ -0,0 +1,87 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from ..host_tests_logger import HtrunLogger + + +class ConnectorPrimitiveException(Exception): + """ + Exception in connector primitive module. + """ + pass + + +class ConnectorPrimitive(object): + + def __init__(self, name): + self.LAST_ERROR = None + self.logger = HtrunLogger(name) + self.polling_timeout = 60 + + def write_kv(self, key, value): + """! Forms and sends Key-Value protocol message. + @details On how to parse K-V sent from DUT see KiViBufferWalker::KIVI_REGEX + On how DUT sends K-V please see greentea_write_postamble() function in greentea-client + @return Returns buffer with K-V message sent to DUT on success, None on failure + """ + # All Key-Value messages ends with newline character + kv_buff = "{{%s;%s}}"% (key, value) + '\n' + + if self.write(kv_buff): + self.logger.prn_txd(kv_buff.rstrip()) + return kv_buff + else: + return None + + def read(self, count): + """! Read data from DUT + @param count Number of bytes to read + @return Bytes read + """ + raise NotImplementedError + + def write(self, payload, log=False): + """! Read data from DUT + @param payload Buffer with data to send + @param log Set to True if you want to enable logging for this function + @return Payload (what was actually sent - if possible to establish that) + """ + raise NotImplementedError + + def flush(self): + """! Flush read/write channels of DUT """ + raise NotImplementedError + + def reset(self): + """! Reset the dut + """ + raise NotImplementedError + + def connected(self): + """! Check if there is a connection to DUT + @return True if there is conenction to DUT (read/write/flush API works) + """ + raise NotImplementedError + + def error(self): + """! Returns LAST_ERROR value + @return Value of self.LAST_ERROR + """ + return self.LAST_ERROR + + def finish(self): + """! Handle DUT dtor like (close resource) operations here + """ + raise NotImplementedError diff --git a/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive_fastmodel.py b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive_fastmodel.py new file mode 100644 index 0000000000..a198de83f6 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive_fastmodel.py @@ -0,0 +1,160 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 telnetlib +import socket +from .conn_primitive import ConnectorPrimitive, ConnectorPrimitiveException + + +class FastmodelConnectorPrimitive(ConnectorPrimitive): + def __init__(self, name, config): + ConnectorPrimitive.__init__(self, name) + self.config = config + self.fm_config = config.get('fm_config', None) + self.platform_name = config.get('platform_name', None) + self.image_path = config.get('image_path', None) + self.polling_timeout = int(config.get('polling_timeout', 60)) + + # FastModel Agent tool-kit + self.fm_agent_module = None + self.resource = None + + # Initialize FastModel + if self.__fastmodel_init(): + + # FastModel Launch load and run, equivalent to DUT connection, flashing and reset... + self.__fastmodel_launch() + self.__fastmodel_load(self.image_path) + self.__fastmodel_run() + + + def __fastmodel_init(self): + """! Initialize models using fm_agent APIs """ + self.logger.prn_inf("Initializing FastModel...") + + try: + self.fm_agent_module = __import__("fm_agent") + except ImportError as e: + self.logger.prn_err("unable to load mbed-fastmodel-agent module. Check if the module install correctly.") + self.fm_agent_module = None + self.logger.prn_err("Importing failed : %s" % str(e)) + raise ConnectorPrimitiveException("Importing failed : %s" % str(e)) + try: + self.resource = self.fm_agent_module.FastmodelAgent(logger=self.logger) + self.resource.setup_simulator(self.platform_name,self.fm_config) + if self.__resource_allocated(): + pass + except self.fm_agent_module.SimulatorError as e: + self.logger.prn_err("module fm_agent, create() failed: %s"% str(e)) + raise ConnectorPrimitiveException("FastModel Initializing failed as throw SimulatorError!") + + return True + + def __fastmodel_launch(self): + """! launch the FastModel""" + self.logger.prn_inf("Launching FastModel...") + try: + if not self.resource.start_simulator(): + raise ConnectorPrimitiveException("FastModel running failed, run_simulator() return False!") + except self.fm_agent_module.SimulatorError as e: + self.logger.prn_err("start_simulator() failed: %s"% str(e)) + raise ConnectorPrimitiveException("FastModel launching failed as throw FastModelError!") + + def __fastmodel_run(self): + """! Use fm_agent API to run the FastModel """ + self.logger.prn_inf("Running FastModel...") + try: + if not self.resource.run_simulator(): + raise ConnectorPrimitiveException("FastModel running failed, run_simulator() return False!") + except self.fm_agent_module.SimulatorError as e: + self.logger.prn_err("run_simulator() failed: %s"% str(e)) + raise ConnectorPrimitiveException("FastModel running failed as throw SimulatorError!") + + def __fastmodel_load(self, filename): + """! Use fm_agent API to load image to FastModel, this is functional equivalent to flashing DUT""" + self.logger.prn_inf("loading FastModel with image '%s'..."% filename) + try: + if not self.resource.load_simulator(filename): + raise ConnectorPrimitiveException("FastModel loading failed, load_simulator() return False!") + except self.fm_agent_module.SimulatorError as e: + self.logger.prn_err("run_simulator() failed: %s"% str(e)) + raise ConnectorPrimitiveException("FastModel loading failed as throw SimulatorError!") + + def __resource_allocated(self): + """! Check whether FastModel resource been allocated + @return True or throw an exception + """ + if self.resource: + return True + else: + self.logger.prn_err("FastModel resource not available!") + return False + + def read(self, count): + """! Read data from DUT, count is not used for FastModel""" + date = str() + if self.__resource_allocated(): + try: + data = self.resource.read() + except self.fm_agent_module.SimulatorError as e: + self.logger.prn_err("FastmodelConnectorPrimitive.read() failed: %s"% str(e)) + else: + return data + else: + return False + def write(self, payload, log=False): + """! Write 'payload' to DUT""" + if self.__resource_allocated(): + if log: + self.logger.prn_txd(payload) + try: + self.resource.write(payload) + except self.fm_agent_module.SimulatorError as e: + self.logger.prn_err("FastmodelConnectorPrimitive.write() failed: %s"% str(e)) + else: + return True + else: + return False + + def flush(self): + """! flush not supported in FastModel_module""" + pass + + def connected(self): + """! return whether FastModel is connected """ + if self.__resource_allocated(): + return self.resource.is_simulator_alive + else: + return False + + def finish(self): + """! shutdown the FastModel and release the allocation """ + if self.__resource_allocated(): + try: + self.resource.shutdown_simulator() + self.resource = None + except self.fm_agent_module.SimulatorError as e: + self.logger.prn_err("FastmodelConnectorPrimitive.finish() failed: %s"% str(e)) + + def reset(self): + if self.__resource_allocated(): + try: + if not self.resource.reset_simulator(): + self.logger.prn_err("FastModel reset failed, reset_simulator() return False!") + except self.fm_agent_module.SimulatorError as e: + self.logger.prn_err("FastmodelConnectorPrimitive.reset() failed: %s"% str(e)) + + def __del__(self): + self.finish() diff --git a/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive_remote.py b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive_remote.py new file mode 100644 index 0000000000..2396c8e6cf --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive_remote.py @@ -0,0 +1,197 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 time +from .. import DEFAULT_BAUD_RATE +from .conn_primitive import ConnectorPrimitive + + +class RemoteConnectorPrimitive(ConnectorPrimitive): + def __init__(self, name, config, importer=__import__): + ConnectorPrimitive.__init__(self, name) + self.config = config + self.target_id = self.config.get('target_id', None) + self.grm_host = config.get('grm_host', None) + self.grm_port = config.get('grm_port', None) + if self.grm_port: + self.grm_port = int(self.grm_port) + self.grm_module = config.get('grm_module', 'unknown') + self.platform_name = config.get('platform_name', None) + self.baudrate = config.get('baudrate', DEFAULT_BAUD_RATE) + self.image_path = config.get('image_path', None) + self.forced_reset_timeout = config.get('forced_reset_timeout', 0) + self.allocate_requirements = { + "platform_name": self.platform_name, + "power_on": True, + "connected": True + } + + if self.config.get("tags"): + self.allocate_requirements["tags"] = {} + for tag in config["tags"].split(','): + self.allocate_requirements["tags"][tag] = True + + # Global Resource Mgr tool-kit + self.remote_module = None + self.selected_resource = None + self.client = None + + # Initialize remote resource manager + self.__remote_init(importer) + + def __remote_init(self, importer): + """! Initialize DUT using GRM APIs """ + + # We want to load global resource manager module by name from command line (switch --grm) + try: + self.remote_module = importer(self.grm_module) + except ImportError as error: + self.logger.prn_err("unable to load global resource manager '%s' module!" % self.grm_module) + self.logger.prn_err(str(error)) + self.remote_module = None + return False + + self.logger.prn_inf("remote resources initialization: remote(host=%s, port=%s)" % + (self.grm_host, self.grm_port)) + + # Connect to remote global resource manager + self.client = self.remote_module.create(host=self.grm_host, port=self.grm_port) + + # First get the resources + resources = self.client.get_resources() + self.logger.prn_inf("remote resources count: %d" % len(resources)) + + # Query for available resource + # Automatic selection and allocation of a resource + try: + self.selected_resource = self.client.allocate(self.allocate_requirements) + except Exception as error: + self.logger.prn_err("can't allocate resource: '%s', reason: %s" % (self.platform_name, str(error))) + return False + + # Remote DUT connection, flashing and reset... + try: + self.__remote_flashing(self.image_path, forceflash=True) + self.__remote_connect(baudrate=self.baudrate) + self.__remote_reset(delay=self.forced_reset_timeout) + except Exception as error: + self.logger.prn_err(str(error)) + self.__remote_release() + return False + return True + + def __remote_connect(self, baudrate=DEFAULT_BAUD_RATE): + """! Open remote connection to DUT """ + self.logger.prn_inf("opening connection to platform at baudrate='%s'" % baudrate) + if not self.selected_resource: + raise Exception("remote resource not exists!") + try: + serial_parameters = self.remote_module.SerialParameters(baudrate=baudrate) + self.selected_resource.open_connection(parameters=serial_parameters) + except Exception: + self.logger.prn_inf("open_connection() failed") + raise + + def __remote_disconnect(self): + if not self.selected_resource: + raise Exception("remote resource not exists!") + try: + if self.connected(): + self.selected_resource.close_connection() + except Exception as error: + self.logger.prn_err("RemoteConnectorPrimitive.disconnect() failed, reason: " + str(error)) + + def __remote_reset(self, delay=0): + """! Use GRM remote API to reset DUT """ + self.logger.prn_inf("remote resources reset...") + if not self.selected_resource: + raise Exception("remote resource not exists!") + try: + if self.selected_resource.reset() is False: + raise Exception("remote resources reset failed!") + except Exception: + self.logger.prn_inf("reset() failed") + raise + + # Post-reset sleep + if delay: + self.logger.prn_inf("waiting %.2f sec after reset"% delay) + time.sleep(delay) + + def __remote_flashing(self, filename, forceflash=False): + """! Use GRM remote API to flash DUT """ + self.logger.prn_inf("remote resources flashing with '%s'..." % filename) + if not self.selected_resource: + raise Exception("remote resource not exists!") + try: + if self.selected_resource.flash(filename, forceflash=forceflash) is False: + raise Exception("remote resource flashing failed!") + except Exception: + self.logger.prn_inf("flash() failed") + raise + + def read(self, count): + """! Read 'count' bytes of data from DUT """ + if not self.connected(): + raise Exception("remote resource not exists!") + data = str() + try: + data = self.selected_resource.read(count) + except Exception as error: + self.logger.prn_err("RemoteConnectorPrimitive.read(%d): %s" % (count, str(error))) + return data + + def write(self, payload, log=False): + """! Write 'payload' to DUT """ + if self.connected(): + try: + self.selected_resource.write(payload) + if log: + self.logger.prn_txd(payload) + return True + except Exception as error: + self.LAST_ERROR = "remote write error: %s" % str(error) + self.logger.prn_err(str(error)) + return False + + def flush(self): + pass + + def allocated(self): + return self.remote_module and self.selected_resource and self.selected_resource.is_allocated + + def connected(self): + return self.allocated() and self.selected_resource.is_connected + + def __remote_release(self): + try: + if self.allocated(): + self.selected_resource.release() + self.selected_resource = None + except Exception as error: + self.logger.prn_err("RemoteConnectorPrimitive.release failed, reason: " + str(error)) + + def finish(self): + # Finally once we're done with the resource + # we disconnect and release the allocation + if self.allocated(): + self.__remote_disconnect() + self.__remote_release() + + def reset(self): + self.__remote_reset(delay=self.forced_reset_timeout) + + def __del__(self): + self.finish() diff --git a/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive_serial.py b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive_serial.py new file mode 100644 index 0000000000..bc8d48d176 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_primitive_serial.py @@ -0,0 +1,156 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 time +from serial import Serial, SerialException + +from .. import host_tests_plugins +from ..host_tests_plugins.host_test_plugins import HostTestPluginBase +from .conn_primitive import ConnectorPrimitive, ConnectorPrimitiveException + + +class SerialConnectorPrimitive(ConnectorPrimitive): + def __init__(self, name, port, baudrate, config): + ConnectorPrimitive.__init__(self, name) + self.port = port + self.baudrate = int(baudrate) + self.read_timeout = 0.01 # 10 milli sec + self.write_timeout = 5 + self.config = config + self.target_id = self.config.get('target_id', None) + self.mcu = self.config.get('mcu', None) + self.polling_timeout = config.get('polling_timeout', 60) + self.forced_reset_timeout = config.get('forced_reset_timeout', 1) + self.skip_reset = config.get('skip_reset', False) + self.serial = None + + # Assume the provided serial port is good. Don't attempt to use the + # target_id to re-discover the serial port, as the board may not be a + # fully valid DAPLink-compatable or Mbed Enabled board (it may be + # missing a mount point). Do not attempt to check if the serial port + # for given target_id changed. We will attempt to open the port and + # pass the already opened port object (not name) to the reset plugin. + serial_port = None + if self.port is not None: + # A serial port was provided. + # Don't pass in the target_id, so that no change in serial port via + # auto-discovery happens. + self.logger.prn_inf("using specified port '%s'" % (self.port)) + serial_port = HostTestPluginBase().check_serial_port_ready(self.port, target_id=None, timeout=self.polling_timeout) + else: + # No serial port was provided. + # Fallback to auto-discovery via target_id. + self.logger.prn_inf("getting serial port via mbedls)") + serial_port = HostTestPluginBase().check_serial_port_ready(self.port, target_id=self.target_id, timeout=self.polling_timeout) + + if serial_port is None: + raise ConnectorPrimitiveException("Serial port not ready!") + + if serial_port != self.port: + # Serial port changed for given targetID + self.logger.prn_inf("serial port changed from '%s to '%s')"% (self.port, serial_port)) + self.port = serial_port + + startTime = time.time() + self.logger.prn_inf("serial(port=%s, baudrate=%d, read_timeout=%s, write_timeout=%d)"% (self.port, self.baudrate, self.read_timeout, self.write_timeout)) + while time.time() - startTime < self.polling_timeout: + try: + # TIMEOUT: While creating Serial object timeout is delibrately passed as 0. Because blocking in Serial.read + # impacts thread and mutliprocess functioning in Python. Hence, instead in self.read() s delay (sleep()) is + # inserted to let serial buffer collect data and avoid spinning on non blocking read(). + self.serial = Serial(self.port, baudrate=self.baudrate, timeout=0, write_timeout=self.write_timeout) + except SerialException as e: + self.serial = None + self.LAST_ERROR = "connection lost, serial.Serial(%s, %d, %d, %d): %s"% (self.port, + self.baudrate, + self.read_timeout, + self.write_timeout, + str(e)) + self.logger.prn_err(str(e)) + self.logger.prn_err("Retry after 1 sec until %s seconds" % self.polling_timeout) + else: + if not self.skip_reset: + self.reset_dev_via_serial(delay=self.forced_reset_timeout) + break + time.sleep(1) + + def reset_dev_via_serial(self, delay=1): + """! Reset device using selected method, calls one of the reset plugins """ + reset_type = self.config.get('reset_type', 'default') + if not reset_type: + reset_type = 'default' + disk = self.config.get('disk', None) + + self.logger.prn_inf("reset device using '%s' plugin..."% reset_type) + result = host_tests_plugins.call_plugin('ResetMethod', + reset_type, + serial=self.serial, + disk=disk, + mcu=self.mcu, + target_id=self.target_id, + polling_timeout=self.config.get('polling_timeout')) + # Post-reset sleep + if delay: + self.logger.prn_inf("waiting %.2f sec after reset"% delay) + time.sleep(delay) + self.logger.prn_inf("wait for it...") + return result + + def read(self, count): + """! Read data from serial port RX buffer """ + # TIMEOUT: Since read is called in a loop, wait for self.timeout period before calling serial.read(). See + # comment on serial.Serial() call above about timeout. + time.sleep(self.read_timeout) + c = str() + try: + if self.serial: + c = self.serial.read(count) + except SerialException as e: + self.serial = None + self.LAST_ERROR = "connection lost, serial.read(%d): %s"% (count, str(e)) + self.logger.prn_err(str(e)) + return c + + def write(self, payload, log=False): + """! Write data to serial port TX buffer """ + try: + if self.serial: + self.serial.write(payload.encode('utf-8')) + if log: + self.logger.prn_txd(payload) + return True + except SerialException as e: + self.serial = None + self.LAST_ERROR = "connection lost, serial.write(%d bytes): %s"% (len(payload), str(e)) + self.logger.prn_err(str(e)) + return False + + def flush(self): + if self.serial: + self.serial.flush() + + def connected(self): + return bool(self.serial) + + def finish(self): + if self.serial: + self.serial.close() + + def reset(self): + self.reset_dev_via_serial(self.forced_reset_timeout) + + def __del__(self): + self.finish() diff --git a/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_proxy.py b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_proxy.py new file mode 100644 index 0000000000..014507d244 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_conn_proxy/conn_proxy.py @@ -0,0 +1,310 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +import sys +import uuid +from time import time +from ..host_tests_logger import HtrunLogger +from .conn_primitive_serial import SerialConnectorPrimitive +from .conn_primitive_remote import RemoteConnectorPrimitive +from .conn_primitive_fastmodel import FastmodelConnectorPrimitive + +if (sys.version_info > (3, 0)): + from queue import Empty as QueueEmpty # Queue here refers to the module, not a class +else: + from Queue import Empty as QueueEmpty + +class KiViBufferWalker(): + """! Simple auxiliary class used to walk through a buffer and search for KV tokens """ + def __init__(self): + self.KIVI_REGEX = r"\{\{([\w\d_-]+);([^\}]+)\}\}" + self.buff = str() + self.kvl = [] + self.re_kv = re.compile(self.KIVI_REGEX) + + def append(self, payload): + """! Append stream buffer with payload and process. Returns non-KV strings""" + logger = HtrunLogger('CONN') + try: + self.buff += payload.decode('utf-8') + except UnicodeDecodeError: + logger.prn_wrn("UnicodeDecodeError encountered!") + self.buff += payload.decode('utf-8','ignore') + lines = self.buff.split('\n') + self.buff = lines[-1] # remaining + lines.pop(-1) + # List of line or strings that did not match K,V pair. + discarded = [] + + for line in lines: + m = self.re_kv.search(line) + if m: + (key, value) = m.groups() + self.kvl.append((key, value, time())) + line = line.strip() + match = m.group(0) + pos = line.find(match) + before = line[:pos] + after = line[pos + len(match):] + if len(before) > 0: + discarded.append(before) + if len(after) > 0: + # not a K,V pair part + discarded.append(after) + else: + # not a K,V pair + discarded.append(line) + return discarded + + def search(self): + """! Check if there is a KV value in buffer """ + return len(self.kvl) > 0 + + def pop_kv(self): + if len(self.kvl): + return self.kvl.pop(0) + return None, None, time() + + +def conn_primitive_factory(conn_resource, config, event_queue, logger): + """! Factory producing connectors based on type and config + @param conn_resource Name of connection primitive (e.g. 'serial' for + local serial port connection or 'grm' for global resource manager) + @param event_queue Even queue of Key-Value protocol + @param config Global configuration for connection process + @param logger Host Test logger instance + @return Object of type or None if type of connection primitive unknown (conn_resource) + """ + polling_timeout = int(config.get('polling_timeout', 60)) + logger.prn_inf("notify event queue about extra %d sec timeout for serial port pooling"%polling_timeout) + event_queue.put(('__timeout', polling_timeout, time())) + + if conn_resource == 'serial': + # Standard serial port connection + # Notify event queue we will wait additional time for serial port to be ready + + # Get extra configuration related to serial port + port = config.get('port') + baudrate = config.get('baudrate') + + logger.prn_inf("initializing serial port listener... ") + connector = SerialConnectorPrimitive( + 'SERI', + port, + baudrate, + config=config) + elif conn_resource == 'grm': + # Start GRM (Gloabal Resource Mgr) collection + logger.prn_inf("initializing global resource mgr listener... ") + connector = RemoteConnectorPrimitive('GLRM', config=config) + elif conn_resource == 'fmc': + # Start Fast Model Connection collection + logger.prn_inf("initializing fast model connection") + connector = FastmodelConnectorPrimitive('FSMD', config=config) + else: + logger.pn_err("unknown connection resource!") + raise NotImplementedError("ConnectorPrimitive factory: unknown connection resource '%s'!"% conn_resource) + + return connector + + +def conn_process(event_queue, dut_event_queue, config): + + def __notify_conn_lost(): + error_msg = connector.error() + connector.finish() + event_queue.put(('__notify_conn_lost', error_msg, time())) + + def __notify_sync_failed(): + error_msg = connector.error() + connector.finish() + event_queue.put(('__notify_sync_failed', error_msg, time())) + + logger = HtrunLogger('CONN') + logger.prn_inf("starting connection process...") + + # Send connection process start event to host process + # NOTE: Do not send any other Key-Value pairs before this! + event_queue.put(('__conn_process_start', 1, time())) + + # Configuration of conn_opriocess behaviour + sync_behavior = int(config.get('sync_behavior', 1)) + sync_timeout = config.get('sync_timeout', 1.0) + conn_resource = config.get('conn_resource', 'serial') + last_sync = False + + # Create connector instance with proper configuration + connector = conn_primitive_factory(conn_resource, config, event_queue, logger) + + # If the connector failed, stop the process now + if not connector.connected(): + logger.prn_err("Failed to connect to resource") + __notify_conn_lost() + return 0 + + # Create simple buffer we will use for Key-Value protocol data + kv_buffer = KiViBufferWalker() + + # List of all sent to target UUIDs (if multiple found) + sync_uuid_list = [] + + # We will ignore all kv pairs before we get sync back + sync_uuid_discovered = False + + def __send_sync(timeout=None): + sync_uuid = str(uuid.uuid4()) + # Handshake, we will send {{sync;UUID}} preamble and wait for mirrored reply + if timeout: + logger.prn_inf("Reset the part and send in new preamble...") + connector.reset() + logger.prn_inf("resending new preamble '%s' after %0.2f sec"% (sync_uuid, timeout)) + else: + logger.prn_inf("sending preamble '%s'"% sync_uuid) + + if connector.write_kv('__sync', sync_uuid): + return sync_uuid + else: + return None + + # Send simple string to device to 'wake up' greentea-client k-v parser + if not connector.write("mbed" * 10, log=True): + # Failed to write 'wake up' string, exit conn_process + __notify_conn_lost() + return 0 + + + # Sync packet management allows us to manipulate the way htrun sends __sync packet(s) + # With current settings we can force on htrun to send __sync packets in this manner: + # + # * --sync=0 - No sync packets will be sent to target platform + # * --sync=-10 - __sync packets will be sent unless we will reach + # timeout or proper response is sent from target platform + # * --sync=N - Send up to N __sync packets to target platform. Response + # is sent unless we get response from target platform or + # timeout occur + + if sync_behavior > 0: + # Sending up to 'n' __sync packets + logger.prn_inf("sending up to %s __sync packets (specified with --sync=%s)"% (sync_behavior, sync_behavior)) + sync_uuid = __send_sync() + + if sync_uuid: + sync_uuid_list.append(sync_uuid) + sync_behavior -= 1 + else: + __notify_conn_lost() + return 0 + elif sync_behavior == 0: + # No __sync packets + logger.prn_wrn("skipping __sync packet (specified with --sync=%s)"% sync_behavior) + else: + # Send __sync until we go reply + logger.prn_inf("sending multiple __sync packets (specified with --sync=%s)"% sync_behavior) + + sync_uuid = __send_sync() + if sync_uuid: + sync_uuid_list.append(sync_uuid) + sync_behavior -= 1 + else: + __notify_conn_lost() + return 0 + + loop_timer = time() + while True: + + # Check if connection is lost to serial + if not connector.connected(): + __notify_conn_lost() + break + + # Send data to DUT + try: + (key, value, _) = dut_event_queue.get(block=False) + except QueueEmpty: + pass # Check if target sent something + else: + # Return if state machine in host_test_default has finished to end process + if key == '__host_test_finished' and value == True: + logger.prn_inf("received special event '%s' value='%s', finishing"% (key, value)) + connector.finish() + return 0 + elif key == '__reset': + logger.prn_inf("received special event '%s', resetting dut" % (key)) + connector.reset() + event_queue.put(("reset_complete", 0, time())) + elif not connector.write_kv(key, value): + connector.write_kv(key, value) + __notify_conn_lost() + break + + # Since read is done every 0.2 sec, with maximum baud rate we can receive 2304 bytes in one read in worst case. + data = connector.read(2304) + if data: + # Stream data stream KV parsing + print_lines = kv_buffer.append(data) + for line in print_lines: + logger.prn_rxd(line) + event_queue.put(('__rxd_line', line, time())) + while kv_buffer.search(): + key, value, timestamp = kv_buffer.pop_kv() + + if sync_uuid_discovered: + event_queue.put((key, value, timestamp)) + logger.prn_inf("found KV pair in stream: {{%s;%s}}, queued..."% (key, value)) + else: + if key == '__sync': + if value in sync_uuid_list: + sync_uuid_discovered = True + event_queue.put((key, value, time())) + idx = sync_uuid_list.index(value) + logger.prn_inf("found SYNC in stream: {{%s;%s}} it is #%d sent, queued..."% (key, value, idx)) + else: + logger.prn_err("found faulty SYNC in stream: {{%s;%s}}, ignored..."% (key, value)) + logger.prn_inf("Resetting the part and sync timeout to clear out the buffer...") + connector.reset() + loop_timer = time() + else: + logger.prn_wrn("found KV pair in stream: {{%s;%s}}, ignoring..."% (key, value)) + + if not sync_uuid_discovered: + # Resending __sync after 'sync_timeout' secs (default 1 sec) + # to target platform. If 'sync_behavior' counter is != 0 we + # will continue to send __sync packets to target platform. + # If we specify 'sync_behavior' < 0 we will send 'forever' + # (or until we get reply) + + if sync_behavior != 0: + time_to_sync_again = time() - loop_timer + if time_to_sync_again > sync_timeout: + sync_uuid = __send_sync(timeout=time_to_sync_again) + + if sync_uuid: + sync_uuid_list.append(sync_uuid) + sync_behavior -= 1 + loop_timer = time() + #Sync behavior will be zero and if last sync fails we should report connection + #lost + if sync_behavior == 0: + last_sync = True + else: + __notify_conn_lost() + break + elif last_sync == True: + #SYNC lost connection event : Device not responding, send sync failed + __notify_sync_failed() + break + + return 0 diff --git a/tools/python/mbed_os_tools/test/host_tests_logger/__init__.py b/tools/python/mbed_os_tools/test/host_tests_logger/__init__.py new file mode 100644 index 0000000000..65c753d4db --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_logger/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from .ht_logger import HtrunLogger diff --git a/tools/python/mbed_os_tools/test/host_tests_logger/ht_logger.py b/tools/python/mbed_os_tools/test/host_tests_logger/ht_logger.py new file mode 100644 index 0000000000..a34003153d --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_logger/ht_logger.py @@ -0,0 +1,41 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 sys +import logging +from functools import partial + + +class HtrunLogger(object): + """! Yet another logger flavour """ + def __init__(self, name): + logging.basicConfig(stream=sys.stdout,format='[%(created).2f][%(name)s]%(message)s', level=logging.DEBUG) + self.logger = logging.getLogger(name) + self.format_str = '[%(logger_level)s] %(message)s' + + def __prn_log(self, logger_level, text, timestamp=None): + self.logger.debug(self.format_str% { + 'logger_level' : logger_level, + 'message' : text, + }) + + self.prn_dbg = partial(__prn_log, self, 'DBG') + self.prn_wrn = partial(__prn_log, self, 'WRN') + self.prn_err = partial(__prn_log, self, 'ERR') + self.prn_inf = partial(__prn_log, self, 'INF') + self.prn_txt = partial(__prn_log, self, 'TXT') + self.prn_txd = partial(__prn_log, self, 'TXD') + self.prn_rxd = partial(__prn_log, self, 'RXD') diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/__init__.py b/tools/python/mbed_os_tools/test/host_tests_plugins/__init__.py new file mode 100644 index 0000000000..600d0fea82 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/__init__.py @@ -0,0 +1,100 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +"""! @package mbed-host-test-plugins + +This package contains plugins used by host test to reset, flash devices etc. +This package can be extended with new packages to add more generic functionality + +""" + +from . import host_test_registry + +# This plugins provide 'flashing' and 'reset' methods to host test scripts +from . import module_copy_shell +from . import module_copy_mbed +from . import module_reset_mbed +from . import module_power_cycle_mbed +from . import module_copy_pyocd +from . import module_reset_pyocd + +# Additional, non standard platforms +from . import module_copy_silabs +from . import module_reset_silabs +from . import module_copy_stlink +from . import module_reset_stlink +from . import module_copy_ublox +from . import module_reset_ublox +from . import module_reset_mps2 +from . import module_copy_mps2 +#import module_copy_jn51xx +#import module_reset_jn51xx + + +# Plugin registry instance +HOST_TEST_PLUGIN_REGISTRY = host_test_registry.HostTestRegistry() + +# Static plugin registration +# Some plugins are commented out if they are not stable or not commonly used +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_mbed.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_shell.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_mbed.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_pyocd.load_plugin()) + +# Extra platforms support +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_mps2.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_mps2.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_silabs.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_silabs.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_stlink.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_stlink.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_power_cycle_mbed.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_pyocd.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_ublox.load_plugin()) +HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_ublox.load_plugin()) +#HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_jn51xx.load_plugin()) +#HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_jn51xx.load_plugin()) + +# TODO: extend plugin loading to files with name module_*.py loaded ad-hoc + +############################################################################### +# Functional interface for host test plugin registry +############################################################################### +def call_plugin(type, capability, *args, **kwargs): + """! Interface to call plugin registry functional way + @param capability Plugin capability we want to call + @param args Additional parameters passed to plugin + @param kwargs Additional parameters passed to plugin + @return Returns return value from call_plugin call + """ + return HOST_TEST_PLUGIN_REGISTRY.call_plugin(type, capability, *args, **kwargs) + +def get_plugin_caps(type): + """! Get list of all capabilities for plugin family with the same type + @param type Type of a plugin + @return Returns list of all capabilities for plugin family with the same type. If there are no capabilities empty list is returned + """ + return HOST_TEST_PLUGIN_REGISTRY.get_plugin_caps(type) + +def get_plugin_info(): + """! Return plugins information + @return Dictionary HOST_TEST_PLUGIN_REGISTRY + """ + return HOST_TEST_PLUGIN_REGISTRY.get_dict() + +def print_plugin_info(): + """! Prints plugins' information in user friendly way + """ + print(HOST_TEST_PLUGIN_REGISTRY) diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/host_test_plugins.py b/tools/python/mbed_os_tools/test/host_tests_plugins/host_test_plugins.py new file mode 100644 index 0000000000..94796fb21e --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/host_test_plugins.py @@ -0,0 +1,270 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 sys +import platform + +from os import access, F_OK +from sys import stdout +from time import sleep +from subprocess import call + +from ... import detect +from ..host_tests_logger import HtrunLogger + + +class HostTestPluginBase: + """! Base class for all plugins used with host tests + """ + ########################################################################### + # Interface: + ########################################################################### + + ########################################################################### + # Interface attributes defining plugin name, type etc. + ########################################################################### + name = "HostTestPluginBase" # Plugin name, can be plugin class name + type = "BasePlugin" # Plugin type: ResetMethod, CopyMethod etc. + capabilities = [] # Capabilities names: what plugin can achieve + # (e.g. reset using some external command line tool) + required_parameters = [] # Parameters required for 'kwargs' in plugin APIs: e.g. self.execute() + stable = False # Determine if plugin is stable and can be used + + def __init__(self): + """ ctor + """ + # Setting Host Test Logger instance + ht_loggers = { + 'BasePlugin' : HtrunLogger('PLGN'), + 'CopyMethod' : HtrunLogger('COPY'), + 'ResetMethod' : HtrunLogger('REST'), + } + self.plugin_logger = ht_loggers.get(self.type, ht_loggers['BasePlugin']) + + ########################################################################### + # Interface methods + ########################################################################### + + def setup(self, *args, **kwargs): + """ Configure plugin, this function should be called before plugin execute() method is used. + """ + return False + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + @return Capability call return value + """ + return False + + def is_os_supported(self, os_name=None): + """! + @return Returns true if plugin works (supportes) under certain OS + @os_name String describing OS. + See self.mbed_os_support() and self.mbed_os_info() + @details In some cases a plugin will not work under particular OS + mainly because command / software used to implement plugin + functionality is not available e.g. on MacOS or Linux. + """ + return True + + ########################################################################### + # Interface helper methods - overload only if you need to have custom behaviour + ########################################################################### + def print_plugin_error(self, text): + """! Function prints error in console and exits always with False + @param text Text to print + """ + self.plugin_logger.prn_err(text) + return False + + def print_plugin_info(self, text, NL=True): + """! Function prints notification in console and exits always with True + @param text Text to print + @param NL Deprecated! Newline will be added behind text if this flag is True + """ + + self.plugin_logger.prn_inf(text) + return True + + def print_plugin_char(self, char): + """ Function prints char on stdout + """ + stdout.write(char) + stdout.flush() + return True + + def check_mount_point_ready(self, destination_disk, init_delay=0.2, loop_delay=0.25, target_id=None, timeout=60): + """! Waits until destination_disk is ready and can be accessed by e.g. copy commands + @return True if mount point was ready in given time, False otherwise + @param destination_disk Mount point (disk) which will be checked for readiness + @param init_delay - Initial delay time before first access check + @param loop_delay - polling delay for access check + @param timeout Mount point pooling timeout in seconds + """ + + if target_id: + # Wait for mount point to appear with mbed-ls + # and if it does check if mount point for target_id changed + # If mount point changed, use new mount point and check if its ready (os.access) + new_destination_disk = destination_disk + + # Sometimes OSes take a long time to mount devices (up to one minute). + # Current pooling time: 120x 500ms = 1 minute + self.print_plugin_info("Waiting up to %d sec for '%s' mount point (current is '%s')..."% (timeout, target_id, destination_disk)) + timeout_step = 0.5 + timeout = int(timeout / timeout_step) + for i in range(timeout): + # mbed_os_tools.detect.create() should be done inside the loop. + # Otherwise it will loop on same data. + mbeds = detect.create() + mbed_list = mbeds.list_mbeds() #list of mbeds present + # get first item in list with a matching target_id, if present + mbed_target = next((x for x in mbed_list if x['target_id']==target_id), None) + + if mbed_target is not None: + # Only assign if mount point is present and known (not None) + if 'mount_point' in mbed_target and mbed_target['mount_point'] is not None: + new_destination_disk = mbed_target['mount_point'] + break + sleep(timeout_step) + + if new_destination_disk != destination_disk: + # Mount point changed, update to new mount point from mbed-ls + self.print_plugin_info("Mount point for '%s' changed from '%s' to '%s'..."% (target_id, destination_disk, new_destination_disk)) + destination_disk = new_destination_disk + + result = True + # Check if mount point we've promoted to be valid one (by optional target_id check above) + # Let's wait for 30 * loop_delay + init_delay max + if not access(destination_disk, F_OK): + self.print_plugin_info("Waiting for mount point '%s' to be ready..."% destination_disk, NL=False) + sleep(init_delay) + for i in range(30): + if access(destination_disk, F_OK): + result = True + break + sleep(loop_delay) + self.print_plugin_char('.') + else: + self.print_plugin_error("mount {} is not accessible ...".format(destination_disk)) + result = False + return (result, destination_disk) + + def check_serial_port_ready(self, serial_port, target_id=None, timeout=60): + """! Function checks (using mbed-ls) and updates serial port name information for DUT with specified target_id. + If no target_id is specified function returns old serial port name. + @param serial_port Current serial port name + @param target_id Target ID of a device under test which serial port will be checked and updated if needed + @param timeout Serial port pooling timeout in seconds + @return Tuple with result (always True) and serial port read from mbed-ls + """ + # If serial port changed (check using mbed-ls), use new serial port + new_serial_port = None + + if target_id: + # Sometimes OSes take a long time to mount devices (up to one minute). + # Current pooling time: 120x 500ms = 1 minute + self.print_plugin_info("Waiting up to %d sec for '%s' serial port (current is '%s')..."% (timeout, target_id, serial_port)) + timeout_step = 0.5 + timeout = int(timeout / timeout_step) + for i in range(timeout): + # mbed_os_tools.detect.create() should be done inside the loop. Otherwise it will loop on same data. + mbeds = detect.create() + mbed_list = mbeds.list_mbeds() #list of mbeds present + # get first item in list with a matching target_id, if present + mbed_target = next((x for x in mbed_list if x['target_id']==target_id), None) + + if mbed_target is not None: + # Only assign if serial port is present and known (not None) + if 'serial_port' in mbed_target and mbed_target['serial_port'] is not None: + new_serial_port = mbed_target['serial_port'] + if new_serial_port != serial_port: + # Serial port changed, update to new serial port from mbed-ls + self.print_plugin_info("Serial port for tid='%s' changed from '%s' to '%s'..." % (target_id, serial_port, new_serial_port)) + break + sleep(timeout_step) + else: + new_serial_port = serial_port + + return new_serial_port + + def check_parameters(self, capability, *args, **kwargs): + """! This function should be ran each time we call execute() to check if none of the required parameters is missing + @param capability Capability name + @param args Additional parameters + @param kwargs Additional parameters + @return Returns True if all parameters are passed to plugin, else return False + """ + missing_parameters = [] + for parameter in self.required_parameters: + if parameter not in kwargs: + missing_parameters.append(parameter) + if len(missing_parameters): + self.print_plugin_error("execute parameter(s) '%s' missing!"% (', '.join(missing_parameters))) + return False + return True + + def run_command(self, cmd, shell=True, stdin = None): + """! Runs command from command line. + @param cmd Command to execute + @param shell True if shell command should be executed (eg. ls, ps) + @param stdin A custom stdin for the process running the command (defaults to None) + @details Function prints 'cmd' return code if execution failed + @return True if command successfully executed + """ + result = True + try: + ret = call(cmd, shell=shell, stdin=stdin) + if ret: + self.print_plugin_error("[ret=%d] Command: %s"% (int(ret), cmd)) + return False + except Exception as e: + result = False + self.print_plugin_error("[ret=%d] Command: %s"% (int(ret), cmd)) + self.print_plugin_error(str(e)) + return result + + def mbed_os_info(self): + """! Returns information about host OS + @return Returns tuple with information about OS and host platform + """ + result = (os.name, + platform.system(), + platform.release(), + platform.version(), + sys.platform) + return result + + def mbed_os_support(self): + """! Function used to determine host OS + @return Returns None if host OS is unknown, else string with name + @details This function should be ported for new OS support + """ + result = None + os_info = self.mbed_os_info() + if (os_info[0] == 'nt' and os_info[1] == 'Windows'): + result = 'Windows7' + elif (os_info[0] == 'posix' and os_info[1] == 'Linux' and ('Ubuntu' in os_info[3])): + result = 'Ubuntu' + elif (os_info[0] == 'posix' and os_info[1] == 'Linux'): + result = 'LinuxGeneric' + elif (os_info[0] == 'posix' and os_info[1] == 'Darwin'): + result = 'Darwin' + return result diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/host_test_registry.py b/tools/python/mbed_os_tools/test/host_tests_plugins/host_test_registry.py new file mode 100644 index 0000000000..124361bef4 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/host_test_registry.py @@ -0,0 +1,129 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +class HostTestRegistry: + """ Simple class used to register and store + host test plugins for further usage + """ + # Here we actually store all the plugins + PLUGINS = {} # 'Plugin Name' : Plugin Object + + def print_error(self, text): + """! Prints error directly on console + + @param text Error message text message + """ + print("Plugin load failed. Reason: %s"% text) + + def register_plugin(self, plugin): + """! Registers and stores plugin inside registry for further use. + + @param plugin Plugin name + + @return True if plugin setup was successful and plugin can be registered, else False + + @details Method also calls plugin's setup() function to configure plugin if needed. + Note: Different groups of plugins may demand different extra parameter. Plugins + should be at least for one type of plugin configured with the same parameters + because we do not know which of them will actually use particular parameter. + """ + # TODO: + # - check for unique caps for specified type + if plugin.name not in self.PLUGINS: + if plugin.setup(): # Setup plugin can be completed without errors + self.PLUGINS[plugin.name] = plugin + return True + else: + self.print_error("%s setup failed"% plugin.name) + else: + self.print_error("%s already loaded"% plugin.name) + return False + + def call_plugin(self, type, capability, *args, **kwargs): + """! Execute plugin functionality respectively to its purpose + @param type Plugin type + @param capability Plugin capability name + @param args Additional plugin parameters + @param kwargs Additional plugin parameters + @return Returns result from plugin's execute() method + """ + for plugin_name in self.PLUGINS: + plugin = self.PLUGINS[plugin_name] + if plugin.type == type and capability in plugin.capabilities: + return plugin.execute(capability, *args, **kwargs) + return False + + def get_plugin_caps(self, type): + """! Returns list of all capabilities for plugin family with the same type + @param type Plugin type + @return Returns list of capabilities for plugin. If there are no capabilities empty list is returned + """ + result = [] + for plugin_name in self.PLUGINS: + plugin = self.PLUGINS[plugin_name] + if plugin.type == type: + result.extend(plugin.capabilities) + return sorted(result) + + def load_plugin(self, name): + """! Used to load module from system (by import) + @param name name of the module to import + @return Returns result of __import__ operation + """ + mod = __import__("module_%s"% name) + return mod + + def get_string(self): + """! User friendly printing method to show hooked plugins + @return Returns string formatted with PrettyTable + """ + from prettytable import PrettyTable, HEADER + column_names = ['name', 'type', 'capabilities', 'stable', 'os_support', 'required_parameters'] + pt = PrettyTable(column_names, junction_char="|", hrules=HEADER) + for column in column_names: + pt.align[column] = 'l' + for plugin_name in sorted(self.PLUGINS.keys()): + name = self.PLUGINS[plugin_name].name + type = self.PLUGINS[plugin_name].type + stable = self.PLUGINS[plugin_name].stable + capabilities = ', '.join(self.PLUGINS[plugin_name].capabilities) + is_os_supported = self.PLUGINS[plugin_name].is_os_supported() + required_parameters = ', '.join(self.PLUGINS[plugin_name].required_parameters) + row = [name, type, capabilities, stable, is_os_supported, required_parameters] + pt.add_row(row) + return pt.get_string() + + def get_dict(self): + column_names = ['name', 'type', 'capabilities', 'stable'] + result = {} + for plugin_name in sorted(self.PLUGINS.keys()): + name = self.PLUGINS[plugin_name].name + type = self.PLUGINS[plugin_name].type + stable = self.PLUGINS[plugin_name].stable + capabilities = self.PLUGINS[plugin_name].capabilities + is_os_supported = self.PLUGINS[plugin_name].is_os_supported() + required_parameters = self.PLUGINS[plugin_name].required_parameters + result[plugin_name] = { + "name" : name, + "type" : type, + "stable" : stable, + "capabilities" : capabilities, + "os_support" : is_os_supported, + "required_parameters" : required_parameters + } + return result + + def __str__(self): + return self.get_string() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_jn51xx.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_jn51xx.py new file mode 100644 index 0000000000..01451d9f07 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_jn51xx.py @@ -0,0 +1,88 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginCopyMethod_JN51xx(HostTestPluginBase): + + # Plugin interface + name = 'HostTestPluginCopyMethod_JN51xx' + type = 'CopyMethod' + capabilities = ['jn51xx'] + required_parameters = ['image_path', 'serial'] + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def is_os_supported(self, os_name=None): + """! In this implementation this plugin only is supporeted under Windows machines + """ + # If no OS name provided use host OS name + if not os_name: + os_name = self.mbed_os_support() + + # This plugin only works on Windows + if os_name and os_name.startswith('Windows'): + return True + return False + + def setup(self, *args, **kwargs): + """! Configure plugin, this function should be called before plugin execute() method is used. + """ + self.JN51XX_PROGRAMMER = 'JN51xxProgrammer.exe' + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + @return Capability call return value + """ + if not kwargs['image_path']: + self.print_plugin_error("Error: image path not specified") + return False + + if not kwargs['serial']: + self.print_plugin_error("Error: serial port not set (not opened?)") + return False + + result = False + if self.check_parameters(capability, *args, **kwargs): + if kwargs['image_path'] and kwargs['serial']: + image_path = os.path.normpath(kwargs['image_path']) + serial_port = kwargs['serial'] + if capability == 'jn51xx': + # Example: + # JN51xxProgrammer.exe -s COM15 -f -V0 + cmd = [self.JN51XX_PROGRAMMER, + '-s', serial_port, + '-f', image_path, + '-V0' + ] + result = self.run_command(cmd) + return result + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginCopyMethod_JN51xx() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_mbed.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_mbed.py new file mode 100644 index 0000000000..7aa9972a39 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_mbed.py @@ -0,0 +1,99 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +from shutil import copy +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginCopyMethod_Mbed(HostTestPluginBase): + """ Generic flashing method for mbed-enabled devices (by copy) + """ + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def generic_mbed_copy(self, image_path, destination_disk): + """! Generic mbed copy method for "mbed enabled" devices. + + @param image_path Path to binary file to be flashed + @param destination_disk Path to destination (mbed mount point) + + @details It uses standard python shutil function to copy image_file (target specific binary) to device's disk. + + @return Returns True if copy (flashing) was successful + """ + result = True + if not destination_disk.endswith('/') and not destination_disk.endswith('\\'): + destination_disk += '/' + try: + copy(image_path, destination_disk) + except Exception as e: + self.print_plugin_error("shutil.copy('%s', '%s')"% (image_path, destination_disk)) + self.print_plugin_error("Error: %s"% str(e)) + result = False + return result + + # Plugin interface + name = 'HostTestPluginCopyMethod_Mbed' + type = 'CopyMethod' + stable = True + capabilities = ['shutil', 'default'] + required_parameters = ['image_path', 'destination_disk'] + + def setup(self, *args, **kwargs): + """ Configure plugin, this function should be called before plugin execute() method is used. + """ + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name. + @details Each capability may directly just call some command line program or execute building pythonic function + @return Returns True if 'capability' operation was successful + """ + if not kwargs['image_path']: + self.print_plugin_error("Error: image path not specified") + return False + + if not kwargs['destination_disk']: + self.print_plugin_error("Error: destination disk not specified") + return False + + # This optional parameter can be used if TargetID is provided (-t switch) + target_id = kwargs.get('target_id', None) + pooling_timeout = kwargs.get('polling_timeout', 60) + + result = False + if self.check_parameters(capability, *args, **kwargs): + # Capability 'default' is a dummy capability + if kwargs['image_path'] and kwargs['destination_disk']: + if capability == 'shutil': + image_path = os.path.normpath(kwargs['image_path']) + destination_disk = os.path.normpath(kwargs['destination_disk']) + # Wait for mount point to be ready + # if mount point changed according to target_id use new mount point + # available in result (_, destination_disk) of check_mount_point_ready + mount_res, destination_disk = self.check_mount_point_ready(destination_disk, target_id=self.target_id, timeout=pooling_timeout) # Blocking + if mount_res: + result = self.generic_mbed_copy(image_path, destination_disk) + return result + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginCopyMethod_Mbed() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_mps2.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_mps2.py new file mode 100644 index 0000000000..abfb55affb --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_mps2.py @@ -0,0 +1,105 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +from shutil import copy +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginCopyMethod_MPS2(HostTestPluginBase): + # MPS2 specific flashing / binary setup funcitons + + name = 'HostTestPluginCopyMethod_MPS2' + type = 'CopyMethod' + stable = True + capabilities = ['mps2'] + required_parameters = ['image_path', 'destination_disk'] + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def mps2_copy(self, image_path, destination_disk): + """! mps2 copy method for "mbed enabled" devices. + This copies the file on the MPS2 keeping the same extension but + renaming it "mbed.extension" + @param image_path Path to file to be copied + @param destination_disk Path to destination (mbed mount point) + + @details this uses shutil copy to copy the file. + + @return Returns True if copy (flashing) was successful + """ + result = True + # Keep the same extension in the test spec and on the MPS2 + _, extension = os.path.splitext(image_path); + destination_path = os.path.join(destination_disk, "mbed" + extension) + try: + copy(image_path, destination_path) + # sync command on mac ignores command line arguments. + if os.name == 'posix': + result = self.run_command('sync -f %s' % destination_path, shell=True) + except Exception as e: + self.print_plugin_error("shutil.copy('%s', '%s')"% (image_path, destination_path)) + self.print_plugin_error("Error: %s"% str(e)) + result = False + + return result + + def setup(self, *args, **kwargs): + """ Configure plugin, this function should be called before plugin execute() method is used. + """ + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name. + @details Each capability may directly just call some command line program or execute building pythonic function + @return Returns True if 'capability' operation was successful + """ + + + if not kwargs['image_path']: + self.print_plugin_error("Error: image path not specified") + return False + + if not kwargs['destination_disk']: + self.print_plugin_error("Error: destination disk not specified") + return False + + # This optional parameter can be used if TargetID is provided (-t switch) + target_id = kwargs.get('target_id', None) + pooling_timeout = kwargs.get('polling_timeout', 60) + result = False + + if self.check_parameters(capability, *args, **kwargs): + # Capability 'default' is a dummy capability + if kwargs['image_path'] and kwargs['destination_disk']: + if capability == 'mps2': + image_path = os.path.normpath(kwargs['image_path']) + destination_disk = os.path.normpath(kwargs['destination_disk']) + # Wait for mount point to be ready + # if mount point changed according to target_id use new mount point + # available in result (_, destination_disk) of check_mount_point_ready + result, destination_disk = self.check_mount_point_ready(destination_disk, target_id=target_id, timeout=pooling_timeout) # Blocking + if result: + result = self.mps2_copy(image_path, destination_disk) + return result + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginCopyMethod_MPS2() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_pyocd.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_pyocd.py new file mode 100644 index 0000000000..77191877f2 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_pyocd.py @@ -0,0 +1,97 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +from .host_test_plugins import HostTestPluginBase + +try: + from pyocd.core.helpers import ConnectHelper + from pyocd.flash.file_programmer import FileProgrammer + PYOCD_PRESENT = True +except ImportError: + PYOCD_PRESENT = False + +class HostTestPluginCopyMethod_pyOCD(HostTestPluginBase): + # Plugin interface + name = 'HostTestPluginCopyMethod_pyOCD' + type = 'CopyMethod' + stable = True + capabilities = ['pyocd'] + required_parameters = ['image_path', 'target_id'] + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def setup(self, *args, **kwargs): + """ Configure plugin, this function should be called before plugin execute() method is used. + """ + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + @return Capability call return value + """ + if not PYOCD_PRESENT: + self.print_plugin_error( + 'The "pyocd" feature is not installed. Please run ' + '"pip install mbed-os-tools[pyocd]" to enable the "pyocd" copy plugin.' + ) + return False + + if not self.check_parameters(capability, *args, **kwargs): + return False + + if not kwargs['image_path']: + self.print_plugin_error("Error: image path not specified") + return False + + if not kwargs['target_id']: + self.print_plugin_error("Error: Target ID") + return False + + target_id = kwargs['target_id'] + image_path = os.path.normpath(kwargs['image_path']) + with ConnectHelper.session_with_chosen_probe(unique_id=target_id, resume_on_disconnect=False) as session: + # Performance hack! + # Eventually pyOCD will know default clock speed + # per target + test_clock = 10000000 + target_type = session.board.target_type + if target_type == "nrf51": + # Override clock since 10MHz is too fast + test_clock = 1000000 + if target_type == "ncs36510": + # Override clock since 10MHz is too fast + test_clock = 1000000 + + # Configure link + session.probe.set_clock(test_clock) + + # Program the file + programmer = FileProgrammer(session) + programmer.program(image_path, format=kwargs['format']) + + return True + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginCopyMethod_pyOCD() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_shell.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_shell.py new file mode 100644 index 0000000000..c5080ee43c --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_shell.py @@ -0,0 +1,94 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +from os.path import join, basename +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginCopyMethod_Shell(HostTestPluginBase): + # Plugin interface + name = 'HostTestPluginCopyMethod_Shell' + type = 'CopyMethod' + stable = True + capabilities = ['shell', 'cp', 'copy', 'xcopy'] + required_parameters = ['image_path', 'destination_disk'] + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def setup(self, *args, **kwargs): + """ Configure plugin, this function should be called before plugin execute() method is used. + """ + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + @return Capability call return value + """ + if not kwargs['image_path']: + self.print_plugin_error("Error: image path not specified") + return False + + if not kwargs['destination_disk']: + self.print_plugin_error("Error: destination disk not specified") + return False + + # This optional parameter can be used if TargetID is provided (-t switch) + target_id = kwargs.get('target_id', None) + pooling_timeout = kwargs.get('polling_timeout', 60) + + result = False + if self.check_parameters(capability, *args, **kwargs): + if kwargs['image_path'] and kwargs['destination_disk']: + image_path = os.path.normpath(kwargs['image_path']) + destination_disk = os.path.normpath(kwargs['destination_disk']) + # Wait for mount point to be ready + # if mount point changed according to target_id use new mount point + # available in result (_, destination_disk) of check_mount_point_ready + mount_res, destination_disk = self.check_mount_point_ready(destination_disk, target_id=target_id, timeout=pooling_timeout) # Blocking + if not mount_res: + return result # mount point is not ready return + # Prepare correct command line parameter values + image_base_name = basename(image_path) + destination_path = join(destination_disk, image_base_name) + if capability == 'shell': + if os.name == 'nt': capability = 'copy' + elif os.name == 'posix': capability = 'cp' + if capability == 'cp' or capability == 'copy' or capability == 'copy': + copy_method = capability + cmd = [copy_method, image_path, destination_path] + if os.name == 'posix': + result = self.run_command(cmd, shell=False) + if os.uname()[0] == 'Linux': + result = result and self.run_command(["sync", "-f", destination_path]) + else: + result = result and self.run_command(["sync"]) + else: + result = self.run_command(cmd) + return result + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginCopyMethod_Shell() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_silabs.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_silabs.py new file mode 100644 index 0000000000..7cd4e4bf0d --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_silabs.py @@ -0,0 +1,72 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginCopyMethod_Silabs(HostTestPluginBase): + + # Plugin interface + name = 'HostTestPluginCopyMethod_Silabs' + type = 'CopyMethod' + capabilities = ['eACommander', 'eACommander-usb'] + required_parameters = ['image_path', 'destination_disk'] + stable = True + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def setup(self, *args, **kwargs): + """ Configure plugin, this function should be called before plugin execute() method is used. + """ + self.EACOMMANDER_CMD = 'eACommander.exe' + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + + @return Capability call return value + """ + result = False + if self.check_parameters(capability, *args, **kwargs) is True: + image_path = os.path.normpath(kwargs['image_path']) + destination_disk = os.path.normpath(kwargs['destination_disk']) + if capability == 'eACommander': + cmd = [self.EACOMMANDER_CMD, + '--serialno', destination_disk, + '--flash', image_path, + '--resettype', '2', '--reset'] + result = self.run_command(cmd) + elif capability == 'eACommander-usb': + cmd = [self.EACOMMANDER_CMD, + '--usb', destination_disk, + '--flash', image_path] + result = self.run_command(cmd) + return result + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginCopyMethod_Silabs() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_stlink.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_stlink.py new file mode 100644 index 0000000000..617e3e7d91 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_stlink.py @@ -0,0 +1,79 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginCopyMethod_Stlink(HostTestPluginBase): + + # Plugin interface + name = 'HostTestPluginCopyMethod_Stlink' + type = 'CopyMethod' + capabilities = ['stlink'] + required_parameters = ['image_path'] + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def is_os_supported(self, os_name=None): + """! In this implementation this plugin only is supporeted under Windows machines + """ + # If no OS name provided use host OS name + if not os_name: + os_name = self.mbed_os_support() + + # This plugin only works on Windows + if os_name and os_name.startswith('Windows'): + return True + return False + + def setup(self, *args, **kwargs): + """! Configure plugin, this function should be called before plugin execute() method is used. + """ + self.ST_LINK_CLI = 'ST-LINK_CLI.exe' + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + + @return Capability call return value + """ + result = False + if self.check_parameters(capability, *args, **kwargs) is True: + image_path = os.path.normpath(kwargs['image_path']) + if capability == 'stlink': + # Example: + # ST-LINK_CLI.exe -p "C:\Work\mbed\build\test\DISCO_F429ZI\GCC_ARM\MBED_A1\basic.bin" + cmd = [self.ST_LINK_CLI, + '-p', image_path, '0x08000000', + '-V' + ] + result = self.run_command(cmd) + return result + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginCopyMethod_Stlink() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_ublox.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_ublox.py new file mode 100644 index 0000000000..6885ccd021 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_copy_ublox.py @@ -0,0 +1,79 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginCopyMethod_ublox(HostTestPluginBase): + + # Plugin interface + name = 'HostTestPluginCopyMethod_ublox' + type = 'CopyMethod' + capabilities = ['ublox'] + required_parameters = ['image_path'] + + def is_os_supported(self, os_name=None): + """! In this implementation this plugin only is supporeted under Windows machines + """ + # If no OS name provided use host OS name + if not os_name: + os_name = self.mbed_os_support() + + # This plugin only works on Windows + if os_name and os_name.startswith('Windows'): + return True + return False + + def setup(self, *args, **kwargs): + """! Configure plugin, this function should be called before plugin execute() method is used. + """ + self.FLASH_ERASE = 'FlashErase.exe' + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + + @return Capability call return value + """ + result = False + if self.check_parameters(capability, *args, **kwargs) is True: + image_path = os.path.normpath(kwargs['image_path']) + if capability == 'ublox': + # Example: + # FLASH_ERASE -c 2 -s 0xD7000 -l 0x20000 -f "binary_file.bin" + cmd = [self.FLASH_ERASE, + '-c', + 'A', + '-s', + '0xD7000', + '-l', + '0x20000', + '-f', image_path + ] + result = self.run_command(cmd) + return result + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginCopyMethod_ublox() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_power_cycle_mbed.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_power_cycle_mbed.py new file mode 100644 index 0000000000..d68b1dce02 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_power_cycle_mbed.py @@ -0,0 +1,181 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 json +import time +import requests +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginPowerCycleResetMethod(HostTestPluginBase): + + # Plugin interface + name = 'HostTestPluginPowerCycleResetMethod' + type = 'ResetMethod' + stable = True + capabilities = ['power_cycle'] + required_parameters = ['target_id', 'device_info'] + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def setup(self, *args, **kwargs): + """! Configure plugin, this function should be called before plugin execute() method is used. + """ + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + @return Capability call return value + """ + if 'target_id' not in kwargs or not kwargs['target_id']: + self.print_plugin_error("Error: This plugin requires mbed target_id") + return False + + if 'device_info' not in kwargs or type(kwargs['device_info']) is not dict: + self.print_plugin_error("Error: This plugin requires dict parameter 'device_info' passed by the caller.") + return False + + result = False + if self.check_parameters(capability, *args, **kwargs) is True: + if capability in HostTestPluginPowerCycleResetMethod.capabilities: + target_id = kwargs['target_id'] + device_info = kwargs['device_info'] + ret = self.__get_mbed_tas_rm_addr() + if ret: + ip, port = ret + result = self.__hw_reset(ip, port, target_id, device_info) + return result + + def __get_mbed_tas_rm_addr(self): + """ + Get IP and Port of mbed tas rm service. + :return: + """ + try: + ip = os.environ['MBED_TAS_RM_IP'] + port = os.environ['MBED_TAS_RM_PORT'] + return ip, port + except KeyError as e: + self.print_plugin_error("HOST: Failed to read environment variable (" + str(e) + "). Can't perform hardware reset.") + + return None + + def __hw_reset(self, ip, port, target_id, device_info): + """ + Reset target device using TAS RM API + + :param ip: + :param port: + :param target_id: + :param device_info: + :return: + """ + + switch_off_req = { + "name": "switchResource", + "sub_requests": [ + { + "resource_type": "mbed_platform", + "resource_id": target_id, + "switch_command": "OFF" + } + ] + } + + + switch_on_req = { + "name": "switchResource", + "sub_requests": [ + { + "resource_type": "mbed_platform", + "resource_id": target_id, + "switch_command": "ON" + } + ] + } + + result = False + + # reset target + switch_off_req = self.__run_request(ip, port, switch_off_req) + if switch_off_req is None: + self.print_plugin_error("HOST: Failed to communicate with TAS RM!") + return result + + if "error" in switch_off_req['sub_requests'][0]: + self.print_plugin_error("HOST: Failed to reset target. error = %s" % switch_off_req['sub_requests'][0]['error']) + return result + + def poll_state(required_state): + switch_state_req = { + "name": "switchResource", + "sub_requests": [ + { + "resource_type": "mbed_platform", + "resource_id": target_id, + "switch_command": "STATE" + } + ] + } + resp = self.__run_request(ip, port, switch_state_req) + start = time.time() + while resp and (resp['sub_requests'][0]['state'] != required_state or (required_state == 'ON' and + resp['sub_requests'][0]["mount_point"] == "Not Connected")) and (time.time() - start) < 300: + time.sleep(2) + resp = self.__run_request(ip, port, resp) + return resp + + poll_state("OFF") + + self.__run_request(ip, port, switch_on_req) + resp = poll_state("ON") + if resp and resp['sub_requests'][0]['state'] == 'ON' and resp['sub_requests'][0]["mount_point"] != "Not Connected": + for k, v in resp['sub_requests'][0].viewitems(): + device_info[k] = v + result = True + else: + self.print_plugin_error("HOST: Failed to reset device %s" % target_id) + + return result + + @staticmethod + def __run_request(ip, port, request): + """ + + :param request: + :return: + """ + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + get_resp = requests.get("http://%s:%s/" % (ip, port), data=json.dumps(request), headers=headers) + resp = get_resp.json() + if get_resp.status_code == 200: + return resp + else: + return None + + +def load_plugin(): + """! Returns plugin available in this module + """ + return HostTestPluginPowerCycleResetMethod() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_jn51xx.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_jn51xx.py new file mode 100644 index 0000000000..86085f37c3 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_jn51xx.py @@ -0,0 +1,87 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginResetMethod_JN51xx(HostTestPluginBase): + + # Plugin interface + name = 'HostTestPluginResetMethod_JN51xx' + type = 'ResetMethod' + capabilities = ['jn51xx'] + required_parameters = ['serial'] + stable = False + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def is_os_supported(self, os_name=None): + """! In this implementation this plugin only is supporeted under Windows machines + """ + # If no OS name provided use host OS name + if not os_name: + os_name = self.mbed_os_support() + + # This plugin only works on Windows + if os_name and os_name.startswith('Windows'): + return True + return False + + def setup(self, *args, **kwargs): + """! Configure plugin, this function should be called before plugin execute() method is used. + """ + # Note you need to have eACommander.exe on your system path! + self.JN51XX_PROGRAMMER = 'JN51xxProgrammer.exe' + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + @return Capability call return value + """ + if not kwargs['serial']: + self.print_plugin_error("Error: serial port not set (not opened?)") + return False + + result = False + if self.check_parameters(capability, *args, **kwargs): + if kwargs['serial']: + if capability == 'jn51xx': + # Example: + # The device should be automatically reset before the programmer disconnects. + # Issuing a command with no file to program or read will put the device into + # programming mode and then reset it. E.g. + # $ JN51xxProgrammer.exe -s COM5 -V0 + # COM5: Detected JN5179 with MAC address 00:15:8D:00:01:24:E0:37 + serial_port = kwargs['serial'] + cmd = [self.JN51XX_PROGRAMMER, + '-s', serial_port, + '-V0' + ] + result = self.run_command(cmd) + return result + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginResetMethod_JN51xx() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_mbed.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_mbed.py new file mode 100644 index 0000000000..1a6c5d8589 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_mbed.py @@ -0,0 +1,131 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +import pkg_resources +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginResetMethod_Mbed(HostTestPluginBase): + + # Plugin interface + name = 'HostTestPluginResetMethod_Mbed' + type = 'ResetMethod' + stable = True + capabilities = ['default'] + required_parameters = ['serial'] + + def __init__(self): + """! ctor + @details We can check module version by referring to version attribute + import pkg_resources + print pkg_resources.require("mbed-host-tests")[0].version + '2.7' + """ + HostTestPluginBase.__init__(self) + self.re_float = re.compile("^\d+\.\d+") + pyserial_version = pkg_resources.require("pyserial")[0].version + self.pyserial_version = self.get_pyserial_version(pyserial_version) + self.is_pyserial_v3 = float(self.pyserial_version) >= 3.0 + + def get_pyserial_version(self, pyserial_version): + """! Retrieve pyserial module version + @return Returns float with pyserial module number + """ + version = 3.0 + m = self.re_float.search(pyserial_version) + if m: + try: + version = float(m.group(0)) + except ValueError: + version = 3.0 # We will assume you've got latest (3.0+) + return version + + def safe_sendBreak(self, serial): + """! Closure for pyserial version dependant API calls + """ + if self.is_pyserial_v3: + return self._safe_sendBreak_v3_0(serial) + return self._safe_sendBreak_v2_7(serial) + + def _safe_sendBreak_v2_7(self, serial): + """! pyserial 2.7 API implementation of sendBreak/setBreak + @details + Below API is deprecated for pyserial 3.x versions! + http://pyserial.readthedocs.org/en/latest/pyserial_api.html#serial.Serial.sendBreak + http://pyserial.readthedocs.org/en/latest/pyserial_api.html#serial.Serial.setBreak + """ + result = True + try: + serial.sendBreak() + except: + # In Linux a termios.error is raised in sendBreak and in setBreak. + # The following setBreak() is needed to release the reset signal on the target mcu. + try: + serial.setBreak(False) + except: + result = False + return result + + def _safe_sendBreak_v3_0(self, serial): + """! pyserial 3.x API implementation of send_brea / break_condition + @details + http://pyserial.readthedocs.org/en/latest/pyserial_api.html#serial.Serial.send_break + http://pyserial.readthedocs.org/en/latest/pyserial_api.html#serial.Serial.break_condition + """ + result = True + try: + serial.send_break() + except: + # In Linux a termios.error is raised in sendBreak and in setBreak. + # The following break_condition = False is needed to release the reset signal on the target mcu. + try: + serial.break_condition = False + except Exception as e: + self.print_plugin_error("Error while doing 'serial.break_condition = False' : %s"% str(e)) + result = False + return result + + def setup(self, *args, **kwargs): + """! Configure plugin, this function should be called before plugin execute() method is used. + """ + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + @return Capability call return value + """ + if not kwargs['serial']: + self.print_plugin_error("Error: serial port not set (not opened?)") + return False + + result = False + if self.check_parameters(capability, *args, **kwargs) is True: + if kwargs['serial']: + if capability == 'default': + serial = kwargs['serial'] + result = self.safe_sendBreak(serial) + return result + + +def load_plugin(): + """! Returns plugin available in this module + """ + return HostTestPluginResetMethod_Mbed() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_mps2.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_mps2.py new file mode 100644 index 0000000000..8767ffb36f --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_mps2.py @@ -0,0 +1,91 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 time + +from .host_test_plugins import HostTestPluginBase + +# Note: This plugin is not fully functional, needs improvements + +class HostTestPluginResetMethod_MPS2(HostTestPluginBase): + """! Plugin used to reset ARM_MPS2 platform + + @details Supports: + reboot.txt - startup from standby state, reboots when in run mode. + """ + + # Plugin interface + name = 'HostTestPluginResetMethod_MPS2' + type = 'ResetMethod' + capabilities = ['reboot.txt'] + required_parameters = ['disk'] + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def touch_file(self, path): + """ Touch file and set timestamp to items + """ + with open(path, 'a'): + os.utime(path, None) + + def setup(self, *args, **kwargs): + """ Prepare / configure plugin to work. + This method can receive plugin specific parameters by kwargs and + ignore other parameters which may affect other plugins. + """ + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + + @return Capability call return value + """ + result = False + if not kwargs['disk']: + self.print_plugin_error("Error: disk not specified") + return False + + destination_disk = kwargs.get('disk', None) + + # This optional parameter can be used if TargetID is provided (-t switch) + target_id = kwargs.get('target_id', None) + pooling_timeout = kwargs.get('polling_timeout', 60) + if self.check_parameters(capability, *args, **kwargs) is True: + + if capability == 'reboot.txt': + reboot_file_path = os.path.join(destination_disk, capability) + reboot_fh = open(reboot_file_path, "w") + reboot_fh.close() + # Make sure the file is written to the board before continuing + if os.name == 'posix': + self.run_command('sync -f %s' % reboot_file_path, shell=True) + time.sleep(3) # sufficient delay for device to boot up + result, destination_disk = self.check_mount_point_ready(destination_disk, target_id=target_id, timeout=pooling_timeout) + return result + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginResetMethod_MPS2() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_pyocd.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_pyocd.py new file mode 100644 index 0000000000..45b691c92e --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_pyocd.py @@ -0,0 +1,83 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from .host_test_plugins import HostTestPluginBase + +try: + from pyocd.core.helpers import ConnectHelper + PYOCD_PRESENT = True +except ImportError: + PYOCD_PRESENT = False + + +class HostTestPluginResetMethod_pyOCD(HostTestPluginBase): + + # Plugin interface + name = 'HostTestPluginResetMethod_pyOCD' + type = 'ResetMethod' + stable = True + capabilities = ['pyocd'] + required_parameters = ['target_id'] + + def __init__(self): + """! ctor + @details We can check module version by referring to version attribute + import pkg_resources + print pkg_resources.require("mbed-host-tests")[0].version + '2.7' + """ + HostTestPluginBase.__init__(self) + + def setup(self, *args, **kwargs): + """! Configure plugin, this function should be called before plugin execute() method is used. + """ + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + @return Capability call return value + """ + if not PYOCD_PRESENT: + self.print_plugin_error( + 'The "pyocd" feature is not installed. Please run ' + '"pip install mbed-os-tools[pyocd]" to enable the "pyocd" reset plugin.' + ) + return False + + if not kwargs['target_id']: + self.print_plugin_error("Error: target_id not set") + return False + + result = False + if self.check_parameters(capability, *args, **kwargs) is True: + if kwargs['target_id']: + if capability == 'pyocd': + target_id = kwargs['target_id'] + with ConnectHelper.session_with_chosen_probe(unique_id=target_id, + resume_on_disconnect=False) as session: + session.target.reset() + session.target.resume() + result = True + return result + + +def load_plugin(): + """! Returns plugin available in this module + """ + return HostTestPluginResetMethod_pyOCD() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_silabs.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_silabs.py new file mode 100644 index 0000000000..fe79d3b876 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_silabs.py @@ -0,0 +1,75 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginResetMethod_SiLabs(HostTestPluginBase): + + # Plugin interface + name = 'HostTestPluginResetMethod_SiLabs' + type = 'ResetMethod' + capabilities = ['eACommander', 'eACommander-usb'] + required_parameters = ['disk'] + stable = True + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def setup(self, *args, **kwargs): + """ Configure plugin, this function should be called before plugin execute() method is used. + """ + # Note you need to have eACommander.exe on your system path! + self.EACOMMANDER_CMD = 'eACommander.exe' + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + + @return Capability call return value + """ + result = False + if self.check_parameters(capability, *args, **kwargs) is True: + disk = kwargs['disk'].rstrip('/\\') + + if capability == 'eACommander': + # For this copy method 'disk' will be 'serialno' for eACommander command line parameters + # Note: Commands are executed in the order they are specified on the command line + cmd = [self.EACOMMANDER_CMD, + '--serialno', disk, + '--resettype', '2', '--reset',] + result = self.run_command(cmd) + elif capability == 'eACommander-usb': + # For this copy method 'disk' will be 'usb address' for eACommander command line parameters + # Note: Commands are executed in the order they are specified on the command line + cmd = [self.EACOMMANDER_CMD, + '--usb', disk, + '--resettype', '2', '--reset',] + result = self.run_command(cmd) + return result + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginResetMethod_SiLabs() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_stlink.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_stlink.py new file mode 100644 index 0000000000..776172468a --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_stlink.py @@ -0,0 +1,109 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 sys +import tempfile +from .host_test_plugins import HostTestPluginBase + +FIX_FILE_NAME = "enter_file.txt" + + +class HostTestPluginResetMethod_Stlink(HostTestPluginBase): + + # Plugin interface + name = 'HostTestPluginResetMethod_Stlink' + type = 'ResetMethod' + capabilities = ['stlink'] + required_parameters = [] + stable = False + + def __init__(self): + """ ctor + """ + HostTestPluginBase.__init__(self) + + def is_os_supported(self, os_name=None): + """! In this implementation this plugin only is supporeted under Windows machines + """ + # If no OS name provided use host OS name + if not os_name: + os_name = self.mbed_os_support() + + # This plugin only works on Windows + if os_name and os_name.startswith('Windows'): + return True + return False + + def setup(self, *args, **kwargs): + """! Configure plugin, this function should be called before plugin execute() method is used. + """ + # Note you need to have eACommander.exe on your system path! + self.ST_LINK_CLI = 'ST-LINK_CLI.exe' + return True + + def create_stlink_fix_file(self, file_path): + """! Creates a file with a line separator + This is to work around a bug in ST-LINK CLI that does not let the target run after burning it. + See https://github.com/ARMmbed/mbed-os-tools/issues/147 for the details. + @param file_path A path to write into this file + """ + try: + with open(file_path, "w") as fix_file: + fix_file.write(os.linesep) + except (OSError, IOError): + self.print_plugin_error("Error opening STLINK-PRESS-ENTER-BUG file") + sys.exit(1) + + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + + @return Capability call return value + """ + result = False + if self.check_parameters(capability, *args, **kwargs) is True: + if capability == 'stlink': + # Example: + # ST-LINK_CLI.exe -Rst -Run + cmd = [self.ST_LINK_CLI, + '-Rst', '-Run'] + + # Due to the ST-LINK bug, we must press enter after burning the target + # We do this here automatically by passing a file which contains an `ENTER` (line separator) + # to the ST-LINK CLI as `stdin` for the running process + enter_file_path = os.path.join(tempfile.gettempdir(), FIX_FILE_NAME) + self.create_stlink_fix_file(enter_file_path) + try: + with open(enter_file_path, 'r') as fix_file: + stdin_arg = kwargs.get('stdin', fix_file) + result = self.run_command(cmd, stdin = stdin_arg) + except (OSError, IOError): + self.print_plugin_error("Error opening STLINK-PRESS-ENTER-BUG file") + sys.exit(1) + + return result + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginResetMethod_Stlink() diff --git a/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_ublox.py b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_ublox.py new file mode 100644 index 0000000000..1eab914bd0 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_plugins/module_reset_ublox.py @@ -0,0 +1,73 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from .host_test_plugins import HostTestPluginBase + + +class HostTestPluginResetMethod_ublox(HostTestPluginBase): + + # Plugin interface + name = 'HostTestPluginResetMethod_ublox' + type = 'ResetMethod' + capabilities = ['ublox'] + required_parameters = [] + stable = False + + def is_os_supported(self, os_name=None): + """! In this implementation this plugin only is supporeted under Windows machines + """ + # If no OS name provided use host OS name + if not os_name: + os_name = self.mbed_os_support() + + # This plugin only works on Windows + if os_name and os_name.startswith('Windows'): + return True + return False + + def setup(self, *args, **kwargs): + """! Configure plugin, this function should be called before plugin execute() method is used. + """ + # Note you need to have jlink.exe on your system path! + self.JLINK = 'jlink.exe' + return True + + def execute(self, capability, *args, **kwargs): + """! Executes capability by name + + @param capability Capability name + @param args Additional arguments + @param kwargs Additional arguments + + @details Each capability e.g. may directly just call some command line program or execute building pythonic function + + @return Capability call return value + """ + result = False + if self.check_parameters(capability, *args, **kwargs) is True: + if capability == 'ublox': + # Example: + # JLINK.exe --CommanderScript aCommandFile + cmd = [self.JLINK, + '-CommanderScript', + r'reset.jlink'] + result = self.run_command(cmd) + return result + + +def load_plugin(): + """ Returns plugin available in this module + """ + return HostTestPluginResetMethod_ublox() diff --git a/tools/python/mbed_os_tools/test/host_tests_registry/__init__.py b/tools/python/mbed_os_tools/test/host_tests_registry/__init__.py new file mode 100644 index 0000000000..932e78511b --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_registry/__init__.py @@ -0,0 +1,22 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +"""! @package host_registry + +Host registry is used to store all host tests (by id) which can be called from test framework + +""" + +from .host_registry import HostRegistry diff --git a/tools/python/mbed_os_tools/test/host_tests_registry/host_registry.py b/tools/python/mbed_os_tools/test/host_tests_registry/host_registry.py new file mode 100644 index 0000000000..4e69523925 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_registry/host_registry.py @@ -0,0 +1,150 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +try: + from imp import load_source +except ImportError: + import importlib + import sys + + def load_source(module_name, file_path): + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + sys.modules[module_name] = module + return module + + +from inspect import getmembers, isclass +from os import listdir +from os.path import abspath, exists, isdir, isfile, join + +from ..host_tests.base_host_test import BaseHostTest + + +class HostRegistry: + """ Class stores registry with host tests and objects representing them + """ + HOST_TESTS = {} # Map between host_test_name -> host_test_object + + def register_host_test(self, ht_name, ht_object): + """! Registers host test object by name + + @param ht_name Host test unique name + @param ht_object Host test class object + """ + if ht_name not in self.HOST_TESTS: + self.HOST_TESTS[ht_name] = ht_object + + def unregister_host_test(self, ht_name): + """! Unregisters host test object by name + + @param ht_name Host test unique name + """ + if ht_name in self.HOST_TESTS: + del self.HOST_TESTS[ht_name] + + def get_host_test(self, ht_name): + """! Fetches host test object by name + + @param ht_name Host test unique name + + @return Host test callable object or None if object is not found + """ + return self.HOST_TESTS[ht_name] if ht_name in self.HOST_TESTS else None + + def is_host_test(self, ht_name): + """! Checks (by name) if host test object is registered already + + @param ht_name Host test unique name + + @return True if ht_name is registered (available), else False + """ + return (ht_name in self.HOST_TESTS and + self.HOST_TESTS[ht_name] is not None) + + def table(self, verbose=False): + """! Prints list of registered host test classes (by name) + @Detail For devel & debug purposes + """ + from prettytable import PrettyTable, HEADER + column_names = ['name', 'class', 'origin'] + pt = PrettyTable(column_names, junction_char="|", hrules=HEADER) + for column in column_names: + pt.align[column] = 'l' + + for name, host_test in sorted(self.HOST_TESTS.items()): + cls_str = str(host_test.__class__) + if host_test.script_location: + src_path = host_test.script_location + else: + src_path = 'mbed-host-tests' + pt.add_row([name, cls_str, src_path]) + return pt.get_string() + + def register_from_path(self, path, verbose=False): + """ Enumerates and registers locally stored host tests + Host test are derived from mbed_os_tools.test.BaseHostTest classes + """ + if path: + path = path.strip('"') + if verbose: + print("HOST: Inspecting '%s' for local host tests..." % path) + if exists(path) and isdir(path): + python_modules = [ + f for f in listdir(path) + if isfile(join(path, f)) and f.endswith(".py") + ] + for module_file in python_modules: + self._add_module_to_registry(path, module_file, verbose) + + def _add_module_to_registry(self, path, module_file, verbose): + module_name = module_file[:-3] + try: + mod = load_source(module_name, abspath(join(path, module_file))) + except Exception as e: + print( + "HOST: Error! While loading local host test module '%s'" + % join(path, module_file) + ) + print("HOST: %s" % str(e)) + return + if verbose: + print("HOST: Loading module '%s': %s" % (module_file, str(mod))) + + for name, obj in getmembers(mod): + if ( + isclass(obj) and + issubclass(obj, BaseHostTest) and + str(obj) != str(BaseHostTest) + ): + if obj.name: + host_test_name = obj.name + else: + host_test_name = module_name + host_test_cls = obj + host_test_cls.script_location = join(path, module_file) + if verbose: + print( + "HOST: Found host test implementation: %s -|> %s" + % (str(obj), str(BaseHostTest)) + ) + print( + "HOST: Registering '%s' as '%s'" + % (str(host_test_cls), host_test_name) + ) + self.register_host_test( + host_test_name, host_test_cls() + ) diff --git a/tools/python/mbed_os_tools/test/host_tests_runner/__init__.py b/tools/python/mbed_os_tools/test/host_tests_runner/__init__.py new file mode 100644 index 0000000000..a35ec01b5a --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_runner/__init__.py @@ -0,0 +1,25 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +"""! @package mbed-host-test-runner + +This package contains basic host test implementation with algorithms to flash and reset device. +Functionality can be overridden by set of plugins which can provide specialised flashing and reset implementations. + +""" + +from mbed_os_tools import VERSION + +__version__ = VERSION diff --git a/tools/python/mbed_os_tools/test/host_tests_runner/host_test.py b/tools/python/mbed_os_tools/test/host_tests_runner/host_test.py new file mode 100644 index 0000000000..0cb5920ed7 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_runner/host_test.py @@ -0,0 +1,134 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from sys import stdout +from .mbed_base import Mbed +from . import __version__ + + +class HostTestResults(object): + """! Test results set by host tests """ + + def enum(self, **enums): + return type('Enum', (), enums) + + def __init__(self): + self.TestResults = self.enum( + RESULT_SUCCESS = 'success', + RESULT_FAILURE = 'failure', + RESULT_ERROR = 'error', + RESULT_END = 'end', + RESULT_UNDEF = 'undefined', + RESULT_TIMEOUT = 'timeout', + RESULT_IOERR_COPY = "ioerr_copy", + RESULT_IOERR_DISK = "ioerr_disk", + RESULT_IO_SERIAL = 'ioerr_serial', + RESULT_NO_IMAGE = 'no_image', + RESULT_NOT_DETECTED = "not_detected", + RESULT_MBED_ASSERT = "mbed_assert", + RESULT_PASSIVE = "passive", + RESULT_BUILD_FAILED = 'build_failed', + RESULT_SYNC_FAILED = 'sync_failed' + ) + + # Magically creates attributes in this class corresponding + # to RESULT_ elements in self.TestResults enum + for attr in self.TestResults.__dict__: + if attr.startswith('RESULT_'): + setattr(self, attr, self.TestResults.__dict__[attr]) + + # Indexes of this list define string->int mapping between + # actual strings with results + self.TestResultsList = [ + self.TestResults.RESULT_SUCCESS, + self.TestResults.RESULT_FAILURE, + self.TestResults.RESULT_ERROR, + self.TestResults.RESULT_END, + self.TestResults.RESULT_UNDEF, + self.TestResults.RESULT_TIMEOUT, + self.TestResults.RESULT_IOERR_COPY, + self.TestResults.RESULT_IOERR_DISK, + self.TestResults.RESULT_IO_SERIAL, + self.TestResults.RESULT_NO_IMAGE, + self.TestResults.RESULT_NOT_DETECTED, + self.TestResults.RESULT_MBED_ASSERT, + self.TestResults.RESULT_PASSIVE, + self.TestResults.RESULT_BUILD_FAILED, + self.TestResults.RESULT_SYNC_FAILED + ] + + def get_test_result_int(self, test_result_str): + """! Maps test result string to unique integer """ + if test_result_str in self.TestResultsList: + return self.TestResultsList.index(test_result_str) + return -1 + + def __getitem__(self, test_result_str): + """! Returns numerical result code """ + return self.get_test_result_int(test_result_str) + + +class Test(HostTestResults): + """ Base class for host test's test runner + """ + def __init__(self, options): + """ ctor + """ + HostTestResults.__init__(self) + self.mbed = Mbed(options) + + def run(self): + """ Test runner for host test. This function will start executing + test and forward test result via serial port to test suite + """ + pass + + def setup(self): + """! Setup and check if configuration for test is correct. + @details This function can for example check if serial port is already opened + """ + pass + + def notify(self, msg): + """! On screen notification function + @param msg Text message sent to stdout directly + """ + stdout.write(msg) + stdout.flush() + + def print_result(self, result): + """! Test result unified printing function + @param result Should be a member of HostTestResults.RESULT_* enums + """ + self.notify("{{%s}}\n"% result) + self.notify("{{%s}}\n"% self.RESULT_END) + + def finish(self): + """ dctor for this class, finishes tasks and closes resources + """ + pass + + def get_hello_string(self): + """ Hello string used as first print + """ + return "host test executor ver. " + __version__ + + +class DefaultTestSelectorBase(Test): + """! Test class with serial port initialization + @details This is a base for other test selectors, initializes + """ + def __init__(self, options): + Test.__init__(self, options=options) diff --git a/tools/python/mbed_os_tools/test/host_tests_runner/host_test_default.py b/tools/python/mbed_os_tools/test/host_tests_runner/host_test_default.py new file mode 100644 index 0000000000..27bc9e9c28 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_runner/host_test_default.py @@ -0,0 +1,589 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +import sys +import traceback +from time import time +from sre_compile import error + +from multiprocessing import Process, Queue +from .. import host_tests_plugins, BaseHostTest +from ..host_tests_registry import HostRegistry + +# Host test supervisors +from ..host_tests.echo import EchoTest +from ..host_tests.rtc_auto import RTCTest +from ..host_tests.hello_auto import HelloTest +from ..host_tests.detect_auto import DetectPlatformTest +from ..host_tests.wait_us_auto import WaitusTest +from ..host_tests.default_auto import DefaultAuto +from ..host_tests.dev_null_auto import DevNullTest + +from .host_test import DefaultTestSelectorBase +from ..host_tests_logger import HtrunLogger +from ..host_tests_conn_proxy import conn_process +from ..host_tests_toolbox.host_functional import handle_send_break_cmd +if (sys.version_info > (3, 0)): + from queue import Empty as QueueEmpty +else: + from Queue import Empty as QueueEmpty + + +class DefaultTestSelector(DefaultTestSelectorBase): + """! Select default host_test supervision (replaced after auto detection) """ + RESET_TYPE_SW_RST = "software_reset" + RESET_TYPE_HW_RST = "hardware_reset" + + def __init__(self, options): + """! ctor + """ + self.options = options + + self.logger = HtrunLogger('HTST') + + self.registry = HostRegistry() + self.registry.register_host_test("echo", EchoTest()) + self.registry.register_host_test("default", DefaultAuto()) + self.registry.register_host_test("rtc_auto", RTCTest()) + self.registry.register_host_test("hello_auto", HelloTest()) + self.registry.register_host_test("detect_auto", DetectPlatformTest()) + self.registry.register_host_test("default_auto", DefaultAuto()) + self.registry.register_host_test("wait_us_auto", WaitusTest()) + self.registry.register_host_test("dev_null_auto", DevNullTest()) + + # Handle extra command from + if options: + if options.enum_host_tests: + for path in options.enum_host_tests: + self.registry.register_from_path( + path, verbose=options.verbose + ) + + if options.list_reg_hts: # --list option + print(self.registry.table(options.verbose)) + sys.exit(0) + + if options.list_plugins: # --plugins option + host_tests_plugins.print_plugin_info() + sys.exit(0) + + if options.global_resource_mgr or options.fast_model_connection: + # If Global/Simulator Resource Mgr is working it will handle reset/flashing workflow + # So local plugins are offline + self.options.skip_reset = True + self.options.skip_flashing = True + + if options.compare_log: + with open(options.compare_log, "r") as f: + self.compare_log = f.read().splitlines() + + else: + self.compare_log = None + self.serial_output_file = options.serial_output_file + self.compare_log_idx = 0 + DefaultTestSelectorBase.__init__(self, options) + + def is_host_test_obj_compatible(self, obj_instance): + """! Check if host test object loaded is actually host test class + derived from 'mbed_os_tools.test.BaseHostTest()' + Additionaly if host test class implements custom ctor it should + call BaseHostTest().__Init__() + @param obj_instance Instance of host test derived class + @return True if obj_instance is derived from mbed_os_tools.test.BaseHostTest() + and BaseHostTest.__init__() was called, else return False + """ + result = False + if obj_instance: + result = True + self.logger.prn_inf("host test class: '%s'"% obj_instance.__class__) + + # Check if host test (obj_instance) is derived from mbed_os_tools.test.BaseHostTest() + if not isinstance(obj_instance, BaseHostTest): + # In theory we should always get host test objects inheriting from BaseHostTest() + # because loader will only load those. + self.logger.prn_err("host test must inherit from mbed_os_tools.test.BaseHostTest() class") + result = False + + # Check if BaseHostTest.__init__() was called when custom host test is created + if not obj_instance.base_host_test_inited(): + self.logger.prn_err("custom host test __init__() must call BaseHostTest.__init__(self)") + result = False + + return result + + def run_test(self): + """! This function implements key-value protocol state-machine. + Handling of all events and connector are handled here. + @return Return self.TestResults.RESULT_* enum + """ + result = None + timeout_duration = 10 # Default test case timeout + coverage_idle_timeout = 10 # Default coverage idle timeout + event_queue = Queue() # Events from DUT to host + dut_event_queue = Queue() # Events from host to DUT {k;v} + + def callback__notify_prn(key, value, timestamp): + """! Handles __norify_prn. Prints all lines in separate log line """ + for line in value.splitlines(): + self.logger.prn_inf(line) + + callbacks = { + "__notify_prn" : callback__notify_prn + } + + # if True we will allow host test to consume all events after test is finished + callbacks_consume = True + # Flag check if __exit event occurred + callbacks__exit = False + # Flag check if __exit_event_queue event occurred + callbacks__exit_event_queue = False + # Handle to dynamically loaded host test object + self.test_supervisor = None + # Version: greentea-client version from DUT + self.client_version = None + + self.logger.prn_inf("starting host test process...") + + + # Create device info here as it may change after restart. + config = { + "digest" : "serial", + "port" : self.mbed.port, + "baudrate" : self.mbed.serial_baud, + "mcu" : self.mbed.mcu, + "program_cycle_s" : self.options.program_cycle_s, + "reset_type" : self.options.forced_reset_type, + "target_id" : self.options.target_id, + "disk" : self.options.disk, + "polling_timeout" : self.options.polling_timeout, + "forced_reset_timeout" : self.options.forced_reset_timeout, + "sync_behavior" : self.options.sync_behavior, + "platform_name" : self.options.micro, + "image_path" : self.mbed.image_path, + "skip_reset": self.options.skip_reset, + "tags" : self.options.tag_filters, + "sync_timeout": self.options.sync_timeout + } + + if self.options.global_resource_mgr: + grm_config = self._parse_grm(self.options.global_resource_mgr) + grm_config["conn_resource"] = "grm" + config.update(grm_config) + + if self.options.fast_model_connection: + + config.update({ + "conn_resource" : 'fmc', + "fm_config" : self.options.fast_model_connection + }) + + def start_conn_process(): + # DUT-host communication process + args = (event_queue, dut_event_queue, config) + p = Process(target=conn_process, args=args) + p.deamon = True + p.start() + return p + + def process_code_coverage(key, value, timestamp): + """! Process the found coverage key value and perform an idle + loop checking for more timeing out if there is no response from + the target within the idle timeout. + @param key The key from the first coverage event + @param value The value from the first coverage event + @param timestamp The timestamp from the first coverage event + @return The elapsed time taken by the processing of code coverage, + and the (key, value, and timestamp) of the next event + """ + original_start_time = time() + start_time = time() + + # Perform callback on first event + callbacks[key](key, value, timestamp) + + # Start idle timeout loop looking for other events + while (time() - start_time) < coverage_idle_timeout: + try: + (key, value, timestamp) = event_queue.get(timeout=1) + except QueueEmpty: + continue + + # If coverage detected use idle loop + # Prevent breaking idle loop for __rxd_line (occurs between keys) + if key == '__coverage_start' or key == '__rxd_line': + start_time = time() + + # Perform callback + callbacks[key](key, value, timestamp) + continue + + elapsed_time = time() - original_start_time + return elapsed_time, (key, value, timestamp) + + p = start_conn_process() + conn_process_started = False + try: + # Wait for the start event. Process start timeout does not apply in + # Global resource manager case as it may take a while for resource + # to be available. + (key, value, timestamp) = event_queue.get( + timeout=None if self.options.global_resource_mgr else self.options.process_start_timeout) + + if key == '__conn_process_start': + conn_process_started = True + else: + self.logger.prn_err("First expected event was '__conn_process_start', received '%s' instead"% key) + + except QueueEmpty: + self.logger.prn_err("Conn process failed to start in %f sec"% self.options.process_start_timeout) + + if not conn_process_started: + p.terminate() + return self.RESULT_TIMEOUT + + start_time = time() + + try: + consume_preamble_events = True + + while (time() - start_time) < timeout_duration: + # Handle default events like timeout, host_test_name, ... + try: + (key, value, timestamp) = event_queue.get(timeout=1) + except QueueEmpty: + continue + + # Write serial output to the file if specified in options. + if self.serial_output_file: + if key == '__rxd_line': + with open(self.serial_output_file, "a") as f: + f.write("%s\n" % value) + + # In this mode we only check serial output against compare log. + if self.compare_log: + if key == '__rxd_line': + if self.match_log(value): + self.logger.prn_inf("Target log matches compare log!") + result = True + break + + if consume_preamble_events: + if key == '__timeout': + # Override default timeout for this event queue + start_time = time() + timeout_duration = int(value) # New timeout + self.logger.prn_inf("setting timeout to: %d sec"% int(value)) + elif key == '__version': + self.client_version = value + self.logger.prn_inf("DUT greentea-client version: " + self.client_version) + elif key == '__host_test_name': + # Load dynamically requested host test + self.test_supervisor = self.registry.get_host_test(value) + + # Check if host test object loaded is actually host test class + # derived from 'mbed_os_tools.test.BaseHostTest()' + # Additionaly if host test class implements custom ctor it should + # call BaseHostTest().__Init__() + if self.test_supervisor and self.is_host_test_obj_compatible(self.test_supervisor): + # Pass communication queues and setup() host test + self.test_supervisor.setup_communication(event_queue, dut_event_queue, config) + try: + # After setup() user should already register all callbacks + self.test_supervisor.setup() + except (TypeError, ValueError): + # setup() can throw in normal circumstances TypeError and ValueError + self.logger.prn_err("host test setup() failed, reason:") + self.logger.prn_inf("==== Traceback start ====") + for line in traceback.format_exc().splitlines(): + print(line) + self.logger.prn_inf("==== Traceback end ====") + result = self.RESULT_ERROR + event_queue.put(('__exit_event_queue', 0, time())) + + self.logger.prn_inf("host test setup() call...") + if self.test_supervisor.get_callbacks(): + callbacks.update(self.test_supervisor.get_callbacks()) + self.logger.prn_inf("CALLBACKs updated") + else: + self.logger.prn_wrn("no CALLBACKs specified by host test") + self.logger.prn_inf("host test detected: %s"% value) + else: + self.logger.prn_err("host test not detected: %s"% value) + result = self.RESULT_ERROR + event_queue.put(('__exit_event_queue', 0, time())) + + consume_preamble_events = False + elif key == '__sync': + # This is DUT-Host Test handshake event + self.logger.prn_inf("sync KV found, uuid=%s, timestamp=%f"% (str(value), timestamp)) + elif key == '__notify_sync_failed': + # This event is sent by conn_process, SYNC failed + self.logger.prn_err(value) + self.logger.prn_wrn("stopped to consume events due to %s event"% key) + callbacks_consume = False + result = self.RESULT_SYNC_FAILED + event_queue.put(('__exit_event_queue', 0, time())) + elif key == '__notify_conn_lost': + # This event is sent by conn_process, DUT connection was lost + self.logger.prn_err(value) + self.logger.prn_wrn("stopped to consume events due to %s event"% key) + callbacks_consume = False + result = self.RESULT_IO_SERIAL + event_queue.put(('__exit_event_queue', 0, time())) + elif key == '__exit_event_queue': + # This event is sent by the host test indicating no more events expected + self.logger.prn_inf("%s received"% (key)) + callbacks__exit_event_queue = True + break + elif key.startswith('__'): + # Consume other system level events + pass + else: + self.logger.prn_err("orphan event in preamble phase: {{%s;%s}}, timestamp=%f"% (key, str(value), timestamp)) + else: + # If coverage detected switch to idle loop + if key == '__coverage_start': + self.logger.prn_inf("starting coverage idle timeout loop...") + elapsed_time, (key, value, timestamp) = process_code_coverage(key, value, timestamp) + + # Ignore the time taken by the code coverage + timeout_duration += elapsed_time + self.logger.prn_inf("exiting coverage idle timeout loop (elapsed_time: %.2f" % elapsed_time) + + if key == '__notify_complete': + # This event is sent by Host Test, test result is in value + # or if value is None, value will be retrieved from HostTest.result() method + self.logger.prn_inf("%s(%s)" % (key, str(value))) + result = value + event_queue.put(('__exit_event_queue', 0, time())) + elif key == '__reset': + # This event only resets the dut, not the host test + dut_event_queue.put(('__reset', True, time())) + elif key == '__reset_dut': + # Disconnect to avoid connection lost event + dut_event_queue.put(('__host_test_finished', True, time())) + p.join() + + if value == DefaultTestSelector.RESET_TYPE_SW_RST: + self.logger.prn_inf("Performing software reset.") + # Just disconnecting and re-connecting comm process will soft reset DUT + elif value == DefaultTestSelector.RESET_TYPE_HW_RST: + self.logger.prn_inf("Performing hard reset.") + # request hardware reset + self.mbed.hw_reset() + else: + self.logger.prn_err("Invalid reset type (%s). Supported types [%s]." % + (value, ", ".join([DefaultTestSelector.RESET_TYPE_HW_RST, + DefaultTestSelector.RESET_TYPE_SW_RST]))) + self.logger.prn_inf("Software reset will be performed.") + + # connect to the device + p = start_conn_process() + elif key == '__notify_conn_lost': + # This event is sent by conn_process, DUT connection was lost + self.logger.prn_err(value) + self.logger.prn_wrn("stopped to consume events due to %s event"% key) + callbacks_consume = False + result = self.RESULT_IO_SERIAL + event_queue.put(('__exit_event_queue', 0, time())) + elif key == '__exit': + # This event is sent by DUT, test suite exited + self.logger.prn_inf("%s(%s)"% (key, str(value))) + callbacks__exit = True + event_queue.put(('__exit_event_queue', 0, time())) + elif key == '__exit_event_queue': + # This event is sent by the host test indicating no more events expected + self.logger.prn_inf("%s received"% (key)) + callbacks__exit_event_queue = True + break + elif key == '__timeout_set': + # Dynamic timeout set + timeout_duration = int(value) # New timeout + self.logger.prn_inf("setting timeout to: %d sec"% int(value)) + elif key == '__timeout_adjust': + # Dynamic timeout adjust + timeout_duration = timeout_duration + int(value) # adjust time + self.logger.prn_inf("adjusting timeout with %d sec (now %d)" % (int(value), timeout_duration)) + elif key in callbacks: + # Handle callback + callbacks[key](key, value, timestamp) + else: + self.logger.prn_err("orphan event in main phase: {{%s;%s}}, timestamp=%f"% (key, str(value), timestamp)) + except Exception: + self.logger.prn_err("something went wrong in event main loop!") + self.logger.prn_inf("==== Traceback start ====") + for line in traceback.format_exc().splitlines(): + print(line) + self.logger.prn_inf("==== Traceback end ====") + result = self.RESULT_ERROR + + time_duration = time() - start_time + self.logger.prn_inf("test suite run finished after %.2f sec..."% time_duration) + + if self.compare_log and result is None: + if self.compare_log_idx < len(self.compare_log): + self.logger.prn_err("Expected output [%s] not received in log." % self.compare_log[self.compare_log_idx]) + + # Force conn_proxy process to return + dut_event_queue.put(('__host_test_finished', True, time())) + p.join() + self.logger.prn_inf("CONN exited with code: %s"% str(p.exitcode)) + + # Callbacks... + self.logger.prn_inf("No events in queue" if event_queue.empty() else "Some events in queue") + + # If host test was used we will: + # 1. Consume all existing events in queue if consume=True + # 2. Check result from host test and call teardown() + + # NOTE: with the introduction of the '__exit_event_queue' event, there + # should never be left events assuming the DUT has stopped sending data + # over the serial data. Leaving this for now to catch anything that slips through. + + if callbacks_consume: + # We are consuming all remaining events if requested + while not event_queue.empty(): + try: + (key, value, timestamp) = event_queue.get(timeout=1) + except QueueEmpty: + break + + if key == '__notify_complete': + # This event is sent by Host Test, test result is in value + # or if value is None, value will be retrieved from HostTest.result() method + self.logger.prn_inf("%s(%s)"% (key, str(value))) + result = value + elif key.startswith('__'): + # Consume other system level events + pass + elif key in callbacks: + callbacks[key](key, value, timestamp) + else: + self.logger.prn_wrn(">>> orphan event: {{%s;%s}}, timestamp=%f"% (key, str(value), timestamp)) + self.logger.prn_inf("stopped consuming events") + + if result is not None: # We must compare here against None! + # Here for example we've received some error code like IOERR_COPY + self.logger.prn_inf("host test result() call skipped, received: %s"% str(result)) + else: + if self.test_supervisor: + result = self.test_supervisor.result() + self.logger.prn_inf("host test result(): %s"% str(result)) + + if not callbacks__exit: + self.logger.prn_wrn("missing __exit event from DUT") + + if not callbacks__exit_event_queue: + self.logger.prn_wrn("missing __exit_event_queue event from host test") + + #if not callbacks__exit_event_queue and not result: + if not callbacks__exit_event_queue and result is None: + self.logger.prn_err("missing __exit_event_queue event from " + \ + "host test and no result from host test, timeout...") + result = self.RESULT_TIMEOUT + + self.logger.prn_inf("calling blocking teardown()") + if self.test_supervisor: + self.test_supervisor.teardown() + self.logger.prn_inf("teardown() finished") + + return result + + def execute(self): + """! Test runner for host test. + + @details This function will start executing test and forward test result via serial port + to test suite. This function is sensitive to work-flow flags such as --skip-flashing, + --skip-reset etc. + First function will flash device with binary, initialize serial port for communication, + reset target. On serial port handshake with test case will be performed. It is when host + test reads property data from serial port (sent over serial port). + At the end of the procedure proper host test (defined in set properties) will be executed + and test execution timeout will be measured. + """ + result = self.RESULT_UNDEF + + # hello sting with htrun version, for debug purposes + self.logger.prn_inf(self.get_hello_string()) + + try: + # Copy image to device + if self.options.skip_flashing: + self.logger.prn_inf("copy image onto target... SKIPPED!") + else: + self.logger.prn_inf("copy image onto target...") + result = self.mbed.copy_image() + if not result: + result = self.RESULT_IOERR_COPY + return self.get_test_result_int(result) + + # Execute test if flashing was successful or skipped + test_result = self.run_test() + + if test_result == True: + result = self.RESULT_SUCCESS + elif test_result == False: + result = self.RESULT_FAILURE + elif test_result is None: + result = self.RESULT_ERROR + else: + result = test_result + + # This will be captured by Greentea + self.logger.prn_inf("{{result;%s}}"% result) + return self.get_test_result_int(result) + + except KeyboardInterrupt: + return(-3) # Keyboard interrupt + + def match_log(self, line): + """ + Matches lines from compare log with the target serial output. Compare log lines are matched in seq using index + self.compare_log_idx. Lines can be strings to be matched as is or regular expressions. + + :param line: + :return: + """ + if self.compare_log_idx < len(self.compare_log): + regex = self.compare_log[self.compare_log_idx] + # Either the line is matched as is or it is checked as a regular expression. + try: + if regex in line or re.search(regex, line): + self.compare_log_idx += 1 + except error: + # May not be a regular expression + return False + return self.compare_log_idx == len(self.compare_log) + + @staticmethod + def _parse_grm(grm_arg): + grm_module, leftover = grm_arg.split(':', 1) + parts = leftover.rsplit(':', 1) + + try: + grm_host, grm_port = parts + _ = int(grm_port) + except ValueError: + # No valid port was found, so assume no port was supplied + grm_host = leftover + grm_port = None + + return { + "grm_module" : grm_module, + "grm_host" : grm_host, + "grm_port" : grm_port, + } diff --git a/tools/python/mbed_os_tools/test/host_tests_runner/mbed_base.py b/tools/python/mbed_os_tools/test/host_tests_runner/mbed_base.py new file mode 100644 index 0000000000..1595434c77 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_runner/mbed_base.py @@ -0,0 +1,257 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 json +import os +from time import sleep +from .. import host_tests_plugins as ht_plugins +from ... import detect +from .. import DEFAULT_BAUD_RATE +from ..host_tests_logger import HtrunLogger + + +class Mbed: + """! Base class for a host driven test + @details This class stores information about things like disk, port, serial speed etc. + Class is also responsible for manipulation of serial port between host and mbed device + """ + def __init__(self, options): + """ ctor + """ + # For compatibility with old mbed. We can use command line options for Mbed object + # or we can pass options directly from . + self.options = options + self.logger = HtrunLogger('MBED') + # Options related to copy / reset mbed device + self.port = self.options.port + self.mcu = self.options.micro + self.disk = self.options.disk + self.target_id = self.options.target_id + self.image_path = self.options.image_path.strip('"') if self.options.image_path is not None else '' + self.copy_method = self.options.copy_method + self.retry_copy = self.options.retry_copy + self.program_cycle_s = float(self.options.program_cycle_s if self.options.program_cycle_s is not None else 2.0) + self.polling_timeout = self.options.polling_timeout + + # Serial port settings + self.serial_baud = DEFAULT_BAUD_RATE + self.serial_timeout = 1 + + # Users can use command to pass port speeds together with port name. E.g. COM4:115200:1 + # Format if PORT:SPEED:TIMEOUT + port_config = self.port.split(':') if self.port else '' + if len(port_config) == 2: + # -p COM4:115200 + self.port = port_config[0] + self.serial_baud = int(port_config[1]) + elif len(port_config) == 3: + # -p COM4:115200:0.5 + self.port = port_config[0] + self.serial_baud = int(port_config[1]) + self.serial_timeout = float(port_config[2]) + + # Overriding baud rate value with command line specified value + self.serial_baud = self.options.baud_rate if self.options.baud_rate else self.serial_baud + + # Test configuration in JSON format + self.test_cfg = None + if self.options.json_test_configuration is not None: + # We need to normalize path before we open file + json_test_configuration_path = self.options.json_test_configuration.strip("\"'") + try: + self.logger.prn_inf("Loading test configuration from '%s'..." % json_test_configuration_path) + with open(json_test_configuration_path) as data_file: + self.test_cfg = json.load(data_file) + except IOError as e: + self.logger.prn_err("Test configuration JSON file '{0}' I/O error({1}): {2}" + .format(json_test_configuration_path, e.errno, e.strerror)) + except: + self.logger.prn_err("Test configuration JSON Unexpected error:", str(e)) + raise + + def copy_image(self, image_path=None, disk=None, copy_method=None, port=None, mcu=None, retry_copy=5): + """! Closure for copy_image_raw() method. + @return Returns result from copy plugin + """ + def get_remount_count(disk_path, tries=2): + """! Get the remount count from 'DETAILS.TXT' file + @return Returns count, None if not-available + """ + + #In case of no disk path, nothing to do + if disk_path is None: + return None + + for cur_try in range(1, tries + 1): + try: + files_on_disk = [x.upper() for x in os.listdir(disk_path)] + if 'DETAILS.TXT' in files_on_disk: + with open(os.path.join(disk_path, 'DETAILS.TXT'), 'r') as details_txt: + for line in details_txt.readlines(): + if 'Remount count:' in line: + return int(line.replace('Remount count: ', '')) + # Remount count not found in file + return None + # 'DETAILS.TXT file not found + else: + return None + + except OSError as e: + self.logger.prn_err("Failed to get remount count due to OSError.", str(e)) + self.logger.prn_inf("Retrying in 1 second (try %s of %s)" % (cur_try, tries)) + sleep(1) + # Failed to get remount count + return None + + def check_flash_error(target_id, disk, initial_remount_count): + """! Check for flash errors + @return Returns false if FAIL.TXT present, else true + """ + if not target_id: + self.logger.prn_wrn("Target ID not found: Skipping flash check and retry") + return True + + if not copy_method in ["shell", "default"]: + # We're using a "copy method" that may not necessarily require + # an "Mbed Enabled" device. In this case we shouldn't use + # mbedls.detect to attempt to rediscover the mount point, as + # mbedls.detect is only compatible with Mbed Enabled devices. + # It's best just to return `True` and continue here. This will + # avoid the inevitable 2.5s delay caused by us repeatedly + # attempting to enumerate Mbed Enabled devices in the code + # below when none are connected. The user has specified a + # non-Mbed plugin copy method, so we shouldn't delay them by + # trying to check for Mbed Enabled devices. + return True + + bad_files = set(['FAIL.TXT']) + # Re-try at max 5 times with 0.5 sec in delay + for i in range(5): + # mbed_os_tools.detect.create() should be done inside the loop. Otherwise it will loop on same data. + mbeds = detect.create() + mbed_list = mbeds.list_mbeds() #list of mbeds present + # get first item in list with a matching target_id, if present + mbed_target = next((x for x in mbed_list if x['target_id']==target_id), None) + + if mbed_target is not None: + if 'mount_point' in mbed_target and mbed_target['mount_point'] is not None: + if not initial_remount_count is None: + new_remount_count = get_remount_count(disk) + if not new_remount_count is None and new_remount_count == initial_remount_count: + sleep(0.5) + continue + + common_items = [] + try: + items = set([x.upper() for x in os.listdir(mbed_target['mount_point'])]) + common_items = bad_files.intersection(items) + except OSError as e: + print("Failed to enumerate disk files, retrying") + continue + + for common_item in common_items: + full_path = os.path.join(mbed_target['mount_point'], common_item) + self.logger.prn_err("Found %s"% (full_path)) + bad_file_contents = "[failed to read bad file]" + try: + with open(full_path, "r") as bad_file: + bad_file_contents = bad_file.read() + except IOError as error: + self.logger.prn_err("Error opening '%s': %s" % (full_path, error)) + + self.logger.prn_err("Error file contents:\n%s" % bad_file_contents) + if common_items: + return False + sleep(0.5) + return True + + # Set-up closure environment + if not image_path: + image_path = self.image_path + if not disk: + disk = self.disk + if not copy_method: + copy_method = self.copy_method + if not port: + port = self.port + if not mcu: + mcu = self.mcu + if not retry_copy: + retry_copy = self.retry_copy + target_id = self.target_id + + if not image_path: + self.logger.prn_err("Error: image path not specified") + return False + + if not os.path.isfile(image_path): + self.logger.prn_err("Error: image file (%s) not found" % image_path) + return False + + for count in range(0, retry_copy): + initial_remount_count = get_remount_count(disk) + # Call proper copy method + result = self.copy_image_raw(image_path, disk, copy_method, port, mcu) + sleep(self.program_cycle_s) + if not result: + continue + result = check_flash_error(target_id, disk, initial_remount_count) + if result: + break + return result + + def copy_image_raw(self, image_path=None, disk=None, copy_method=None, port=None, mcu=None): + """! Copy file depending on method you want to use. Handles exception + and return code from shell copy commands. + @return Returns result from copy plugin + @details Method which is actually copying image to mbed + """ + # image_path - Where is binary with target's firmware + + # Select copy_method + # We override 'default' method with 'shell' method + copy_method = { + None : 'shell', + 'default' : 'shell', + }.get(copy_method, copy_method) + + result = ht_plugins.call_plugin('CopyMethod', + copy_method, + image_path=image_path, + mcu=mcu, + serial=port, + destination_disk=disk, + target_id=self.target_id, + pooling_timeout=self.polling_timeout, + format=self.options.format + ) + return result + + def hw_reset(self): + """ + Performs hardware reset of target ned device. + + :return: + """ + device_info = {} + result = ht_plugins.call_plugin('ResetMethod', + 'power_cycle', + target_id=self.target_id, + device_info=device_info, + format=self.options.format) + if result: + self.port = device_info['serial_port'] + self.disk = device_info['mount_point'] + return result diff --git a/tools/python/mbed_os_tools/test/host_tests_toolbox/__init__.py b/tools/python/mbed_os_tools/test/host_tests_toolbox/__init__.py new file mode 100644 index 0000000000..cc2c62d8d1 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_toolbox/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from .host_functional import reset_dev +from .host_functional import flash_dev +from .host_functional import handle_send_break_cmd diff --git a/tools/python/mbed_os_tools/test/host_tests_toolbox/host_functional.py b/tools/python/mbed_os_tools/test/host_tests_toolbox/host_functional.py new file mode 100644 index 0000000000..c75579e4a8 --- /dev/null +++ b/tools/python/mbed_os_tools/test/host_tests_toolbox/host_functional.py @@ -0,0 +1,152 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 sys +import json +from time import sleep +from serial import Serial, SerialException +from .. import host_tests_plugins, DEFAULT_BAUD_RATE + + +def flash_dev(disk=None, + image_path=None, + copy_method='default', + port=None, + program_cycle_s=4): + """! Flash device using pythonic interface + @param disk Switch -d + @param image_path Switch -f + @param copy_method Switch -c (default: shell) + @param port Switch -p + """ + if copy_method == 'default': + copy_method = 'shell' + result = False + result = host_tests_plugins.call_plugin('CopyMethod', + copy_method, + image_path=image_path, + serial=port, + destination_disk=disk) + sleep(program_cycle_s) + return result + +def reset_dev(port=None, + disk=None, + reset_type='default', + reset_timeout=1, + serial_port=None, + baudrate=DEFAULT_BAUD_RATE, + timeout=1, + verbose=False): + """! Reset device using pythonic interface + @param port Switch -p + @param disk Switch -d + @param reset_type Switch -r + @param reset_timeout Switch -R + @param serial_port Serial port handler, set to None if you want this function to open serial + + @param baudrate Serial port baudrate + @param timeout Serial port timeout + @param verbose Verbose mode + """ + + result = False + if not serial_port: + try: + with Serial(port, baudrate=baudrate, timeout=timeout) as serial_port: + result = host_tests_plugins.call_plugin('ResetMethod', + reset_type, + serial=serial_port, + disk=disk) + sleep(reset_timeout) + except SerialException as e: + if verbose: + print("%s" % (str(e))) + result = False + return result + +def handle_send_break_cmd(port, + disk, + reset_type=None, + baudrate=None, + timeout=1, + verbose=False): + """! Resets platforms and prints serial port output + @detail Mix with switch -r RESET_TYPE and -p PORT for versatility + """ + if not reset_type: + reset_type = 'default' + + port_config = port.split(':') + if len(port_config) == 2: + # -p COM4:115200 + port = port_config[0] + baudrate = int(port_config[1]) if not baudrate else baudrate + elif len(port_config) == 3: + # -p COM4:115200:0.5 + port = port_config[0] + baudrate = int(port_config[1]) if not baudrate else baudrate + timeout = float(port_config[2]) + + # Use default baud rate value if not set + if not baudrate: + baudrate = DEFAULT_BAUD_RATE + + if verbose: + print("mbedhtrun: serial port configuration: %s:%s:%s"% (port, str(baudrate), str(timeout))) + + try: + serial_port = Serial(port, baudrate=baudrate, timeout=timeout) + except Exception as e: + print("mbedhtrun: %s" % (str(e))) + print(json.dumps({ + "port" : port, + "disk" : disk, + "baudrate" : baudrate, + "timeout" : timeout, + "reset_type" : reset_type, + }, indent=4)) + return False + + serial_port.flush() + # Reset using one of the plugins + result = host_tests_plugins.call_plugin('ResetMethod', reset_type, serial=serial_port, disk=disk) + if not result: + print("mbedhtrun: reset plugin failed") + print(json.dumps({ + "port" : port, + "disk" : disk, + "baudrate" : baudrate, + "timeout" : timeout, + "reset_type" : reset_type + }, indent=4)) + return False + + print("mbedhtrun: serial dump started (use ctrl+c to break)") + try: + while True: + test_output = serial_port.read(512) + if test_output: + sys.stdout.write('%s'% test_output) + if "{end}" in test_output: + if verbose: + print() + print("mbedhtrun: stopped (found '{end}' terminator)") + break + except KeyboardInterrupt: + print("ctrl+c break") + + serial_port.close() + return True diff --git a/tools/python/mbed_os_tools/test/mbed_common_api.py b/tools/python/mbed_os_tools/test/mbed_common_api.py new file mode 100644 index 0000000000..0b8e3c7066 --- /dev/null +++ b/tools/python/mbed_os_tools/test/mbed_common_api.py @@ -0,0 +1,54 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from subprocess import call, Popen, PIPE + + +def run_cli_command(cmd, shell=True, verbose=False): + """! Runs command from command line + @param shell Shell command (e.g. ls, ps) + @param verbose Verbose mode flag + @return Returns (True, 0) if command was executed successfully else return (False, error code) + """ + result = True + ret = 0 + try: + ret = call(cmd, shell=shell) + if ret: + result = False + if verbose: + print("mbedgt: [ret=%d] Command: %s"% (int(ret), cmd)) + except OSError as e: + result = False + if verbose: + print("mbedgt: [ret=%d] Command: %s"% (int(ret), cmd)) + print(str(e)) + return (result, ret) + +def run_cli_process(cmd): + """! Runs command as a process and return stdout, stderr and ret code + @param cmd Command to execute + @return Tuple of (stdout, stderr, returncode) + """ + try: + p = Popen(cmd, stdout=PIPE, stderr=PIPE) + _stdout, _stderr = p.communicate() + except OSError as e: + print("mbedgt: Command: %s"% (cmd)) + print(str(e)) + print("mbedgt: traceback...") + print(e.child_traceback) + return str(), str(), -1 + return _stdout, _stderr, p.returncode diff --git a/tools/python/mbed_os_tools/test/mbed_coverage_api.py b/tools/python/mbed_os_tools/test/mbed_coverage_api.py new file mode 100644 index 0000000000..d05f50dceb --- /dev/null +++ b/tools/python/mbed_os_tools/test/mbed_coverage_api.py @@ -0,0 +1,66 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 + +""" +def __default_coverage_start_callback(self, key, value, timestamp): + # {{__coverage_start;PATH;PAYLOAD}} + # PAYLAODED is HEX coded string + lcov_path, lcov_payload = value.split(';') + try: + bin_payload = coverage_pack_hex_payload(lcov_payload) + coverage_dump_file(lcov_path, bin_payload) + + self.log("dumped %d bytes to '%s'"% (len(bin_payload), lcov_path)) + except Exception as e: + self.log("LCOV:" + str(e)) +""" + +def coverage_pack_hex_payload(payload): + """! Convert a block of hex string data back to binary and return the binary data + @param payload String with hex encoded ascii data, e.g.: '6164636772...' + @return bytearray with payload with data + """ + # This payload might be packed with dot compression + # where byte value 0x00 is coded as ".", and not as "00" + payload = payload.replace('.', '00') + + hex_pairs = map(''.join, zip(*[iter(payload)] * 2)) # ['61', '64', '63', '67', '72', ... ] + bin_payload = bytearray([int(s, 16) for s in hex_pairs]) + return bin_payload + + +def coverage_dump_file(build_path, path, payload): + """! Creates file and dumps payload to it on specified path (even if path doesn't exist) + @param path Path to file + @param payload Binary data to store in a file + @return True if operation was completed + """ + result = True + try: + d, filename = os.path.split(path) + if not os.path.isabs(d) and not os.path.exists(d): + # For a relative path that do not exist. Try adding ./build/ prefix + d = build_path + path = os.path.join(d, filename) + if not os.path.exists(d): + os.makedirs(d) + with open(path, "wb") as f: + f.write(payload) + except IOError as e: + print(str(e)) + result = False + return result diff --git a/tools/python/mbed_os_tools/test/mbed_greentea_cli.py b/tools/python/mbed_os_tools/test/mbed_greentea_cli.py new file mode 100644 index 0000000000..06cc6dd5a0 --- /dev/null +++ b/tools/python/mbed_os_tools/test/mbed_greentea_cli.py @@ -0,0 +1,102 @@ + +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 fnmatch + +from .cmake_handlers import list_binaries_for_builds, list_binaries_for_targets +from .mbed_greentea_log import gt_logger + +RET_NO_DEVICES = 1001 +RET_YOTTA_BUILD_FAIL = -1 +LOCAL_HOST_TESTS_DIR = './test/host_tests' # Used by mbedhtrun -e + + +def get_local_host_tests_dir(path): + """! Forms path to local host tests. Performs additional basic checks if directory exists etc. + """ + # If specified path exist return path + if path and os.path.exists(path) and os.path.isdir(path): + return path + # If specified path is not set or doesn't exist returns default path + if not path and os.path.exists(LOCAL_HOST_TESTS_DIR) and os.path.isdir(LOCAL_HOST_TESTS_DIR): + return LOCAL_HOST_TESTS_DIR + return None + +def create_filtered_test_list(ctest_test_list, test_by_names, skip_test, test_spec=None): + """! Filters test case list (filtered with switch -n) and return filtered list. + @ctest_test_list List iof tests, originally from CTestTestFile.cmake in yotta module. Now comes from test specification + @test_by_names Command line switch -n + @skip_test Command line switch -i + @param test_spec Test specification object loaded with --test-spec switch + @return + """ + + filtered_ctest_test_list = ctest_test_list + test_list = None + invalid_test_names = [] + if filtered_ctest_test_list is None: + return {} + + if test_by_names: + filtered_ctest_test_list = {} # Subset of 'ctest_test_list' + test_list = test_by_names.lower().split(',') + gt_logger.gt_log("test case filter (specified with -n option)") + + for test_name in set(test_list): + gt_logger.gt_log_tab(test_name) + matches = [test for test in ctest_test_list.keys() if fnmatch.fnmatch(test, test_name)] + if matches: + for match in matches: + gt_logger.gt_log_tab("test filtered in '%s'"% gt_logger.gt_bright(match)) + filtered_ctest_test_list[match] = ctest_test_list[match] + else: + invalid_test_names.append(test_name) + + if skip_test: + test_list = skip_test.split(',') + gt_logger.gt_log("test case filter (specified with -i option)") + + for test_name in set(test_list): + gt_logger.gt_log_tab(test_name) + matches = [test for test in filtered_ctest_test_list.keys() if fnmatch.fnmatch(test, test_name)] + if matches: + for match in matches: + gt_logger.gt_log_tab("test filtered out '%s'"% gt_logger.gt_bright(match)) + del filtered_ctest_test_list[match] + else: + invalid_test_names.append(test_name) + + if invalid_test_names: + opt_to_print = '-n' if test_by_names else 'skip-test' + gt_logger.gt_log_warn("invalid test case names (specified with '%s' option)"% opt_to_print) + for test_name in invalid_test_names: + if test_spec: + test_spec_name = test_spec.test_spec_filename + gt_logger.gt_log_warn("test name '%s' not found in '%s' (specified with --test-spec option)"% (gt_logger.gt_bright(test_name), + gt_logger.gt_bright(test_spec_name))) + else: + gt_logger.gt_log_warn("test name '%s' not found in CTestTestFile.cmake (specified with '%s' option)"% (gt_logger.gt_bright(test_name), + opt_to_print)) + gt_logger.gt_log_tab("note: test case names are case sensitive") + gt_logger.gt_log_tab("note: see list of available test cases below") + # Print available test suite names (binary names user can use with -n + if test_spec: + list_binaries_for_builds(test_spec) + else: + list_binaries_for_targets() + + return filtered_ctest_test_list diff --git a/tools/python/mbed_os_tools/test/mbed_greentea_dlm.py b/tools/python/mbed_os_tools/test/mbed_greentea_dlm.py new file mode 100644 index 0000000000..59bdace099 --- /dev/null +++ b/tools/python/mbed_os_tools/test/mbed_greentea_dlm.py @@ -0,0 +1,169 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 json +import uuid +import lockfile +from os.path import expanduser + +HOME_DIR = expanduser("~") +GREENTEA_HOME_DIR = ".mbed-greentea" +GREENTEA_GLOBAL_LOCK = "glock.lock" +GREENTEA_KETTLE = "kettle.json" # active Greentea instances +GREENTEA_KETTLE_PATH = os.path.join(HOME_DIR, GREENTEA_HOME_DIR, GREENTEA_KETTLE) + + +def greentea_home_dir_init(): + """ Initialize data in home directory for locking features + """ + if not os.path.isdir(os.path.join(HOME_DIR, GREENTEA_HOME_DIR)): + os.mkdir(os.path.join(HOME_DIR, GREENTEA_HOME_DIR)) + +def greentea_get_app_sem(): + """ Obtain locking mechanism info + """ + greentea_home_dir_init() + gt_instance_uuid = str(uuid.uuid4()) # String version + gt_file_sem_name = os.path.join(HOME_DIR, GREENTEA_HOME_DIR, gt_instance_uuid) + gt_file_sem = lockfile.LockFile(gt_file_sem_name) + return gt_file_sem, gt_file_sem_name, gt_instance_uuid + +def greentea_get_target_lock(target_id): + greentea_home_dir_init() + file_path = os.path.join(HOME_DIR, GREENTEA_HOME_DIR, target_id) + lock = lockfile.LockFile(file_path) + return lock + +def greentea_get_global_lock(): + greentea_home_dir_init() + file_path = os.path.join(HOME_DIR, GREENTEA_HOME_DIR, GREENTEA_GLOBAL_LOCK) + lock = lockfile.LockFile(file_path) + return lock + +def greentea_update_kettle(greentea_uuid): + from time import gmtime, strftime + + with greentea_get_global_lock(): + current_brew = get_json_data_from_file(GREENTEA_KETTLE_PATH) + if not current_brew: + current_brew = {} + current_brew[greentea_uuid] = { + "start_time" : strftime("%Y-%m-%d %H:%M:%S", gmtime()), + "cwd" : os.getcwd(), + "locks" : [] + } + with open(GREENTEA_KETTLE_PATH, 'w') as kettle_file: + json.dump(current_brew, kettle_file, indent=4) + +def greentea_clean_kettle(greentea_uuid): + """ Clean info in local file system config file + """ + with greentea_get_global_lock(): + current_brew = get_json_data_from_file(GREENTEA_KETTLE_PATH) + if not current_brew: + current_brew = {} + current_brew.pop(greentea_uuid, None) + with open(GREENTEA_KETTLE_PATH, 'w') as kettle_file: + json.dump(current_brew, kettle_file, indent=4) + +def greentea_acquire_target_id(target_id, gt_instance_uuid): + """ Acquire lock on target_id for given greentea UUID + """ + with greentea_get_global_lock(): + current_brew = get_json_data_from_file(GREENTEA_KETTLE_PATH) + if current_brew: + current_brew[gt_instance_uuid]['locks'].append(target_id) + with open(GREENTEA_KETTLE_PATH, 'w') as kettle_file: + json.dump(current_brew, kettle_file, indent=4) + +def greentea_acquire_target_id_from_list(possible_target_ids, gt_instance_uuid): + """ Acquire lock on target_id from list of possible target_ids for given greentea UUID + """ + target_id = None + already_locked_target_ids = [] + with greentea_get_global_lock(): + current_brew = get_json_data_from_file(GREENTEA_KETTLE_PATH) + # Get all already locked target_id + for cb in current_brew: + locks_list = current_brew[cb]['locks'] + already_locked_target_ids.extend(locks_list) + + # Remove from possible_target_ids elements from already_locked_target_ids + available_target_ids = [item for item in possible_target_ids if item not in already_locked_target_ids] + + if available_target_ids: + target_id = available_target_ids[0] + current_brew[gt_instance_uuid]['locks'].append(target_id) + with open(GREENTEA_KETTLE_PATH, 'w') as kettle_file: + json.dump(current_brew, kettle_file, indent=4) + return target_id + +def greentea_release_target_id(target_id, gt_instance_uuid): + """ Release target_id for given greentea UUID + """ + with greentea_get_global_lock(): + current_brew = get_json_data_from_file(GREENTEA_KETTLE_PATH) + if current_brew: + current_brew[gt_instance_uuid]['locks'].remove(target_id) + with open(GREENTEA_KETTLE_PATH, 'w') as kettle_file: + json.dump(current_brew, kettle_file, indent=4) + +def get_json_data_from_file(json_spec_filename): + """ Loads from file JSON formatted string to data structure + """ + result = None + try: + with open(json_spec_filename, 'r') as data_file: + try: + result = json.load(data_file) + except ValueError: + result = None + except IOError: + result = None + return result + +def greentea_kettle_info(): + """ generates human friendly info about current kettle state + + @details + { + "475a46d0-41fe-41dc-b5e6-5197a2fcbb28": { + "locks": [], + "start_time": "2015-10-23 09:29:54", + "cwd": "c:\\Work\\mbed-drivers" + } + } + """ + from prettytable import PrettyTable + with greentea_get_global_lock(): + current_brew = get_json_data_from_file(GREENTEA_KETTLE_PATH) + cols = ['greentea_uuid', 'start_time', 'cwd', 'locks'] + pt = PrettyTable(cols) + + for col in cols: + pt.align[col] = "l" + pt.padding_width = 1 # One space between column edges and contents (default) + + row = [] + for greentea_uuid in current_brew: + kettle = current_brew[greentea_uuid] + row.append(greentea_uuid) + row.append(kettle['start_time']) + row.append(kettle['cwd']) + row.append('\n'.join(kettle['locks'])) + pt.add_row(row) + row = [] + return pt.get_string() diff --git a/tools/python/mbed_os_tools/test/mbed_greentea_hooks.py b/tools/python/mbed_os_tools/test/mbed_greentea_hooks.py new file mode 100644 index 0000000000..0d0fd695b8 --- /dev/null +++ b/tools/python/mbed_os_tools/test/mbed_greentea_hooks.py @@ -0,0 +1,246 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +import json +from subprocess import Popen, PIPE +from .mbed_greentea_log import gt_logger + +""" +List of available hooks: +""" + + +class GreenteaTestHook(object): + """! Class used to define + """ + name = None + + def __init__(self, name): + self.name = name + + def run(self, format=None): + pass + +class GreenteaCliTestHook(GreenteaTestHook): + """! Class used to define a hook which will call command line program + """ + cmd = None + + def __init__(self, name, cmd): + GreenteaTestHook.__init__(self, name) + self.cmd = cmd + + def run_cli_process(self, cmd): + """! Runs command as a process and return stdout, stderr and ret code + @param cmd Command to execute + @return Tuple of (stdout, stderr, returncode) + """ + _stdout, _stderr, ret = None, None, -1 + try: + p = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True) + _stdout, _stderr = p.communicate() + ret = p.returncode + except OSError as e: + gt_logger.gt_log_err(str(e)) + ret = -1 + return _stdout, _stderr, ret + + def run(self, format=None): + """! Runs hook after command is formated with in-place {tags} + @format Pass format dictionary to replace hook {tags} with real values + @param format Used to format string with cmd, notation used is e.g: {build_name} + """ + gt_logger.gt_log("hook '%s' execution"% self.name) + cmd = self.format_before_run(self.cmd, format) + gt_logger.gt_log_tab("hook command: %s"% cmd) + (_stdout, _stderr, ret) = self.run_cli_process(cmd) + if _stdout: + print(_stdout) + if ret: + gt_logger.gt_log_err("hook exited with error: %d, dumping stderr..."% ret) + print(_stderr) + return ret + + @staticmethod + def format_before_run(cmd, format, verbose=False): + if format: + # We will expand first + cmd_expand = GreenteaCliTestHook.expand_parameters(cmd, format) + if cmd_expand: + cmd = cmd_expand + if verbose: + gt_logger.gt_log_tab("hook expanded: %s"% cmd) + + cmd = cmd.format(**format) + if verbose: + gt_logger.gt_log_tab("hook formated: %s"% cmd) + return cmd + + @staticmethod + def expand_parameters(expr, expandables, delimiter=' '): + """! Expands lists for multiple parameters in hook command + @param expr Expression to expand + @param expandables Dictionary of token: list_to_expand See details for more info + @param delimiter Delimiter used to combine expanded strings, space by default + @details + test_name_list = ['mbed-drivers-test-basic', 'mbed-drivers-test-hello', 'mbed-drivers-test-time_us'] + build_path_list = ['./build/frdm-k64f-gcc', './build/frdm-k64f-armcc'] + expandables = { + "{test_name_list}": test_name_list, + "{build_path_list}": build_path_list + } + expr = "lcov --gcov-tool arm-none-eabi-gcov [-a {build_path_list}/test/{test_name_list}.info] --output-file result.info" + 'expr' expression [-a {build_path_list}/test/{test_name_list}.info] will expand to: + [ + "-a ./build/frdm-k64f-gcc/test/mbed-drivers-test-basic.info", + "-a ./build/frdm-k64f-armcc/test/mbed-drivers-test-basic.info", + "-a ./build/frdm-k64f-gcc/test/mbed-drivers-test-hello.info", + "-a ./build/frdm-k64f-armcc/test/mbed-drivers-test-hello.info", + "-a ./build/frdm-k64f-gcc/test/mbed-drivers-test-time_us.info", + "-a ./build/frdm-k64f-armcc/test/mbed-drivers-test-time_us.info" + ] + """ + result = None + if expandables: + expansion_result = [] + m = re.search('\[.*?\]', expr) + if m: + expr_str_orig = m.group(0) + expr_str_base = m.group(0)[1:-1] + expr_str_list = [expr_str_base] + for token in expandables: + # We will expand only values which are lists (of strings) + if type(expandables[token]) is list: + # Use tokens with curly braces (Python string format like) + format_token = '{' + token + '}' + for expr_str in expr_str_list: + if format_token in expr_str: + patterns = expandables[token] + for pattern in patterns: + s = expr_str + s = s.replace(format_token, pattern) + expr_str_list.append(s) + # Nothing to extend/change in this string + if not any('{' + p + '}' in s for p in expandables.keys() if type(expandables[p]) is list): + expansion_result.append(s) + expansion_result.sort() + result = expr.replace(expr_str_orig, delimiter.join(expansion_result)) + return result + +class LcovHook(GreenteaCliTestHook): + """! Class used to define a LCOV hook + """ + lcov_hooks = { + "hooks": { + "hook_test_end": "$lcov --gcov-tool gcov --capture --directory ./build --output-file {build_path}/{test_name}.info", + "hook_post_all_test_end": "$lcov --gcov-tool gcov [(-a << {build_path}/{test_name_list}.info>>)] --output-file result.info" + } + } + + def __init__(self, name, cmd): + GreenteaCliTestHook.__init__(self, name, cmd) + + @staticmethod + def format_before_run(cmd, format, verbose=False): + if format: + # We will expand first + cmd_expand = GreenteaCliTestHook.expand_parameters(cmd, format) + if cmd_expand: + cmd = cmd_expand + if verbose: + gt_logger.gt_log_tab("hook expanded: %s"% cmd) + + cmd = cmd.format(**format) + cmd = LcovHook.check_if_file_exists_or_is_empty(cmd) + if verbose: + gt_logger.gt_log_tab("hook formated: %s"% cmd) + return cmd + + @staticmethod + def check_if_file_exists_or_is_empty(expr): + """! Check Expression for specific characters in hook command + @param expr Expression to check + @details + expr = "lcov --gcov-tool gcov (-a <<{build_path}/test/{test_name}.info>>) --output-file result.info" + where: + (...) -> specify part to check + <<...>> -> specify part which is a path to a file + 'expr' expression (-a <<{build_path}/test/{test_name}.info>>) will be either: + "-a ./build/frdm-k64f-gcc/test/test_name.info" + or will be removed from command + It is also possible to use it in combination with expand_parameters: + expr = "lcov --gcov-tool gcov [(-a <<./build/{yotta_target_name}/{test_name_list}.info>>)] --output-file result.info" + """ + result = expr + expr_strs_orig = re.findall('\(.*?\)', expr) + for expr_str_orig in expr_strs_orig: + expr_str_base = expr_str_orig[1:-1] + result = result.replace(expr_str_orig, expr_str_base) + m = re.search('\<<.*?\>>', expr_str_base) + if m: + expr_str_path = m.group(0)[2:-2] + # Remove option if file not exists OR if file exists but empty + if not os.path.exists(expr_str_path): + result = result.replace(expr_str_base, '') + elif os.path.getsize(expr_str_path) == 0: + result = result.replace(expr_str_base, '') + + # Remove path limiter + result = result.replace('<<', '') + result = result.replace('>>', '') + return result + +class GreenteaHooks(object): + """! Class used to store all hooks + @details Hooks command starts with '$' dollar sign + """ + HOOKS = {} + def __init__(self, path_to_hooks): + """! Opens JSON file with + """ + try: + if path_to_hooks == 'lcov': + hooks = LcovHook.lcov_hooks + for hook in hooks['hooks']: + hook_name = hook + hook_expression = hooks['hooks'][hook] + self.HOOKS[hook_name] = LcovHook(hook_name, hook_expression[1:]) + else: + with open(path_to_hooks, 'r') as data_file: + hooks = json.load(data_file) + if 'hooks' in hooks: + for hook in hooks['hooks']: + hook_name = hook + hook_expression = hooks['hooks'][hook] + # This is a command line hook + if hook_expression.startswith('$'): + self.HOOKS[hook_name] = GreenteaCliTestHook(hook_name, hook_expression[1:]) + except IOError as e: + print(str(e)) + self.HOOKS = None + + def is_hooked_to(self, hook_name): + return hook_name in self.HOOKS + + def run_hook(self, hook_name, format): + if hook_name in self.HOOKS: + return self.HOOKS[hook_name].run(format) + + def run_hook_ext(self, hook_name, format): + if self.is_hooked_to(hook_name): + # We can execute this test hook just after all tests are finished ('hook_post_test_end') + self.run_hook(hook_name, format) diff --git a/tools/python/mbed_os_tools/test/mbed_greentea_log.py b/tools/python/mbed_os_tools/test/mbed_greentea_log.py new file mode 100644 index 0000000000..b82cfd2dba --- /dev/null +++ b/tools/python/mbed_os_tools/test/mbed_greentea_log.py @@ -0,0 +1,142 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from threading import Lock + +try: + import colorama + COLORAMA = True +except ImportError: + COLORAMA = False + + +class GreenTeaSimpleLockLogger(object): + """! Simple locking printing mechanism + @details We are using parallel testing + """ + # Colors used by color(ama) terminal component + DIM = str() + BRIGHT = str() + GREEN = str() + RED = str() + BLUE = str() + YELLOW = str() + RESET = str() + + def __init__(self, colors=True, use_colorama=False): + self.use_colorama = colorama # Should we try to use colorama + self.colorful(colors) # Set and use colours for formatting + + # Mutext used to protect logger prints + # Usage: + # GREENTEA_LOG_MUTEX.acquire(1) + # GREENTEA_LOG_MUTEX.release() + self.GREENTEA_LOG_MUTEX = Lock() + + if self.colors: + if not self.use_colorama: + self.gt_log("Colorful console output is disabled") + else: + colorama.init() + + def colorful(self, colors): + """! Enable/Disable colourful printing + """ + self.colors = colors + if self.colors: + self.__set_colors() + else: + self.__clear_colors() + + def __set_colors(self): + """! Zeroes colours used for formatting + """ + if self.use_colorama: + self.DIM = colorama.Style.DIM + self.BRIGHT = colorama.Style.BRIGHT + self.GREEN = colorama.Fore.GREEN + self.RED = colorama.Fore.RED + self.BLUE = colorama.Fore.BLUE + self.YELLOW = colorama.Fore.YELLOW + self.RESET = colorama.Style.RESET_ALL + + def __clear_colors(self): + """! Zeroes colours used for formatting + """ + self.DIM = str() + self.BRIGHT = str() + self.GREEN = str() + self.RED = str() + self.BLUE = str() + self.YELLOW = str() + self.RESET = str() + + def __print(self, text): + """! Mutex protected print + """ + self.GREENTEA_LOG_MUTEX.acquire(1) + print(text) + self.GREENTEA_LOG_MUTEX.release() + + def gt_log(self, text, print_text=True): + """! Prints standard log message (in colour if colorama is installed) + @param print_text Forces log function to print on screen (not only return message) + @return Returns string with message + """ + result = self.GREEN + self.BRIGHT + "mbedgt: " + self.RESET + text + if print_text: + self.__print(result) + return result + + def gt_log_tab(self, text, tab_count=1, print_text=True): + """! Prints standard log message with one (1) tab margin on the left + @param tab_count How many tags should be added (indent level) + @param print_text Forces log function to print on screen (not only return message) + @return Returns string with message + """ + result = "\t"*tab_count + text + if print_text: + self.__print(result) + return result + + def gt_log_err(self, text, print_text=True): + """! Prints error log message (in color if colorama is installed) + @param print_text Forces log function to print on screen (not only return message) + @return Returns string with message + """ + result = self.RED + self.BRIGHT + "mbedgt: " + self.RESET + text + if print_text: + self.__print(result) + return result + + def gt_log_warn(self, text, print_text=True): + """! Prints error log message (in color if colorama is installed) + @param print_text Forces log function to print on screen (not only return message) + @return Returns string with message + """ + result = self.YELLOW + "mbedgt: " + self.RESET + text + if print_text: + self.__print(result) + return result + + def gt_bright(self, text): + """! Created bright text using colorama + @return Returns string with additional BRIGHT color codes + """ + if not text: + text = '' + return self.BLUE + self.BRIGHT + text + self.RESET + +gt_logger = GreenTeaSimpleLockLogger(use_colorama=COLORAMA) diff --git a/tools/python/mbed_os_tools/test/mbed_report_api.py b/tools/python/mbed_os_tools/test/mbed_report_api.py new file mode 100644 index 0000000000..4d303e074a --- /dev/null +++ b/tools/python/mbed_os_tools/test/mbed_report_api.py @@ -0,0 +1,809 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +def export_to_file(file_name, payload): + """! Simple file dump used to store reports on disk + @param file_name Report file name (with path if needed) + @param payload Data to store inside file + @return True if report save was successful + """ + result = True + try: + with open(file_name, 'w') as f: + f.write(payload) + except IOError as e: + print("Exporting report to file failed: %s" % str(e)) + result = False + return result + + +def exporter_json(test_result_ext, test_suite_properties=None): + """! Exports test results to indented JSON format + @details This is a machine friendly format + """ + import json + for target in test_result_ext.values(): + for suite in target.values(): + try: + suite["single_test_output"] = suite["single_test_output"] + except KeyError: + pass + return json.dumps(test_result_ext, indent=4) + + +def exporter_text(test_result_ext, test_suite_properties=None): + """! Exports test results to text formatted output + @details This is a human friendly format + @return Tuple with table of results and result quantity summary string + """ + from prettytable import PrettyTable, HEADER + #TODO: export to text, preferably to PrettyTable (SQL like) format + cols = ['target', 'platform_name', 'test suite', 'result', 'elapsed_time (sec)', 'copy_method'] + pt = PrettyTable(cols, junction_char="|", hrules=HEADER) + for col in cols: + pt.align[col] = "l" + pt.padding_width = 1 # One space between column edges and contents (default) + + result_dict = {} # Used to print test suite results + + for target_name in sorted(test_result_ext): + test_results = test_result_ext[target_name] + row = [] + for test_name in sorted(test_results): + test = test_results[test_name] + + # Grab quantity of each test result + if test['single_test_result'] in result_dict: + result_dict[test['single_test_result']] += 1 + else: + result_dict[test['single_test_result']] = 1 + + row.append(target_name) + row.append(test['platform_name']) + row.append(test_name) + row.append(test['single_test_result']) + row.append(round(test['elapsed_time'], 2)) + row.append(test['copy_method']) + pt.add_row(row) + row = [] + + result_pt = pt.get_string() + result_res = ' / '.join(['%s %s' % (value, key) for (key, value) in {k: v for k, v in result_dict.items() if v != 0}.items()]) + return result_pt, result_res + +def exporter_testcase_text(test_result_ext, test_suite_properties=None): + """! Exports test case results to text formatted output + @param test_result_ext Extended report from Greentea + @param test_suite_properties Data from yotta module.json file + @details This is a human friendly format + @return Tuple with table of results and result quantity summary string + """ + from prettytable import PrettyTable, HEADER + #TODO: export to text, preferably to PrettyTable (SQL like) format + cols = ['target', 'platform_name', 'test suite', 'test case', 'passed', 'failed', 'result', 'elapsed_time (sec)'] + pt = PrettyTable(cols, junction_char="|", hrules=HEADER) + for col in cols: + pt.align[col] = "l" + pt.padding_width = 1 # One space between column edges and contents (default) + + result_testcase_dict = {} # Used to print test case results + + for target_name in sorted(test_result_ext): + test_results = test_result_ext[target_name] + row = [] + for test_suite_name in sorted(test_results): + test = test_results[test_suite_name] + + # testcase_result stores info about test case results + testcase_result = test['testcase_result'] + # "testcase_result": { + # "STRINGS004": { + # "duration": 0.009999990463256836, + # "time_start": 1453073018.275, + # "time_end": 1453073018.285, + # "result": 1 + # }, + + for tc_name in sorted(testcase_result): + duration = testcase_result[tc_name].get('duration', 0.0) + # result = testcase_result[tc_name].get('result', 0) + passed = testcase_result[tc_name].get('passed', 0) + failed = testcase_result[tc_name].get('failed', 0) + result_text = testcase_result[tc_name].get('result_text', "UNDEF") + + # Grab quantity of each test result + if result_text in result_testcase_dict: + result_testcase_dict[result_text] += 1 + else: + result_testcase_dict[result_text] = 1 + + row.append(target_name) + row.append(test['platform_name']) + row.append(test_suite_name) + row.append(tc_name) + row.append(passed) + row.append(failed) + row.append(result_text) + row.append(round(duration, 2)) + pt.add_row(row) + row = [] + + result_pt = pt.get_string() + result_res = ' / '.join(['%s %s' % (value, key) for (key, value) in {k: v for k, v in result_testcase_dict.items() if v != 0}.items()]) + return result_pt, result_res + +def exporter_testcase_junit(test_result_ext, test_suite_properties=None): + """! Export test results in JUnit XML compliant format + @param test_result_ext Extended report from Greentea + @param test_spec Dictionary of test build names to test suite properties + @details This function will import junit_xml library to perform report conversion + @return String containing Junit XML formatted test result output + """ + from junit_xml import TestSuite, TestCase + import sys + + test_suites = [] + + for target_name in test_result_ext: + test_results = test_result_ext[target_name] + for test_suite_name in test_results: + test = test_results[test_suite_name] + tc_stdout = test['single_test_output'] + + # testcase_result stores info about test case results + testcase_result = test['testcase_result'] + # "testcase_result": { + # "STRINGS004": { + # "duration": 0.009999990463256836, + # "time_start": 1453073018.275, + # "time_end": 1453073018.285, + # "result": 1 + # }, + + test_cases = [] + + for tc_name in sorted(testcase_result.keys()): + duration = testcase_result[tc_name].get('duration', 0.0) + utest_log = testcase_result[tc_name].get('utest_log', '') + result_text = testcase_result[tc_name].get('result_text', "UNDEF") + + tc_stderr = '\n'.join(utest_log) + tc_class = target_name + '.' + test_suite_name + + if result_text == 'SKIPPED': + # Skipped test cases do not have logs and we do not want to put + # whole log inside JUNIT for skipped test case + tc_stderr = str() + + tc = TestCase(tc_name, tc_class, duration, tc_stdout, tc_stderr) + + if result_text == 'FAIL': + tc.add_failure_info(result_text) + elif result_text == 'SKIPPED': + tc.add_skipped_info(result_text) + elif result_text != 'OK': + tc.add_error_info(result_text) + + test_cases.append(tc) + + ts_name = target_name + + if test_suite_properties is not None: + test_build_properties = test_suite_properties.get(target_name, None) + else: + test_build_properties = None + + ts = TestSuite(ts_name, test_cases, properties=test_build_properties) + test_suites.append(ts) + + return TestSuite.to_xml_string(test_suites) + +html_template = """ + + + + mbed Greentea Results Report + + + + + +
+ +
+

mbed Greentea Results Report

+
+ +
+ +
+ + + + + + %s +
+
+ +
+ +
+ + +""" + +TEST_RESULT_COLOURS = { + 'OK': "limegreen", + 'FAIL': "darkorange", + 'ERROR': "orangered", + 'SKIPPED': "lightsteelblue", + 'UNDEF': "Red", + 'IOERR_COPY': "DarkSalmon", + 'IOERR_DISK': "DarkSalmon", + 'IOERR_SERIAL': "DarkSalmon", + 'TIMEOUT': "DarkKhaki", + 'NO_IMAGE': "DarkSalmon", + 'NOT_RAN': 'grey' + # 'MBED_ASSERT': "", + # 'BUILD_FAILED': "", +} + +TEST_RESULT_DEFAULT_COLOUR = "lavender" + +def get_result_colour_class_css(): + """! Get the CSS for the colour classes + @details Returns a string of the CSS classes that are used to colour the different results + @return String containing the CSS classes + """ + + colour_class_template = """ + + .%s { + background-color: %s; + }""" + + # Create CSS classes for all of the allocated colours + css = "" + for result, colour in TEST_RESULT_COLOURS.items(): + css += colour_class_template % ("result-%s" % result.lower().replace("_", "-"), + colour) + + css += colour_class_template % ("result-other", + TEST_RESULT_DEFAULT_COLOUR) + + return css + +def get_result_colour_class(result): + """! Get the CSS colour class representing the result + @param result The result of the test + @details Returns a string of the CSS colour class of the result, or returns the default if the result is not found + @return String containing the CSS colour class + """ + + if result in TEST_RESULT_COLOURS: + return "result-%s" % result.lower().replace("_", "-") + else: + return "result-other" + +def get_dropdown_html(div_id, dropdown_name, content, title_classes="", output_text=False, sub_dropdown=False): + """! Get the HTML for a dropdown menu + @param title_classes A space separated string of css classes on the title + @param div_id The id of the dropdowns menus inner div + @param dropdown_name The name on the dropdown menu + @param dropdown_classes A space separated string of css classes on the inner div + @param content The content inside of the dropdown menu + @details This function will create the HTML for a dropdown menu + @return String containing the HTML of dropdown menu + """ + + dropdown_template = """ +
+ + +
""" + + dropdown_classes = "" + if output_text: + dropdown_classes += " output-text" + if sub_dropdown: + dropdown_classes += " sub-dropdown-content" + + return dropdown_template % (title_classes, + div_id, + dropdown_name, + div_id, + dropdown_classes, + content) + +def get_result_overlay_testcase_dropdown(result_div_id, index, testcase_result_name, testcase_result): + """! Get the HTML for an individual testcase dropdown + @param result_div_id The div id used for the test + @param index The index of the testcase for the divs unique id + @param testcase_result_name The name of the testcase + @param testcase_result The results of the testcase + @details This function will create the HTML for a testcase dropdown + @return String containing the HTML of the testcases dropdown + """ + + import datetime + + testcase_result_template = """Result: %s + Elapsed Time: %.2f + Start Time: %s + End Time: %s + Failed: %d + Passed: %d +
%s""" + + # Create unique ids to reference the divs + testcase_div_id = "%s_testcase_result_%d" % (result_div_id, index) + testcase_utest_div_id = "%s_testcase_result_%d_utest" % (result_div_id, index) + + testcase_utest_log_dropdown = get_dropdown_html(testcase_utest_div_id, + "uTest Log", + "\n".join(testcase_result.get('utest_log', 'n/a')).rstrip("\n"), + output_text=True, + sub_dropdown=True) + + time_start = 'n/a' + time_end = 'n/a' + if 'time_start' in testcase_result.keys(): + time_start = datetime.datetime.fromtimestamp(testcase_result['time_start']).strftime('%d-%m-%Y %H:%M:%S.%f') + if 'time_end' in testcase_result.keys(): + time_end = datetime.datetime.fromtimestamp(testcase_result['time_end']).strftime('%d-%m-%Y %H:%M:%S.%f') + + testcase_info = testcase_result_template % (testcase_result.get('result_text', 'n/a'), + testcase_result.get('duration', 'n/a'), + time_start, + time_end, + testcase_result.get('failed', 'n/a'), + testcase_result.get('passed', 'n/a'), + testcase_utest_log_dropdown) + + testcase_class = get_result_colour_class(testcase_result['result_text']) + testcase_dropdown = get_dropdown_html(testcase_div_id, + "Testcase: %s
" % testcase_result_name, + testcase_info, + title_classes=testcase_class, + sub_dropdown=True) + return testcase_dropdown + + +def get_result_overlay_testcases_dropdown_menu(result_div_id, test_results): + """! Get the HTML for a test overlay's testcase dropdown menu + @param result_div_id The div id used for the test + @param test_results The results of the test + @details This function will create the HTML for the result overlay's testcases dropdown menu + @return String containing the HTML test overlay's testcase dropdown menu + """ + + testcase_results_div_id = "%s_testcase" % result_div_id + testcase_results_info = "" + + # Loop through the test cases giving them a number to create a unique id + for index, (testcase_result_name, testcase_result) in enumerate(test_results['testcase_result'].items()): + testcase_results_info += get_result_overlay_testcase_dropdown(result_div_id, index, testcase_result_name, testcase_result) + + result_testcases_dropdown = get_dropdown_html(testcase_results_div_id, + "Testcase Results", + testcase_results_info, + sub_dropdown=True) + + return result_testcases_dropdown + +def get_result_overlay_dropdowns(result_div_id, test_results): + """! Get the HTML for a test overlay's dropdown menus + @param result_div_id The div id used for the test + @param test_results The results of the test + @details This function will create the HTML for the dropdown menus of an overlay + @return String containing the HTML test overlay's dropdowns + """ + + # The HTML for the dropdown containing the ouput of the test + result_output_div_id = "%s_output" % result_div_id + result_output_dropdown = get_dropdown_html( + result_output_div_id, "Test Output", + test_results['single_test_output'].rstrip("\n"), + output_text=True + ) + + # Add a dropdown for the testcases if they are present + if len(test_results) > 0: + result_overlay_dropdowns = result_output_dropdown + get_result_overlay_testcases_dropdown_menu(result_div_id, test_results) + else: + result_overlay_dropdowns = result_output_dropdown + + return result_overlay_dropdowns + +def get_result_overlay(result_div_id, test_name, platform, toolchain, test_results): + """! Get the HTML for a test's overlay + @param result_div_id The div id used for the test + @param test_name The name of the test the overlay is for + @param platform The name of the platform the test was performed on + @param toolchain The name of toolchain the test was performed on + @param test_results The results of the test + @details This function will create the HTML of an overlay to display additional information on a test + @return String containing the HTML test overlay + """ + + overlay_template = """
+
+

+ Test: %s x + Result: %s
+ Platform: %s - Toolchain: %s + Elapsed Time: %.2f seconds + Build Path: %s + Absolute Build Path: %s + Copy Method: %s + Image Path: %s +

%s +
+
""" + + overlay_dropdowns = get_result_overlay_dropdowns(result_div_id, test_results) + + return overlay_template % (result_div_id, + test_name, + result_div_id, + test_results['single_test_result'], + platform, + toolchain, + test_results['elapsed_time'], + test_results['build_path'], + test_results['build_path_abs'], + test_results['copy_method'], + test_results['image_path'], + overlay_dropdowns) + +def exporter_html(test_result_ext, test_suite_properties=None): + """! Export test results as HTML + @param test_result_ext Extended report from Greentea + @details This function will create a user friendly HTML report + @return String containing the HTML output + """ + + result_cell_template = """ + +
+
%s - %s% (%s/%s)
+ %s +
+ """ + platform_template = """ + +
Tests
+ %s + + + %s + """ + + unique_test_names = set() + platforms_toolchains = {} + # Populate a set of all of the unique tests + for platform_toolchain, test_list in test_result_ext.items(): + # Format of string is - + # can however contain '-' such as "frdm-k64f" + # is split with '_' fortunately, as in "gcc_arm" + toolchain = platform_toolchain.split('-')[-1] + platform = platform_toolchain.replace('-%s'% toolchain, '') + if platform in platforms_toolchains: + platforms_toolchains[platform].append(toolchain) + else: + platforms_toolchains[platform] = [toolchain] + + for test_name in test_list: + unique_test_names.add(test_name) + + table = "" + platform_row = "" + toolchain_row = "" + + platform_cell_template = """ + +
%s
+ """ + center_cell_template = """ + +
%s
+ """ + + for platform, toolchains in platforms_toolchains.items(): + platform_row += platform_cell_template % (len(toolchains), platform) + for toolchain in toolchains: + toolchain_row += center_cell_template % toolchain + table += platform_template % (platform_row, toolchain_row) + + test_cell_template = """ + %s""" + row_template = """ + %s + """ + + # Loop through the tests and get the results for the different platforms and toolchains + for test_name in unique_test_names: + this_row = test_cell_template % test_name + for platform, toolchains in platforms_toolchains.items(): + for toolchain in toolchains: + test_results = None + + if test_name in test_result_ext["%s-%s" % (platform, toolchain)]: + test_results = test_result_ext["%s-%s" % (platform, toolchain)][test_name] + else: + test_results = { + 'single_test_result': 'NOT_RAN', + 'elapsed_time': 0.0, + 'build_path': 'N/A', + 'build_path_abs': 'N/A', + 'copy_method': 'N/A', + 'image_path': 'N/A', + 'single_test_output': 'N/A', + 'platform_name': platform, + 'test_bin_name': 'N/A', + 'testcase_result': {} + } + + test_results['single_test_passes'] = 0 + test_results['single_test_count'] = 0 + result_div_id = "target_%s_toolchain_%s_test_%s" % (platform, toolchain, test_name.replace('-', '_')) + + result_overlay = get_result_overlay(result_div_id, + test_name, + platform, + toolchain, + test_results) + + # Loop through the test cases and count the passes and failures + for index, (testcase_result_name, testcase_result) in enumerate(test_results['testcase_result'].items()): + test_results['single_test_passes'] += testcase_result['passed'] + test_results['single_test_count'] += 1 + + result_class = get_result_colour_class(test_results['single_test_result']) + try: + percent_pass = int((test_results['single_test_passes']*100.0)/test_results['single_test_count']) + except ZeroDivisionError: + percent_pass = 100 + this_row += result_cell_template % (result_class, + result_div_id, + test_results['single_test_result'], + percent_pass, + test_results['single_test_passes'], + test_results['single_test_count'], + result_overlay) + + table += row_template % this_row + + # Add the numbers of columns to make them have the same width + return html_template % (get_result_colour_class_css(), len(test_result_ext), table) + +def exporter_memory_metrics_csv(test_result_ext, test_suite_properties=None): + """! Export memory metrics as CSV + @param test_result_ext Extended report from Greentea + @details This function will create a CSV file that is parsable via CI software + @return String containing the CSV output + """ + + metrics_report = {} + + for target_name in test_result_ext: + test_results = test_result_ext[target_name] + for test_suite_name in test_results: + test = test_results[test_suite_name] + + if 'memory_metrics' in test and test['memory_metrics']: + memory_metrics = test['memory_metrics'] + + if 'max_heap' in memory_metrics: + report_key = '%s_%s_max_heap_usage' % (target_name, test_suite_name) + metrics_report[report_key] = memory_metrics['max_heap'] + + if 'reserved_heap' in memory_metrics: + report_key = '%s_%s_reserved_heap_usage' % (target_name, test_suite_name) + metrics_report[report_key] = memory_metrics['reserved_heap'] + + if 'thread_stack_summary' in memory_metrics: + thread_stack_summary = memory_metrics['thread_stack_summary'] + + if 'max_stack_size' in thread_stack_summary: + report_key = '%s_%s_max_stack_size' % (target_name, test_suite_name) + metrics_report[report_key] = thread_stack_summary['max_stack_size'] + + if 'max_stack_usage' in thread_stack_summary: + report_key = '%s_%s_max_stack_usage' % (target_name, test_suite_name) + metrics_report[report_key] = thread_stack_summary['max_stack_usage'] + + if 'max_stack_usage_total' in thread_stack_summary: + report_key = '%s_%s_max_stack_usage_total' % (target_name, test_suite_name) + metrics_report[report_key] = thread_stack_summary['max_stack_usage_total'] + + if 'reserved_stack_total' in thread_stack_summary: + report_key = '%s_%s_reserved_stack_total' % (target_name, test_suite_name) + metrics_report[report_key] = thread_stack_summary['reserved_stack_total'] + + column_names = sorted(metrics_report.keys()) + column_values = [str(metrics_report[x]) for x in column_names] + + return "%s\n%s" % (','.join(column_names), ','.join(column_values)) diff --git a/tools/python/mbed_os_tools/test/mbed_target_info.py b/tools/python/mbed_os_tools/test/mbed_target_info.py new file mode 100644 index 0000000000..86bace3368 --- /dev/null +++ b/tools/python/mbed_os_tools/test/mbed_target_info.py @@ -0,0 +1,448 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +import json +from os import walk +try: + from contextlib import suppress +except ImportError: + from contextlib import contextmanager + @contextmanager + def suppress(*excs): + try: + yield + except excs: + pass +from .mbed_common_api import run_cli_process +from .mbed_greentea_log import gt_logger + + +## Information about some properties of targets (platforms) +# +# "default" entry is used to fetch "global" properties if they are not +# specified with each platform +# + +TARGET_INFO_MAPPING = { + "default" : { + "program_cycle_s": 4, + "binary_type": ".bin", + "copy_method": "default", + "reset_method": "default" + }, + + "K64F" : { + "yotta_targets": [ + { + "yotta_target": "frdm-k64f-gcc", + "mbed_toolchain": "GCC_ARM" + }, + { + "yotta_target": "frdm-k64f-armcc", + "mbed_toolchain": "ARM" + } + ], + "properties" : { + "binary_type": ".bin", + "copy_method": "default", + "reset_method": "default", + "program_cycle_s": 4 + } + }, + "RAPIDIOT_K64F" : { + "properties" : { + "forced_reset_timeout":7 + } + }, + "NUCLEO_F401RE" : { + "yotta_targets": [ + { + "yotta_target": "st-nucleo-f401re-gcc", + "mbed_toolchain": "GCC_ARM" + } + ], + "properties" : { + "binary_type": ".bin", + "copy_method": "cp", + "reset_method": "default", + "program_cycle_s": 4 + } + }, + "NRF51_DK" : { + "yotta_targets": [ + { + "yotta_target": "nrf51dk-gcc", + "mbed_toolchain": "GCC_ARM" + }, + { + "yotta_target": "nrf51dk-armcc", + "mbed_toolchain": "ARM" + } + ], + "properties" : { + "binary_type": "-combined.hex", + "copy_method": "shell", + "reset_method": "default", + "program_cycle_s": 4 + } + }, + "NRF51822" : { + "yotta_targets": [ + { + "yotta_target": "mkit-gcc", + "mbed_toolchain": "GCC_ARM" + }, + { + "yotta_target": "mkit-armcc", + "mbed_toolchain": "ARM" + } + ], + "properties" : { + "binary_type": "-combined.hex", + "copy_method": "shell", + "reset_method": "default", + "program_cycle_s": 4 + } + }, + "ARCH_BLE" : { + "yotta_targets": [ + { + "yotta_target": "tinyble-gcc", + "mbed_toolchain": "GCC_ARM" + } + ], + "properties" : { + "binary_type": "-combined.hex", + "copy_method": "shell", + "reset_method": "default", + "program_cycle_s": 4 + } + } +} + +TARGET_TOOLCAHINS = { + '-armcc': 'ARM', + '-gcc': 'GCC_ARM', + '-iar': 'IAR', +} + +def get_mbed_target_call_yotta_target(): + """! Calls yotta's 'yotta target' command to get information about + """ + cmd = ['yotta', '--plain', 'target'] + gt_logger.gt_log("checking yotta target in current directory") + gt_logger.gt_log_tab("calling yotta: %s"% " ".join(cmd)) + _stdout, _stderr, _ret = run_cli_process(cmd) + return _stdout, _stderr, _ret + +def parse_yotta_json_for_build_name(yotta_json_content): + """! Function parse .yotta.json to fetch set yotta target + @param yotta_json_content Content of .yotta_json file + @return String with set yotta target name, None if no target found + """ + try: + return yotta_json_content['build']['target'].split(',')[0] + except KeyError: + return None + +def get_yotta_target_from_local_config(yotta_json='.yotta.json'): + """! Load yotta target from local configuration file + @param yotta_json File in format of .yotta.json which stores current target names + @return Yotta target set in currect directory, None if no info is available + @details + Example structure of .yotta.json file: + { + "build": { + "target": "frdm-k64f-gcc,*", + "targetSetExplicitly": true + } + } + """ + if not os.path.exists(yotta_json): + return None + + try: + gt_logger.gt_log("parsing local file '%s' for target information"% yotta_json) + + with open(yotta_json, 'r') as f: + return parse_yotta_json_for_build_name(json.load(f)) + except (IOError, ValueError) as e: + gt_logger.gt_log(str(e)) + return None + +def get_mbed_target_from_current_dir(): + """! Function uses yotta target command to check current target + @return Returns current target or None if target not found (e.g. not yotta package) + """ + # We will first try to load current target name using .yotta.json file + result = get_yotta_target_from_local_config() + + # If we can't read .yotta.json, we will try to use command line to fetch target name + if not result: + _stdout, _stderr, _ret = get_mbed_target_call_yotta_target() + if not _ret: + for line in _stdout.splitlines(): + target = parse_yotta_target_cmd_output(line) + if target: + result = target + break + return result + +def parse_yotta_target_cmd_output(line): + """! Function parsed output from command 'yotta --plain target' + looking for valid target names. First one will be used as 'default' + of currently set yotta target + @param line Line of text from 'yotta target' command + @return Yotta target name, None if not parsed + @details + + Example call to 'yotta target' command (all lines) + $ yotta --plain target + frdm-k64f-gcc 2.0.0 + kinetis-k64-gcc 2.2.0 + mbed-gcc 1.2.2 + + """ + # Regular expression to parse stings like: 'frdm-k64f-gcc 2.0.0' + m = re.search(r'[\w\d_-]+ \d+\.\d+\.\d+', line) + if m and len(m.group()): + result = line.split()[0] + return result + return None + +def get_mbed_targets_from_yotta_local_module(mbed_classic_name, yotta_targets_path='./yotta_targets'): + """! Function is parsing local yotta targets to fetch matching mbed device target's name + @return Function returns list of possible targets or empty list if value not found + """ + result = [] + + if not os.path.exists(yotta_targets_path): + return result + + # All local directories with yotta targets + target_dirs = [target_dir_name for target_dir_name in os.listdir(yotta_targets_path) if os.path.isdir(os.path.join(yotta_targets_path, target_dir_name))] + + gt_logger.gt_log("local yotta target search in '%s' for compatible mbed-target '%s'"% (gt_logger.gt_bright(yotta_targets_path), gt_logger.gt_bright(mbed_classic_name.lower().strip()))) + + for target_dir in target_dirs: + path = os.path.join(yotta_targets_path, target_dir, 'target.json') + try: + with open(path, 'r') as data_file: + target_json_data = json.load(data_file) + yotta_target_name = parse_mbed_target_from_target_json(mbed_classic_name, target_json_data) + if yotta_target_name: + target_dir_name = os.path.join(yotta_targets_path, target_dir) + gt_logger.gt_log_tab("inside '%s' found compatible target '%s'"% (gt_logger.gt_bright(target_dir_name), gt_logger.gt_bright(yotta_target_name))) + result.append(yotta_target_name) + except IOError as e: + gt_logger.gt_log_err(str(e)) + return result + +def parse_mbed_target_from_target_json(mbed_classic_name, target_json_data): + if (not target_json_data or + 'keywords' not in target_json_data or + 'name' not in target_json_data): + return None + + for keyword in target_json_data['keywords']: + target, _, name = keyword.partition(':') + if (target == "mbed-target" and + name.lower() == mbed_classic_name.lower()): + return target_json_data['name'] + + return None + +def get_mbed_targets_from_yotta(mbed_classic_name): + """! Function is using 'yotta search' command to fetch matching mbed device target's name + @return Function returns list of possible targets or empty list if value not found + @details Example: + $ yt search -k mbed-target:k64f target + frdm-k64f-gcc 0.0.16: Official mbed build target for the mbed frdm-k64f development board. + frdm-k64f-armcc 0.0.10: Official mbed build target for the mbed frdm-k64f development board, using the armcc toolchain. + + Note: Function prints on console + """ + result = [] + cmd = ['yotta', '--plain', 'search', '-k', 'mbed-target:%s'% mbed_classic_name.lower().strip(), 'target'] + gt_logger.gt_log("yotta search for mbed-target '%s'"% gt_logger.gt_bright(mbed_classic_name.lower().strip())) + gt_logger.gt_log_tab("calling yotta: %s"% " ".join(cmd)) + _stdout, _stderr, _ret = run_cli_process(cmd) + if not _ret: + for line in _stdout.splitlines(): + yotta_target_name = parse_yotta_search_cmd_output(line) + if yotta_target_name: + if yotta_target_name and yotta_target_name not in result: + result.append(yotta_target_name) + gt_logger.gt_log_tab("found target '%s'" % gt_logger.gt_bright(yotta_target_name)) + else: + gt_logger.gt_log_err("calling yotta search failed!") + return result + +def parse_yotta_search_cmd_output(line): + m = re.search('([\w\d-]+) \d+\.\d+\.\d+[$:]?', line) + if m and len(m.groups()): + yotta_target_name = m.groups()[0] + return yotta_target_name + return None + +def add_target_info_mapping(mbed_classic_name, map_platform_to_yt_target=None, use_yotta_registry=False): + """! Adds more target information to TARGET_INFO_MAPPING by searching in yotta registry + @return Returns TARGET_INFO_MAPPING updated with new targets + @details Note: function mutates TARGET_INFO_MAPPING + """ + yotta_target_search = get_mbed_targets_from_yotta_local_module(mbed_classic_name) + if use_yotta_registry: + # We can also use yotta registry to check for target compatibility (slower) + yotta_registry_target_search = get_mbed_targets_from_yotta(mbed_classic_name) + yotta_target_search.extend(yotta_registry_target_search) + yotta_target_search = list(set(yotta_target_search)) # Reduce repeated values + + # Add extra targets to already existing and detected in the system platforms + if map_platform_to_yt_target and mbed_classic_name in map_platform_to_yt_target: + yotta_target_search = list(set(yotta_target_search + map_platform_to_yt_target[mbed_classic_name])) + + # Check if this targets are already there + if mbed_classic_name not in TARGET_INFO_MAPPING: + TARGET_INFO_MAPPING[mbed_classic_name] = { + "yotta_targets": [], + "properties" : { + "binary_type": ".bin", + "copy_method": "shell", + "reset_method": "default", + "program_cycle_s": 6 + } + } + + target_desc = TARGET_INFO_MAPPING[mbed_classic_name] + if 'yotta_targets' not in target_desc: + return TARGET_INFO_MAPPING + + # All yt targets supported by 'mbed_classic_name' board + mbeds_yt_targets = [] + for target in target_desc['yotta_targets']: + mbeds_yt_targets.append(target['yotta_target']) + + # Check if any of yotta targets is new to TARGET_INFO_MAPPING + for new_yt_target in yotta_target_search: + if new_yt_target in mbeds_yt_targets: + continue + + gt_logger.gt_log_tab("discovered extra target '%s'"% new_yt_target) + # We want to at least guess toolchain type by target's name suffix + mbed_toolchain = 'UNKNOWN' + for toolchain_suffix in TARGET_TOOLCAHINS: + if new_yt_target.endswith(toolchain_suffix): + mbed_toolchain = TARGET_TOOLCAHINS[toolchain_suffix] + break + + TARGET_INFO_MAPPING[mbed_classic_name]['yotta_targets'].append({ + 'yotta_target': new_yt_target, + 'mbed_toolchain': mbed_toolchain + }) + + return TARGET_INFO_MAPPING + +def get_mbed_clasic_target_info(mbed_classic_name, map_platform_to_yt_target=None, use_yotta_registry=False): + """! Function resolves meta-data information about target given as mbed classic name. + @param mbed_classic_name Mbed classic (mbed 2.0) name e.g. K64F, LPC1768 etc. + @param map_platform_to_yt_target User defined mapping platform:supported target + @details Function first updated TARGET_INFO_MAPPING structure and later checks if mbed classic name is available in mapping structure + @return Returns information about yotta target for specific toolchain + """ + TARGET_INFO_MAPPING = add_target_info_mapping(mbed_classic_name, map_platform_to_yt_target, use_yotta_registry) + return TARGET_INFO_MAPPING[mbed_classic_name] if mbed_classic_name in TARGET_INFO_MAPPING else None + +def get_binary_type_for_platform(platform): + """ + Gives binary type for the given platform. + + :param platform: + :return: + """ + #return TARGET_INFO_MAPPING[platform]['properties']["binary_type"] + return get_platform_property(platform, 'binary_type') + +def get_platform_property(platform, property): + """ + Gives platform property. + + :param platform: + :return: property value, None if property not found + """ + + default = _get_platform_property_from_default(property) + from_targets_json = _get_platform_property_from_targets( + platform, property, default) + if from_targets_json: + return from_targets_json + from_info_mapping = _get_platform_property_from_info_mapping(platform, property) + if from_info_mapping: + return from_info_mapping + return default + +def _get_platform_property_from_default(property): + with suppress(KeyError): + return TARGET_INFO_MAPPING['default'][property] + +def _get_platform_property_from_info_mapping(platform, property): + with suppress(KeyError): + return TARGET_INFO_MAPPING[platform]['properties'][property] + +def _platform_property_from_targets_json(targets, platform, property, default): + """! Get a platforms's property from the target data structure in + targets.json. Takes into account target inheritance. + @param targets Data structure parsed from targets.json + @param platform Name of the platform + @param property Name of the property + @param default the fallback value if none is found, but the target exists + @return property value, None if property not found + + """ + with suppress(KeyError): + return targets[platform][property] + with suppress(KeyError): + for inherited_target in targets[platform]['inherits']: + result = _platform_property_from_targets_json(targets, inherited_target, property, None) + if result: + return result + if platform in targets: + return default + +IGNORED_DIRS = ['.build', 'BUILD', 'tools'] + +def _find_targets_json(path): + for root, dirs, files in walk(path, followlinks=True): + for ignored_dir in IGNORED_DIRS: + if ignored_dir in dirs: + dirs.remove(ignored_dir) + if 'targets.json' in files: + yield os.path.join(root, 'targets.json') + +def _get_platform_property_from_targets(platform, property, default): + """ + Load properties from targets.json file somewhere in the project structure + + :param platform: + :return: property value, None if property not found + """ + for targets_path in _find_targets_json(os.getcwd()): + with suppress(IOError, ValueError): + with open(targets_path, 'r') as f: + targets = json.load(f) + result = _platform_property_from_targets_json(targets, platform, property, default) + if result: + return result diff --git a/tools/python/mbed_os_tools/test/mbed_test_api.py b/tools/python/mbed_os_tools/test/mbed_test_api.py new file mode 100644 index 0000000000..5fa9e0ae22 --- /dev/null +++ b/tools/python/mbed_os_tools/test/mbed_test_api.py @@ -0,0 +1,596 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from past.builtins import basestring + +import re +import os +import sys +import json +import string +from subprocess import Popen, PIPE, STDOUT + +from .cmake_handlers import list_binaries_for_builds, list_binaries_for_targets +from .mbed_coverage_api import coverage_dump_file, coverage_pack_hex_payload +from .mbed_greentea_log import gt_logger +from .mbed_yotta_api import get_test_spec_from_yt_module +from .tests_spec import TestSpec + + + +# Return codes for test script +TEST_RESULT_OK = "OK" +TEST_RESULT_FAIL = "FAIL" +TEST_RESULT_ERROR = "ERROR" +TEST_RESULT_SKIPPED = "SKIPPED" +TEST_RESULT_UNDEF = "UNDEF" +TEST_RESULT_IOERR_COPY = "IOERR_COPY" +TEST_RESULT_IOERR_DISK = "IOERR_DISK" +TEST_RESULT_IOERR_SERIAL = "IOERR_SERIAL" +TEST_RESULT_TIMEOUT = "TIMEOUT" +TEST_RESULT_NO_IMAGE = "NO_IMAGE" +TEST_RESULT_MBED_ASSERT = "MBED_ASSERT" +TEST_RESULT_BUILD_FAILED = "BUILD_FAILED" +TEST_RESULT_SYNC_FAILED = "SYNC_FAILED" + +TEST_RESULTS = [TEST_RESULT_OK, + TEST_RESULT_FAIL, + TEST_RESULT_ERROR, + TEST_RESULT_SKIPPED, + TEST_RESULT_UNDEF, + TEST_RESULT_IOERR_COPY, + TEST_RESULT_IOERR_DISK, + TEST_RESULT_IOERR_SERIAL, + TEST_RESULT_TIMEOUT, + TEST_RESULT_NO_IMAGE, + TEST_RESULT_MBED_ASSERT, + TEST_RESULT_BUILD_FAILED, + TEST_RESULT_SYNC_FAILED + ] + +TEST_RESULT_MAPPING = {"success" : TEST_RESULT_OK, + "failure" : TEST_RESULT_FAIL, + "error" : TEST_RESULT_ERROR, + "skipped" : TEST_RESULT_SKIPPED, + "end" : TEST_RESULT_UNDEF, + "ioerr_copy" : TEST_RESULT_IOERR_COPY, + "ioerr_disk" : TEST_RESULT_IOERR_DISK, + "ioerr_serial" : TEST_RESULT_IOERR_SERIAL, + "timeout" : TEST_RESULT_TIMEOUT, + "no_image" : TEST_RESULT_NO_IMAGE, + "mbed_assert" : TEST_RESULT_MBED_ASSERT, + "build_failed" : TEST_RESULT_BUILD_FAILED, + "sync_failed" : TEST_RESULT_SYNC_FAILED + } + + +# This value is used to tell caller than run_host_test function failed while invoking mbedhtrun +# Just a value greater than zero +RUN_HOST_TEST_POPEN_ERROR = 1729 + +def get_test_result(output): + """! Parse test 'output' data + @details If test result not found returns by default TEST_RESULT_TIMEOUT value + @return Returns found test result + """ + re_detect = re.compile(r"\{result;([\w+_]*)\}") + + for line in output.split(): + search_result = re_detect.search(line) + if search_result: + if search_result.group(1) in TEST_RESULT_MAPPING: + return TEST_RESULT_MAPPING[search_result.group(1)] + else: + return TEST_RESULT_UNDEF + return TEST_RESULT_TIMEOUT + + +def run_command(cmd): + """! Runs command and prints proc stdout on screen + @paran cmd List with command line to execute e.g. ['ls', '-l] + @return Value returned by subprocess.Popen, if failed return None + """ + try: + p = Popen(cmd, + stdout=PIPE, + stderr=STDOUT) + except OSError as e: + gt_logger.gt_log_err("run_host_test.run_command(%s) failed!" % str(cmd)) + gt_logger.gt_log_tab(str(e)) + return None + return p + +def run_htrun(cmd, verbose): + # detect overflow when running tests + htrun_output = str() + # run_command will return None if process can't be opened (Issue #134) + p = run_command(cmd) + if not p: + # int value > 0 notifies caller that starting of host test process failed + return RUN_HOST_TEST_POPEN_ERROR + + htrun_failure_line = re.compile('\[RXD\] (:\d+::FAIL: .*)') + + for line in iter(p.stdout.readline, b''): + decoded_line = line.decode("utf-8", "replace") + htrun_output += decoded_line + # When dumping output to file both \r and \n will be a new line + # To avoid this "extra new-line" we only use \n at the end + + test_error = htrun_failure_line.search(decoded_line) + if test_error: + gt_logger.gt_log_err(test_error.group(1)) + + if verbose: + output = decoded_line.rstrip() + '\n' + try: + # Try to output decoded unicode. Should be fine in most Python 3 + # environments. + sys.stdout.write(output) + except UnicodeEncodeError: + try: + # Try to encode to unicode bytes and let the terminal handle + # the decoding. Some Python 2 and OS combinations handle this + # gracefully. + sys.stdout.write(output.encode("utf-8")) + except TypeError: + # Fallback to printing just ascii characters + sys.stdout.write(output.encode("ascii", "replace").decode("ascii")) + sys.stdout.flush() + + # Check if process was terminated by signal + returncode = p.wait() + return returncode, htrun_output + +def get_testcase_count_and_names(output): + """ Fetches from log utest events with test case count (__testcase_count) and test case names (__testcase_name)* + + @details + Example test case count + names prints + [1467197417.34][HTST][INF] host test detected: default_auto + [1467197417.36][CONN][RXD] {{__testcase_count;2}} + [1467197417.36][CONN][INF] found KV pair in stream: {{__testcase_count;2}}, queued... + [1467197417.39][CONN][RXD] >>> Running 2 test cases... + [1467197417.43][CONN][RXD] {{__testcase_name;C strings: strtok}} + [1467197417.43][CONN][INF] found KV pair in stream: {{__testcase_name;C strings: strtok}}, queued... + [1467197417.47][CONN][RXD] {{__testcase_name;C strings: strpbrk}} + [1467197417.47][CONN][INF] found KV pair in stream: {{__testcase_name;C strings: strpbrk}}, queued... + [1467197417.52][CONN][RXD] >>> Running case #1: 'C strings: strtok'... + [1467197417.56][CONN][RXD] {{__testcase_start;C strings: strtok}} + [1467197417.56][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: strtok}}, queued... + + @return Tuple with (test case count, list of test case names in order of appearance) + """ + testcase_count = 0 + testcase_names = [] + + re_tc_count = re.compile(r"^\[(\d+\.\d+)\]\[(\w+)\]\[(\w+)\].*\{\{(__testcase_count);(\d+)\}\}") + re_tc_names = re.compile(r"^\[(\d+\.\d+)\]\[(\w+)\]\[(\w+)\].*\{\{(__testcase_name);([^;]+)\}\}") + + for line in output.splitlines(): + + m = re_tc_names.search(line) + if m: + testcase_names.append(m.group(5)) + continue + + m = re_tc_count.search(line) + if m: + testcase_count = m.group(5) + + return (testcase_count, testcase_names) + +def get_testcase_utest(output, test_case_name): + """ Fetches from log all prints for given utest test case (from being print to end print) + + @details + Example test case prints + [1455553765.52][CONN][RXD] >>> Running case #1: 'Simple Test'... + [1455553765.52][CONN][RXD] {{__testcase_start;Simple Test}} + [1455553765.52][CONN][INF] found KV pair in stream: {{__testcase_start;Simple Test}}, queued... + [1455553765.58][CONN][RXD] Simple test called + [1455553765.58][CONN][RXD] {{__testcase_finish;Simple Test;1;0}} + [1455553765.58][CONN][INF] found KV pair in stream: {{__testcase_finish;Simple Test;1;0}}, queued... + [1455553765.70][CONN][RXD] >>> 'Simple Test': 1 passed, 0 failed + + @return log lines between start and end test case print + """ + + # Return string with all non-alphanumerics backslashed; + # this is useful if you want to match an arbitrary literal + # string that may have regular expression metacharacters in it. + escaped_test_case_name = re.escape(test_case_name) + + re_tc_utest_log_start = re.compile(r"^\[(\d+\.\d+)\]\[(\w+)\]\[(\w+)\] >>> Running case #(\d)+: '(%s)'"% escaped_test_case_name) + re_tc_utest_log_finish = re.compile(r"^\[(\d+\.\d+)\]\[(\w+)\]\[(\w+)\] >>> '(%s)': (\d+) passed, (\d+) failed"% escaped_test_case_name) + + tc_log_lines = [] + for line in output.splitlines(): + + # utest test case start string search + m = re_tc_utest_log_start.search(line) + if m: + tc_log_lines.append(line) + continue + + # If utest test case end string found + m = re_tc_utest_log_finish.search(line) + if m: + tc_log_lines.append(line) + break + + # Continue adding utest log lines + if tc_log_lines: + tc_log_lines.append(line) + + return tc_log_lines + +def get_coverage_data(build_path, output): + # Example GCOV output + # [1456840876.73][CONN][RXD] {{__coverage_start;c:\Work\core-util/source/PoolAllocator.cpp.gcda;6164636772393034c2733f32...a33e...b9}} + gt_logger.gt_log("checking for GCOV data...") + re_gcov = re.compile(r"^\[(\d+\.\d+)\][^\{]+\{\{(__coverage_start);([^;]+);([^}]+)\}\}$") + for line in output.splitlines(): + m = re_gcov.search(line) + if m: + _, _, gcov_path, gcov_payload = m.groups() + try: + bin_gcov_payload = coverage_pack_hex_payload(gcov_payload) + coverage_dump_file(build_path, gcov_path, bin_gcov_payload) + except Exception as e: + gt_logger.gt_log_err("error while handling GCOV data: " + str(e)) + gt_logger.gt_log_tab("storing %d bytes in '%s'"% (len(bin_gcov_payload), gcov_path)) + +def get_printable_string(unprintable_string): + return "".join(filter(lambda x: x in string.printable, unprintable_string)) + +def get_testcase_summary(output): + """! Searches for test case summary + + String to find: + [1459246276.95][CONN][INF] found KV pair in stream: {{__testcase_summary;7;1}}, queued... + + @return Tuple of (passed, failed) or None if no summary found + """ + re_tc_summary = re.compile(r"^\[(\d+\.\d+)\][^\{]+\{\{(__testcase_summary);(\d+);(\d+)\}\}") + for line in output.splitlines(): + m = re_tc_summary.search(line) + if m: + _, _, passes, failures = m.groups() + return int(passes), int(failures) + return None + +def get_testcase_result(output): + result_test_cases = {} # Test cases results + re_tc_start = re.compile(r"^\[(\d+\.\d+)\][^\{]+\{\{(__testcase_start);([^;]+)\}\}") + re_tc_finish = re.compile(r"^\[(\d+\.\d+)\][^\{]+\{\{(__testcase_finish);([^;]+);(\d+);(\d+)\}\}") + for line in output.splitlines(): + m = re_tc_start.search(line) + if m: + timestamp, _, testcase_id = m.groups() + if testcase_id not in result_test_cases: + result_test_cases[testcase_id] = {} + + # Data collected when __testcase_start is fetched + result_test_cases[testcase_id]['time_start'] = float(timestamp) + result_test_cases[testcase_id]['utest_log'] = get_testcase_utest(output, testcase_id) + + # Data collected when __testcase_finish is fetched + result_test_cases[testcase_id]['duration'] = 0.0 + result_test_cases[testcase_id]['result_text'] = 'ERROR' + result_test_cases[testcase_id]['time_end'] = float(timestamp) + result_test_cases[testcase_id]['passed'] = 0 + result_test_cases[testcase_id]['failed'] = 0 + result_test_cases[testcase_id]['result'] = -4096 + continue + + m = re_tc_finish.search(line) + if m: + timestamp, _, testcase_id, testcase_passed, testcase_failed = m.groups() + + testcase_passed = int(testcase_passed) + testcase_failed = int(testcase_failed) + + testcase_result = 0 # OK case + if testcase_failed != 0: + testcase_result = testcase_failed # testcase_result > 0 is FAILure + + if testcase_id not in result_test_cases: + result_test_cases[testcase_id] = {} + # Setting some info about test case itself + result_test_cases[testcase_id]['duration'] = 0.0 + result_test_cases[testcase_id]['result_text'] = 'OK' + result_test_cases[testcase_id]['time_end'] = float(timestamp) + result_test_cases[testcase_id]['passed'] = testcase_passed + result_test_cases[testcase_id]['failed'] = testcase_failed + result_test_cases[testcase_id]['result'] = testcase_result + # Assign human readable test case result + if testcase_result > 0: + result_test_cases[testcase_id]['result_text'] = 'FAIL' + elif testcase_result < 0: + result_test_cases[testcase_id]['result_text'] = 'ERROR' + + if 'time_start' in result_test_cases[testcase_id]: + result_test_cases[testcase_id]['duration'] = result_test_cases[testcase_id]['time_end'] - result_test_cases[testcase_id]['time_start'] + else: + result_test_cases[testcase_id]['duration'] = 0.0 + + if 'utest_log' not in result_test_cases[testcase_id]: + result_test_cases[testcase_id]['utest_log'] = "__testcase_start tag not found." + + ### Adding missing test cases which were defined with __testcase_name + # Get test case names reported by utest + test case names + # This data will be used to process all tests which were not executed + # do their status can be set to SKIPPED (e.g. in JUnit) + tc_count, tc_names = get_testcase_count_and_names(output) + for testcase_id in tc_names: + if testcase_id not in result_test_cases: + result_test_cases[testcase_id] = {} + # Data collected when __testcase_start is fetched + result_test_cases[testcase_id]['time_start'] = 0.0 + result_test_cases[testcase_id]['utest_log'] = [] + # Data collected when __testcase_finish is fetched + result_test_cases[testcase_id]['duration'] = 0.0 + result_test_cases[testcase_id]['result_text'] = 'SKIPPED' + result_test_cases[testcase_id]['time_end'] = 0.0 + result_test_cases[testcase_id]['passed'] = 0 + result_test_cases[testcase_id]['failed'] = 0 + result_test_cases[testcase_id]['result'] = -8192 + + return result_test_cases + +def get_memory_metrics(output): + """! Searches for test case memory metrics + + String to find: + [1477505660.40][CONN][INF] found KV pair in stream: {{max_heap_usage;2284}}, queued... + + @return Tuple of (max heap usage, thread info list), where thread info list + is a list of dictionaries with format {entry, arg, max_stack, stack_size} + """ + max_heap_usage = None + reserved_heap = None + thread_info = {} + re_tc_max_heap_usage = re.compile(r"^\[(\d+\.\d+)\][^\{]+\{\{(max_heap_usage);(\d+)\}\}") + re_tc_reserved_heap = re.compile(r"^\[(\d+\.\d+)\][^\{]+\{\{(reserved_heap);(\d+)\}\}") + re_tc_thread_info = re.compile(r"^\[(\d+\.\d+)\][^\{]+\{\{(__thread_info);\"([A-Fa-f0-9\-xX]+)\",(\d+),(\d+)\}\}") + for line in output.splitlines(): + m = re_tc_max_heap_usage.search(line) + if m: + _, _, max_heap_usage = m.groups() + max_heap_usage = int(max_heap_usage) + + m = re_tc_reserved_heap.search(line) + if m: + _, _, reserved_heap = m.groups() + reserved_heap = int(reserved_heap) + + m = re_tc_thread_info.search(line) + if m: + _, _, thread_entry_arg, thread_max_stack, thread_stack_size = m.groups() + thread_max_stack = int(thread_max_stack) + thread_stack_size = int(thread_stack_size) + thread_entry_arg_split = thread_entry_arg.split('-') + thread_entry = thread_entry_arg_split[0] + + thread_info[thread_entry_arg] = { + 'entry': thread_entry, + 'max_stack': thread_max_stack, + 'stack_size': thread_stack_size + } + + if len(thread_entry_arg_split) > 1: + thread_arg = thread_entry_arg_split[1] + thread_info[thread_entry_arg]['arg'] = thread_arg + + thread_info_list = list(thread_info.values()) + + return max_heap_usage, reserved_heap, thread_info_list + +def get_thread_with_max_stack_size(thread_stack_info): + max_thread_stack_size = 0 + max_thread = None + max_stack_usage_total = 0 + reserved_stack_total = 0 + for cur_thread_stack_info in thread_stack_info: + if cur_thread_stack_info['stack_size'] > max_thread_stack_size: + max_thread_stack_size = cur_thread_stack_info['stack_size'] + max_thread = cur_thread_stack_info + max_stack_usage_total += cur_thread_stack_info['max_stack'] + reserved_stack_total += cur_thread_stack_info['stack_size'] + max_thread['max_stack_usage_total'] = max_stack_usage_total + max_thread['reserved_stack_total'] = reserved_stack_total + return max_thread + +def get_thread_stack_info_summary(thread_stack_info): + + max_thread_info = get_thread_with_max_stack_size(thread_stack_info) + summary = { + 'max_stack_size': max_thread_info['stack_size'], + 'max_stack_usage': max_thread_info['max_stack'], + 'max_stack_usage_total': max_thread_info['max_stack_usage_total'], + 'reserved_stack_total': max_thread_info['reserved_stack_total'] + } + return summary + +def log_mbed_devices_in_table(muts, cols = ['platform_name', 'platform_name_unique', 'serial_port', 'mount_point', 'target_id']): + """! Print table of muts using prettytable + @param muts List of MUTs to print in table + @param cols Columns used to for a table, required for each mut + @return string with formatted prettytable + """ + from prettytable import PrettyTable, HEADER + pt = PrettyTable(cols, junction_char="|", hrules=HEADER) + for col in cols: + pt.align[col] = "l" + pt.padding_width = 1 # One space between column edges and contents (default) + row = [] + for mut in muts: + for col in cols: + cell_val = mut[col] if col in mut else 'not detected' + row.append(cell_val) + pt.add_row(row) + row = [] + return pt.get_string() + +def get_test_spec(opts): + """! Closure encapsulating how we get test specification and load it from file of from yotta module + @return Returns tuple of (test specification, ret code). Test specification == None if test spec load was not successful + """ + test_spec = None + + # Check if test_spec.json file exist, if so we will pick it up as default file and load it + test_spec_file_name = opts.test_spec + test_spec_file_name_list = [] + + # Note: test_spec.json will have higher priority than module.json file + # so if we are inside directory with module.json and test_spec.json we will use test spec file + # instead of using yotta's module.json file + + def get_all_test_specs_from_build_dir(path_to_scan): + """! Searches for all test_spec.json files + @param path_to_scan Directory path used to recursively search for test_spec.json + @result List of locations of test_spec.json + """ + return [os.path.join(dp, f) for dp, dn, filenames in os.walk(path_to_scan) for f in filenames if f == 'test_spec.json'] + + def merge_multiple_test_specifications_from_file_list(test_spec_file_name_list): + """! For each file in test_spec_file_name_list merge all test specifications into one + @param test_spec_file_name_list List of paths to different test specifications + @return TestSpec object with all test specification data inside + """ + + def copy_builds_between_test_specs(source, destination): + """! Copies build key-value pairs between two test_spec dicts + @param source Source dictionary + @param destination Dictionary with will be applied with 'builds' key-values + @return Dictionary with merged source + """ + result = destination.copy() + if 'builds' in source and 'builds' in destination: + for k in source['builds']: + result['builds'][k] = source['builds'][k] + return result + + merged_test_spec = {} + for test_spec_file in test_spec_file_name_list: + gt_logger.gt_log_tab("using '%s'"% test_spec_file) + try: + with open(test_spec_file, 'r') as f: + test_spec_data = json.load(f) + merged_test_spec = copy_builds_between_test_specs(merged_test_spec, test_spec_data) + except Exception as e: + gt_logger.gt_log_err("Unexpected error while processing '%s' test specification file"% test_spec_file) + gt_logger.gt_log_tab(str(e)) + merged_test_spec = {} + + test_spec = TestSpec() + test_spec.parse(merged_test_spec) + return test_spec + + # Test specification look-up + if opts.test_spec: + # Loading test specification from command line specified file + gt_logger.gt_log("test specification file '%s' (specified with --test-spec option)"% opts.test_spec) + elif os.path.exists('test_spec.json'): + # Test specification file exists in current directory + gt_logger.gt_log("using 'test_spec.json' from current directory!") + test_spec_file_name = 'test_spec.json' + elif 'BUILD' in os.listdir(os.getcwd()): + # Checking 'BUILD' directory for test specifications + # Using `os.listdir()` since it preserves case + test_spec_file_name_list = get_all_test_specs_from_build_dir('BUILD') + elif os.path.exists('.build'): + # Checking .build directory for test specifications + test_spec_file_name_list = get_all_test_specs_from_build_dir('.build') + elif os.path.exists('mbed-os') and 'BUILD' in os.listdir('mbed-os'): + # Checking mbed-os/.build directory for test specifications + # Using `os.listdir()` since it preserves case + test_spec_file_name_list = get_all_test_specs_from_build_dir(os.path.join(['mbed-os', 'BUILD'])) + elif os.path.exists(os.path.join('mbed-os', '.build')): + # Checking mbed-os/.build directory for test specifications + test_spec_file_name_list = get_all_test_specs_from_build_dir(os.path.join(['mbed-os', '.build'])) + + # Actual load and processing of test specification from sources + if test_spec_file_name: + # Test specification from command line (--test-spec) or default test_spec.json will be used + gt_logger.gt_log("using '%s' from current directory!"% test_spec_file_name) + test_spec = TestSpec(test_spec_file_name) + if opts.list_binaries: + list_binaries_for_builds(test_spec) + return None, 0 + elif test_spec_file_name_list: + # Merge multiple test specs into one and keep calm + gt_logger.gt_log("using multiple test specifications from current directory!") + test_spec = merge_multiple_test_specifications_from_file_list(test_spec_file_name_list) + if opts.list_binaries: + list_binaries_for_builds(test_spec) + return None, 0 + elif os.path.exists('module.json'): + # If inside yotta module load module data and generate test spec + gt_logger.gt_log("using 'module.json' from current directory!") + if opts.list_binaries: + # List available test binaries (names, no extension) + list_binaries_for_targets() + return None, 0 + else: + test_spec = get_test_spec_from_yt_module(opts) + else: + gt_logger.gt_log_err("greentea should be run inside a Yotta module or --test-spec switch should be used") + return None, -1 + return test_spec, 0 + +def get_test_build_properties(test_spec, test_build_name): + result = dict() + test_builds = test_spec.get_test_builds(filter_by_names=[test_build_name]) + if test_builds: + test_build = test_builds[0] + result['name'] = test_build.get_name() + result['toolchain'] = test_build.get_toolchain() + result['target'] = test_build.get_platform() + return result + else: + return None + +def parse_global_resource_mgr(global_resource_mgr): + """! Parses --grm switch with global resource manager info + @details K64F:module_name:10.2.123.43:3334 + @return tuple wity four elements from GRM or None if error + """ + try: + platform_name, module_name, leftover = global_resource_mgr.split(':', 2) + parts = leftover.rsplit(':', 1) + + try: + ip_name, port_name = parts + _ = int(port_name) + except ValueError: + # No valid port was found, so assume no port was supplied + ip_name = leftover + port_name = None + + except ValueError as e: + return False + return platform_name, module_name, ip_name, port_name + +def parse_fast_model_connection(fast_model_connection): + """! Parses --fm switch with simulator resource manager info + @details FVP_MPS2_M3:DEFAULT + """ + try: + platform_name, config_name = fast_model_connection.split(':') + except ValueError as e: + return False + return platform_name, config_name diff --git a/tools/python/mbed_os_tools/test/mbed_yotta_api.py b/tools/python/mbed_os_tools/test/mbed_yotta_api.py new file mode 100644 index 0000000000..fd2258d468 --- /dev/null +++ b/tools/python/mbed_os_tools/test/mbed_yotta_api.py @@ -0,0 +1,190 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re +import json + +from .mbed_common_api import run_cli_command +from .mbed_greentea_log import gt_logger +from .mbed_yotta_module_parse import YottaModule, YottaConfig +from .mbed_target_info import ( + get_mbed_target_from_current_dir, get_binary_type_for_platform +) +from .cmake_handlers import load_ctest_testsuite +from .tests_spec import TestSpec, TestBuild, Test, TestBinary + + + +class YottaError(Exception): + """ + Exception raised by this module when it fails to gather test information. + """ + pass + + +def build_with_yotta(yotta_target_name, verbose=False, build_to_release=False, build_to_debug=False): + cmd = ["yotta"] # "yotta %s --target=%s,* build" + if verbose: + cmd.append("-v") + cmd.append("--target=%s,*"% yotta_target_name) + cmd.append("build") + if build_to_release: + cmd.append("-r") + elif build_to_debug: + cmd.append("-d") + + gt_logger.gt_log("building your sources and tests with yotta...") + gt_logger.gt_log_tab("calling yotta: %s"% (" ".join(cmd))) + yotta_result, yotta_ret = run_cli_command(cmd, shell=False, verbose=verbose) + if yotta_result: + gt_logger.gt_log("yotta build for target '%s' was successful"% gt_logger.gt_bright(yotta_target_name)) + else: + gt_logger.gt_log_err("yotta build failed!") + return yotta_result, yotta_ret + + +def get_platform_name_from_yotta_target(target): + """ + Parses target string and gives platform name and toolchain + + :param target: + :return: + """ + target_json_path = os.path.join('yotta_targets', target, 'target.json') + + if not os.path.exists(target_json_path): + gt_logger.gt_log_err('Target json does not exist [%s].\n' % target_json_path + + 'mbed TAS Executor {greentea} must be run inside a pre built yotta module!') + return None + + with open(target_json_path, 'r') as f: + data = f.read() + try: + target_json = json.loads(data) + except (TypeError, ValueError) as e: + gt_logger.gt_log_err('Failed to load json data from target.json! error [%s]\n' % str(e) + + 'Can not determine required mbed platform name!') + return None + + if 'keywords' not in target_json: + gt_logger.gt_log_err("No 'keywords' in target.json! Can not determine required mbed platform name!") + return None + + platform_name = None + for keyword in target_json['keywords']: + m = re.search('mbed-target:(.*)', keyword) + if m is not None: + platform_name = m.group(1).upper() + + if platform_name is None: + gt_logger.gt_log_err('No keyword with format "mbed-target:" found in target.json!\n' + + 'Can not determine required mbed platform name!') + return None + return platform_name + + +def get_test_spec_from_yt_module(opts): + """ + Gives test specification created from yotta module environment. + + :return TestSpec: + """ + ### Read yotta module basic information + yotta_module = YottaModule() + yotta_module.init() # Read actual yotta module data + + # Check if NO greentea-client is in module.json of repo to test, if so abort + if not yotta_module.check_greentea_client(): + error = """ + ***************************************************************************************** + * We've noticed that NO 'greentea-client' module is specified in * + * dependency/testDependency section of this module's 'module.json' file. * + * * + * This version of Greentea requires 'greentea-client' module. * + * Please downgrade to Greentea before v0.2.0: * + * * + * $ pip install "mbed-greentea<0.2.0" --upgrade * + * * + * or port your tests to new Async model: https://github.com/ARMmbed/greentea/pull/78 * + ***************************************************************************************** + """ + raise YottaError(error) + + test_spec = TestSpec() + + ### Selecting yotta targets to process + yt_targets = [] # List of yotta targets specified by user used to process during this run + if opts.list_of_targets: + yt_targets = opts.list_of_targets.split(',') + else: + # Trying to use locally set yotta target + gt_logger.gt_log("checking for yotta target in current directory") + gt_logger.gt_log_tab("reason: no --target switch set") + current_target = get_mbed_target_from_current_dir() + if current_target: + gt_logger.gt_log("assuming default target as '%s'"% gt_logger.gt_bright(current_target)) + # Assuming first target printed by 'yotta search' will be used + yt_targets = [current_target] + else: + gt_logger.gt_log_tab("yotta target in current directory is not set") + gt_logger.gt_log_err("yotta target is not specified. Use '%s' or '%s' command to set target"% + ( + gt_logger.gt_bright('mbedgt -t '), + gt_logger.gt_bright('yotta target ') + )) + raise YottaError("Yotta target not set in current directory!") + + ### Use yotta to search mapping between platform names and available platforms + # Convert platform:target, ... mapping to data structure + yt_target_to_map_platform = {} + if opts.map_platform_to_yt_target: + gt_logger.gt_log("user defined platform -> target supported mapping definition (specified with --map-target switch)") + for mapping in opts.map_platform_to_yt_target.split(','): + if len(mapping.split(':')) == 2: + yt_target, platform = mapping.split(':') + yt_target_to_map_platform[yt_target] = platform + gt_logger.gt_log_tab("mapped yotta target '%s' to be compatible with platform '%s'"% ( + gt_logger.gt_bright(yt_target), + gt_logger.gt_bright(platform) + )) + else: + gt_logger.gt_log_tab("unknown format '%s', use 'target:platform' format"% mapping) + + for yt_target in yt_targets: + if yt_target in yt_target_to_map_platform: + platform = yt_target_to_map_platform[yt_target] + else: + # get it from local Yotta target + platform = get_platform_name_from_yotta_target(yt_target) + + # Toolchain doesn't matter as Greentea does not have to do any selection for it unlike platform + toolchain = yt_target + yotta_config = YottaConfig() + yotta_config.init(yt_target) + baud_rate = yotta_config.get_baudrate() + base_path = os.path.join('.', 'build', yt_target) + tb = TestBuild(yt_target, platform, toolchain, baud_rate, base_path) + test_spec.add_test_builds(yt_target, tb) + + # Find tests + ctest_test_list = load_ctest_testsuite(base_path, + binary_type=get_binary_type_for_platform(platform)) + for name, path in ctest_test_list.items(): + t = Test(name) + t.add_binary(path, TestBinary.BIN_TYPE_BOOTABLE) + tb.add_test(name, t) + + return test_spec diff --git a/tools/python/mbed_os_tools/test/mbed_yotta_module_parse.py b/tools/python/mbed_os_tools/test/mbed_yotta_module_parse.py new file mode 100644 index 0000000000..55a936ea22 --- /dev/null +++ b/tools/python/mbed_os_tools/test/mbed_yotta_module_parse.py @@ -0,0 +1,124 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 json + + +class YottaConfig(object): + + yotta_config = None + + def __init__(self): + self.BUILD_DIR = 'build' + self.YOTTA_CONFIG_NAME = 'yotta_config.json' + self.DEFAULT_BAUDRATE = 115200 + + def init(self, target_name): + """! Loads yotta_config.json as an object from local yotta build directory + @return True if data was successfuly loaded from the file + """ + try: + path = os.path.join(self.BUILD_DIR, target_name, self.YOTTA_CONFIG_NAME) + with open(path, 'r') as data_file: + self.yotta_config = json.load(data_file) + except IOError as e: + self.yotta_config = {} + return bool(len(self.yotta_config)) + + def set_yotta_config(self, yotta_config): + self.yotta_config = yotta_config + + def get_baudrate(self): + """! Returns default baudrate for stdio serial + @return Configuration baudrate of default on (115200) + Example yotta_config.json + { + "minar": { + "initial_event_pool_size": 50, + "additional_event_pools_size": 100 + }, + "mbed-os": { + "net": { + "stacks": { + "lwip": true + } + }, + "stdio": { + "default-baud": 9600 + } + }, + """ + # Get default baudrate for this target + if self.yotta_config and 'mbed-os' in self.yotta_config: + if 'stdio' in self.yotta_config['mbed-os']: + if 'default-baud' in self.yotta_config['mbed-os']['stdio']: + return int(self.yotta_config['mbed-os']['stdio']['default-baud']) + return self.DEFAULT_BAUDRATE + + def get_test_pins(self): + if self.yotta_config and 'hardware' in self.yotta_config: + if 'test-pins' in self.yotta_config['hardware']: + return self.yotta_config['hardware']['test-pins'] + return None + + +class YottaModule(object): + + __yotta_module = None + __greentea_client = 'greentea-client' + + def __init__(self): + self.MODULE_PATH = '.' + self.YOTTA_CONFIG_NAME = 'module.json' + + def init(self): + """! Loads yotta_module.json as an object from local yotta build directory + @return True if data was successfuly loaded from the file + """ + self.__yotta_module = dict() + + try: + path = os.path.join(self.MODULE_PATH, self.YOTTA_CONFIG_NAME) + if os.path.exists(path): + # Load module.json only if it exists + with open(path, 'r') as data_file: + self.__yotta_module = json.load(data_file) + except IOError as e: + print("YottaModule: error - %s" % str(e)) + return bool(self.__yotta_module) # bool({}) == False + + def set_yotta_module(self, yotta_module): + self.__yotta_module = yotta_module + + def get_data(self): + return self.__yotta_module + + def get_name(self): + return self.__yotta_module.get('name', 'unknown') + + def check_greentea_client(self): + if self.get_name() == self.__greentea_client: + return True + + dependencies = self.__yotta_module.get('dependencies', False) + testDependencies = self.__yotta_module.get('testDependencies', False) + if dependencies: + if dependencies.get(self.__greentea_client, False): + return True + if testDependencies: + if testDependencies.get(self.__greentea_client, False): + return True + return False diff --git a/tools/python/mbed_os_tools/test/tests_spec.py b/tools/python/mbed_os_tools/test/tests_spec.py new file mode 100644 index 0000000000..360fab025d --- /dev/null +++ b/tools/python/mbed_os_tools/test/tests_spec.py @@ -0,0 +1,358 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +""" +This module contains classes to represent Test Specification interface that +defines the data to be generated by/from a build system to give enough information +to Greentea. +""" + +import json + + +class TestBinary(object): + """ + Class representing a Test Binary. + """ + + KW_BIN_TYPE = "binary_type" + KW_BIN_PATH = "path" + KW_COMP_LOG = "compare_log" + + BIN_TYPE_BOOTABLE = "bootable" + BIN_TYPE_DEFAULT = BIN_TYPE_BOOTABLE + SUPPORTED_BIN_TYPES = [BIN_TYPE_BOOTABLE] + + def __init__(self, path, binary_type, compare_log): + """ + ctor. + + :param path: + :param binary_type: + :return: + """ + assert binary_type in TestBinary.SUPPORTED_BIN_TYPES, ( + "Binary type %s not supported. Supported types [%s]" + % (binary_type, ", ".join(TestBinary.SUPPORTED_BIN_TYPES)) + ) + self.__path = path + self.__flash_method = binary_type + self.__comp_log = compare_log + + def get_path(self): + """ + Gives binary path. + :return: + """ + return self.__path + + def get_compare_log(self): + """ + Gives compare log file. + :return: + """ + return self.__comp_log + + +class Test(object): + """ + class representing a Test artifact that may contain more than one test binaries. + """ + + KW_TEST_NAME = "name" + KW_TEST_BINS = "binaries" + + def __init__(self, name, default_flash_method=None): + """ + ctor. + + :param name: + :param default_flash_method: + :return: + """ + self.__name = name + self.__default_flash_method = default_flash_method + self.__binaries_by_flash_method = {} + + def get_name(self): + """ + Gives test name. + + :return: + """ + return self.__name + + def get_binary(self, binary_type=TestBinary.BIN_TYPE_DEFAULT): + """ + Gives a test binary of specific flash type. + + :param binary_type: + :return: + """ + return self.__binaries_by_flash_method.get(binary_type, None) + + def parse(self, test_json): + """ + Parse json contents into object. + + :param test_json: + :return: + """ + assert Test.KW_TEST_BINS in test_json, "Test spec should contain key `binaries`" + for binary in test_json[Test.KW_TEST_BINS]: + mandatory_keys = [TestBinary.KW_BIN_PATH] + assert set(mandatory_keys).issubset( + set(binary.keys()) + ), "Binary spec should contain key [%s]" % ",".join(mandatory_keys) + fm = binary.get(TestBinary.KW_BIN_TYPE, self.__default_flash_method) + assert fm is not None, "Binary type not specified in build and binary spec." + tb = TestBinary(binary[TestBinary.KW_BIN_PATH], + fm, + binary.get(TestBinary.KW_COMP_LOG)) + self.__binaries_by_flash_method[fm] = tb + + def add_binary(self, path, binary_type, compare_log=None): + """ + Add binary to the test. + + :param path: + :param binary_type: + :return: + """ + self.__binaries_by_flash_method[binary_type] = TestBinary(path, + binary_type, + compare_log) + + +class TestBuild(object): + """ + class for Test build. + """ + + KW_TEST_BUILD_NAME = "name" + KW_PLATFORM = "platform" + KW_TOOLCHAIN = "toolchain" + KW_BAUD_RATE = "baud_rate" + KW_BUILD_BASE_PATH = "base_path" + KW_TESTS = "tests" + KW_BIN_TYPE = "binary_type" + + def __init__( + self, name, platform, toolchain, baud_rate, base_path, default_flash_method=None + ): + """ + ctor. + + :param name: + :param platform: + :param toolchain: + :param baud_rate: + :param base_path: + :param default_flash_method: + :return: + """ + self.__name = name + self.__platform = platform + self.__toolchain = toolchain + self.__baud_rate = baud_rate + self.__base_path = base_path + self.__default_flash_method = default_flash_method + self.__tests = {} + + def get_name(self): + """ + Gives build name. + + :return: + """ + return self.__name + + def get_platform(self): + """ + Gives mbed classic platform name. + + :return: + """ + return self.__platform + + def get_toolchain(self): + """ + Gives toolchain + + :return: + """ + return self.__toolchain + + def get_baudrate(self): + """ + Gives baud rate. + + :return: + """ + return self.__baud_rate + + def get_path(self): + """ + Gives path. + + :return: + """ + return self.__base_path + + def get_tests(self): + """ + Gives tests dict keyed by test name. + + :return: + """ + return self.__tests + + def parse(self, build_spec): + """ + Parse Test build json. + + :param build_spec: + :return: + """ + assert TestBuild.KW_TESTS in build_spec, ( + "Build spec should contain key '%s'" % TestBuild.KW_TESTS + ) + for name, test_json in build_spec[TestBuild.KW_TESTS].items(): + test = Test(name, default_flash_method=self.__default_flash_method) + test.parse(test_json) + self.__tests[name] = test + + def add_test(self, name, test): + """ + Add test. + + :param name: + :param test: + :return: + """ + self.__tests[name] = test + + +class TestSpec(object): + """ + Test specification. Contains Builds. + """ + + KW_BUILDS = "builds" + test_spec_filename = "runtime_load" + + def __init__(self, test_spec_filename=None): + """ + ctor. + + :return: + """ + self.__target_test_spec = {} + if test_spec_filename: + self.test_spec_filename = test_spec_filename + self.load(self.test_spec_filename) + + def load(self, test_spec_filename): + """ + Load test spec directly from file + + :param test_spec_filename: Name of JSON file with TestSpec to load + :return: Treu if load was successful + """ + try: + with open(test_spec_filename, "r") as f: + self.parse(json.load(f)) + except Exception as e: + print("TestSpec::load('%s') %s" % (test_spec_filename, str(e))) + return False + + self.test_spec_filename = test_spec_filename + return True + + def parse(self, spec): + """ + Parse test spec json. + + :param spec: + :return: + """ + assert TestSpec.KW_BUILDS, ( + "Test spec should contain key '%s'" % TestSpec.KW_BUILDS + ) + for build_name, build in spec[TestSpec.KW_BUILDS].items(): + mandatory_keys = [ + TestBuild.KW_PLATFORM, + TestBuild.KW_TOOLCHAIN, + TestBuild.KW_BAUD_RATE, + TestBuild.KW_BUILD_BASE_PATH, + ] + assert set(mandatory_keys).issubset(set(build.keys())), ( + "Build spec should contain keys [%s]. It has [%s]" + % (",".join(mandatory_keys), ",".join(build.keys())) + ) + platform = build[TestBuild.KW_PLATFORM] + toolchain = build[TestBuild.KW_TOOLCHAIN] + + # If there is no 'name' property in build, we will use build key + # as build name + name = build.get(TestBuild.KW_TEST_BUILD_NAME, build_name) + + tb = TestBuild( + name, + platform, + toolchain, + build[TestBuild.KW_BAUD_RATE], + build[TestBuild.KW_BUILD_BASE_PATH], + build.get(TestBuild.KW_BIN_TYPE, None), + ) + tb.parse(build) + self.__target_test_spec[name] = tb + + def get_test_builds(self, filter_by_names=None): + """ + Gives test builds. + :param filter_by_names: List of names of builds you want to filter + in your result + :return: + """ + result = [] + if filter_by_names: + assert type(filter_by_names) is list + for tb in self.__target_test_spec.values(): + if tb.get_name() in filter_by_names: + result.append(tb) + else: + # When filtering by name is not defined we will return all builds objects + result = list(self.__target_test_spec.values()) + return result + + def get_test_build(self, build_name): + """ + Gives test build with given name. + + :param build_name: + :return: + """ + return self.__target_test_spec.get(build_name, None) + + def add_test_builds(self, name, test_build): + """ + Add test build. + + :param name: + :param test_build: + :return: + """ + self.__target_test_spec[name] = test_build diff --git a/tools/python/mbed_tools/README.md b/tools/python/mbed_tools/README.md new file mode 100644 index 0000000000..ba1621030e --- /dev/null +++ b/tools/python/mbed_tools/README.md @@ -0,0 +1,96 @@ +# Mbed Tools + +![Package](https://badgen.net/badge/Package/mbed-tools/grey) +[![Documentation](https://badgen.net/badge/Documentation/GitHub%20Pages/blue?icon=github)](https://armmbed.github.io/mbed-tools/api/) +[![PyPI](https://badgen.net/pypi/v/mbed-tools)](https://pypi.org/project/mbed-tools/) +[![PyPI - Status](https://img.shields.io/pypi/status/mbed-tools)](https://pypi.org/project/mbed-tools/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mbed-tools)](https://pypi.org/project/mbed-tools/) + +[![License](https://badgen.net/pypi/license/mbed-tools)](https://github.com/ARMmbed/mbed-tools/blob/master/LICENSE) + +[![Build Status](https://dev.azure.com/mbed-tools/mbed-tools/_apis/build/status/Build%20and%20Release?branchName=master&stageName=CI%20Checkpoint)](https://dev.azure.com/mbed-tools/mbed-tools/_build/latest?definitionId=10&branchName=master) +[![Test Coverage](https://codecov.io/gh/ARMmbed/mbed-tools/branch/master/graph/badge.svg)](https://codecov.io/gh/ARMmbed/mbed-tools) +[![Maintainability](https://api.codeclimate.com/v1/badges/b9fca0e16f7a85da7674/maintainability)](https://codeclimate.com/github/ARMmbed/mbed-tools/maintainability) + +## Overview + +This is the **future** command line tool for Mbed OS. It provides the ability to detect Mbed Enabled devices connected +by USB, checkout Mbed projects and perform builds amongst other operations. + +> :warning: While this package is generally available it is not complete. The available functionality can be viewed with +> the `--help` option once installed. Please note that the current tools for Mbed OS 5.x and above can be found at +> https://github.com/ARMmbed/mbed-cli. + +## Releases + +For release notes and a history of changes of all **production** releases, please see the following: + +- [Changelog](https://github.com/ARMmbed/mbed-tools/blob/master/CHANGELOG.md) + +For a the list of all available versions please, please see the: + +- [PyPI Release History](https://pypi.org/project/mbed-tools/#history) + +## Versioning + +The version scheme used follows [PEP440](https://www.python.org/dev/peps/pep-0440/) and +[Semantic Versioning](https://semver.org/). For production quality releases the version will look as follows: + +- `..` + +Pre releases are used to give early access to new functionality, for testing and to get feedback on experimental +features. As such these releases may not be stable and should not be used for production. Additionally any interfaces +introduced in a pre release may be removed or changed without notice. For pre releases the version will look as +follows: + +- `...dev` + +## Installation + +`mbed-tools` relies on the Ninja build system and CMake. +- CMake. [Install version 3.19.0 or newer for all operating systems](https://cmake.org/install/). +- Ninja. [Install version 1.0 or newer for all operating systems](https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages). + +We recommend installing `mbed-tools` in a Python virtual environment to avoid dependency conflicts. + +To install the most recent production quality release use: + +``` +pip install mbed-tools +``` + +To install a specific release: + +``` +pip install mbed-tools== +``` + +## Usage + +Interface definition and usage documentation (for developers of Mbed OS tooling) is available for the most recent +production release here: + +- [GitHub Pages](https://armmbed.github.io/mbed-tools/api/) + +## Project Structure + +The follow described the major aspects of the project structure: + +- `azure-pipelines/` - CI configuration files for Azure Pipelines. +- `src/mbed_tools/` - Python source files. +- `news/` - Collection of news files for unreleased changes. +- `tests/` - Unit and integration tests. + +## Getting Help + +- For interface definition and usage documentation, please see [GitHub Pages](https://armmbed.github.io/mbed-tools/api/). +- For a list of known issues and possible work arounds, please see [Known Issues](KNOWN_ISSUES.md). +- To raise a defect or enhancement please use [GitHub Issues](https://github.com/ARMmbed/mbed-tools/issues). +- To ask a question please use the [Mbed Forum](https://forums.mbed.com/). + +## Contributing + +- Mbed OS is an open source project and we are committed to fostering a welcoming community, please see our + [Code of Conduct](https://github.com/ARMmbed/mbed-tools/blob/master/CODE_OF_CONDUCT.md) for more information. +- For how to contribute to the project, including how to develop the project, + please see the [Contributions Guidelines](https://github.com/ARMmbed/mbed-tools/blob/master/CONTRIBUTING.md) diff --git a/tools/python/mbed_tools/__init__.py b/tools/python/mbed_tools/__init__.py new file mode 100644 index 0000000000..93fe0810fc --- /dev/null +++ b/tools/python/mbed_tools/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Exposes the primary interfaces for the library.""" diff --git a/tools/python/mbed_tools/build/__init__.py b/tools/python/mbed_tools/build/__init__.py new file mode 100644 index 0000000000..0ddf6a6dca --- /dev/null +++ b/tools/python/mbed_tools/build/__init__.py @@ -0,0 +1,15 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Provides the core build system for Mbed OS, which relies on CMake and Ninja as underlying technologies. + +The functionality covered in this package includes the following: + +- Execution of Mbed Pre-Build stages to determine appropriate configuration for Mbed OS and the build process. +- Invocation of the build process for the command line tools and online build service. +- Export of build instructions to third party command line tools and IDEs. +""" +from mbed_tools.build.build import build_project, generate_build_system +from mbed_tools.build.config import generate_config +from mbed_tools.build.flash import flash_binary diff --git a/tools/python/mbed_tools/build/_internal/__init__.py b/tools/python/mbed_tools/build/_internal/__init__.py new file mode 100644 index 0000000000..868c3a2950 --- /dev/null +++ b/tools/python/mbed_tools/build/_internal/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Code not to be accessed by external applications.""" diff --git a/tools/python/mbed_tools/build/_internal/cmake_file.py b/tools/python/mbed_tools/build/_internal/cmake_file.py new file mode 100644 index 0000000000..d6b550bffc --- /dev/null +++ b/tools/python/mbed_tools/build/_internal/cmake_file.py @@ -0,0 +1,39 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Module in charge of CMake file generation.""" +import pathlib + +from typing import Any + +import jinja2 + +from mbed_tools.build._internal.config.config import Config + +TEMPLATES_DIRECTORY = pathlib.Path("_internal", "templates") +TEMPLATE_NAME = "mbed_config.tmpl" + + +def render_mbed_config_cmake_template(config: Config, toolchain_name: str, target_name: str) -> str: + """Renders the mbed_config jinja template with the target and project config settings. + + Args: + config: Config object holding information parsed from the mbed config system. + toolchain_name: Name of the toolchain being used. + target_name: Name of the target. + + Returns: + The rendered mbed_config template. + """ + env = jinja2.Environment(loader=jinja2.PackageLoader("mbed_tools.build", str(TEMPLATES_DIRECTORY)),) + env.filters["to_hex"] = to_hex + template = env.get_template(TEMPLATE_NAME) + config["supported_c_libs"] = [x for x in config["supported_c_libs"][toolchain_name.lower()]] + context = {"target_name": target_name, "toolchain_name": toolchain_name, **config} + return template.render(context) + + +def to_hex(s: Any) -> str: + """Filter to convert integers to hex.""" + return hex(int(s, 0)) diff --git a/tools/python/mbed_tools/build/_internal/config/__init__.py b/tools/python/mbed_tools/build/_internal/config/__init__.py new file mode 100644 index 0000000000..ae3da2a9c6 --- /dev/null +++ b/tools/python/mbed_tools/build/_internal/config/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Use this module to assemble build configuration.""" diff --git a/tools/python/mbed_tools/build/_internal/config/assemble_build_config.py b/tools/python/mbed_tools/build/_internal/config/assemble_build_config.py new file mode 100644 index 0000000000..03b4ef9511 --- /dev/null +++ b/tools/python/mbed_tools/build/_internal/config/assemble_build_config.py @@ -0,0 +1,117 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Configuration assembly algorithm.""" +import itertools + +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable, List, Optional, Set + +from mbed_tools.build._internal.config.config import Config +from mbed_tools.build._internal.config import source +from mbed_tools.build._internal.find_files import LabelFilter, RequiresFilter, filter_files, find_files + + +def assemble_config(target_attributes: dict, search_paths: Iterable[Path], mbed_app_file: Optional[Path]) -> Config: + """Assemble config for given target and program directory. + + Mbed library and application specific config parameters are parsed from mbed_lib.json and mbed_app.json files + located in the project source tree. + The config files contain sets of "labels" which correspond to directory names in the mbed-os source tree. These + labels are used to determine which mbed_lib.json files to include in the final configuration. + + The mbed_app.json config overrides must be applied after all the mbed_lib.json files have been parsed. + Unfortunately, mbed_app.json may also contain filter labels to tell us which mbed libs we're depending on. + This means we have to collect the filter labels from mbed_app.json before parsing any other config files. + Then we parse all the required mbed_lib config and finally apply the app overrides. + + Args: + target_attributes: Mapping of target specific config parameters. + search_paths: Iterable of paths to search for mbed_lib.json files. + mbed_app_file: The path to mbed_app.json. This can be None. + """ + mbed_lib_files = list( + set( + itertools.chain.from_iterable( + find_files("mbed_lib.json", path.absolute().resolve()) for path in search_paths + ) + ) + ) + return _assemble_config_from_sources(target_attributes, mbed_lib_files, mbed_app_file) + + +def _assemble_config_from_sources( + target_attributes: dict, mbed_lib_files: List[Path], mbed_app_file: Optional[Path] = None +) -> Config: + config = Config(source.prepare(target_attributes, source_name="target")) + previous_filter_data = None + app_data = None + if mbed_app_file: + # We need to obtain the file filter data from mbed_app.json so we can select the correct set of mbed_lib.json + # files to include in the config. We don't want to update the config object with all of the app settings yet + # as we won't be able to apply overrides correctly until all relevant mbed_lib.json files have been parsed. + app_data = source.from_file( + mbed_app_file, default_name="app", target_filters=FileFilterData.from_config(config).labels + ) + _get_app_filter_labels(app_data, config) + + current_filter_data = FileFilterData.from_config(config) + while previous_filter_data != current_filter_data: + filtered_files = _filter_files(mbed_lib_files, current_filter_data) + for config_file in filtered_files: + config.update(source.from_file(config_file, target_filters=current_filter_data.labels)) + # Remove any mbed_lib files we've already visited from the list so we don't parse them multiple times. + mbed_lib_files.remove(config_file) + + previous_filter_data = current_filter_data + current_filter_data = FileFilterData.from_config(config) + + # Apply mbed_app.json data last so config parameters are overriden in the correct order. + if app_data: + config.update(app_data) + + return config + + +def _get_app_filter_labels(mbed_app_data: dict, config: Config) -> None: + requires = mbed_app_data.get("requires") + if requires: + config["requires"] = requires + + config.update(_get_file_filter_overrides(mbed_app_data)) + + +def _get_file_filter_overrides(mbed_app_data: dict) -> dict: + return {"overrides": [override for override in mbed_app_data.get("overrides", []) if override.modifier]} + + +@dataclass(frozen=True) +class FileFilterData: + """Data used to navigate mbed-os directories for config files.""" + + labels: Set[str] + features: Set[str] + components: Set[str] + requires: Set[str] + + @classmethod + def from_config(cls, config: Config) -> "FileFilterData": + """Extract file filters from a Config object.""" + return cls( + labels=config.get("labels", set()) | config.get("extra_labels", set()), + features=set(config.get("features", set())), + components=set(config.get("components", set())), + requires=set(config.get("requires", set())), + ) + + +def _filter_files(files: Iterable[Path], filter_data: FileFilterData) -> Iterable[Path]: + filters = ( + LabelFilter("TARGET", filter_data.labels), + LabelFilter("FEATURE", filter_data.features), + LabelFilter("COMPONENT", filter_data.components), + RequiresFilter(filter_data.requires), + ) + return filter_files(files, filters) diff --git a/tools/python/mbed_tools/build/_internal/config/config.py b/tools/python/mbed_tools/build/_internal/config/config.py new file mode 100644 index 0000000000..61ce6772a6 --- /dev/null +++ b/tools/python/mbed_tools/build/_internal/config/config.py @@ -0,0 +1,85 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Build configuration representation.""" +import logging + +from collections import UserDict +from typing import Any, Iterable, Hashable, List + +from mbed_tools.build._internal.config.source import Override, ConfigSetting + +logger = logging.getLogger(__name__) + + +class Config(UserDict): + """Mapping of config settings. + + This object understands how to populate the different 'config sections' which all have different rules for how the + settings are collected. + Applies overrides, appends macros, and updates config settings. + """ + + def __setitem__(self, key: Hashable, item: Any) -> None: + """Set an item based on its key.""" + if key == CONFIG_SECTION: + self._update_config_section(item) + elif key == OVERRIDES_SECTION: + self._handle_overrides(item) + elif key == MACROS_SECTION: + self.data[MACROS_SECTION] = self.data.get(MACROS_SECTION, set()) | item + elif key == REQUIRES_SECTION: + self.data[REQUIRES_SECTION] = self.data.get(REQUIRES_SECTION, set()) | item + else: + super().__setitem__(key, item) + + def _handle_overrides(self, overrides: Iterable[Override]) -> None: + for override in overrides: + logger.debug("Applying override '%s.%s'='%s'", override.namespace, override.name, repr(override.value)) + if override.name in self.data: + _apply_override(self.data, override) + continue + + setting = next( + filter( + lambda x: x.name == override.name and x.namespace == override.namespace, + self.data.get(CONFIG_SECTION, []), + ), + None, + ) + if setting is None: + logger.warning( + f"You are attempting to override an undefined config parameter " + f"`{override.namespace}.{override.name}`.\n" + "It is an error to override an undefined configuration parameter. " + "Please check your target_overrides are correct.\n" + f"The parameter `{override.namespace}.{override.name}` will not be added to the configuration." + ) + else: + setting.value = override.value + + def _update_config_section(self, config_settings: List[ConfigSetting]) -> None: + for setting in config_settings: + logger.debug("Adding config setting: '%s.%s'", setting.namespace, setting.name) + if setting in self.data.get(CONFIG_SECTION, []): + raise ValueError( + f"Setting {setting.namespace}.{setting.name} already defined. You cannot duplicate config settings!" + ) + + self.data[CONFIG_SECTION] = self.data.get(CONFIG_SECTION, []) + config_settings + + +CONFIG_SECTION = "config" +MACROS_SECTION = "macros" +OVERRIDES_SECTION = "overrides" +REQUIRES_SECTION = "requires" + + +def _apply_override(data: dict, override: Override) -> None: + if override.modifier == "add": + data[override.name] |= override.value + elif override.modifier == "remove": + data[override.name] -= override.value + else: + data[override.name] = override.value diff --git a/tools/python/mbed_tools/build/_internal/config/source.py b/tools/python/mbed_tools/build/_internal/config/source.py new file mode 100644 index 0000000000..9cfbc194a5 --- /dev/null +++ b/tools/python/mbed_tools/build/_internal/config/source.py @@ -0,0 +1,175 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Configuration source parser.""" +import logging +import pathlib + +from dataclasses import dataclass +from typing import Iterable, Any, Optional, List + +from mbed_tools.lib.json_helpers import decode_json_file +from mbed_tools.build.exceptions import InvalidConfigOverride +from mbed_tools.lib.python_helpers import flatten_nested + +logger = logging.getLogger(__name__) + + +def from_file( + config_source_file_path: pathlib.Path, target_filters: Iterable[str], default_name: Optional[str] = None +) -> dict: + """Load a JSON config file and prepare the contents as a config source.""" + return prepare(decode_json_file(config_source_file_path), source_name=default_name, target_filters=target_filters) + + +def prepare( + input_data: dict, source_name: Optional[str] = None, target_filters: Optional[Iterable[str]] = None +) -> dict: + """Prepare a config source for entry into the Config object. + + Extracts config and override settings from the source. Flattens these nested dictionaries out into + lists of objects which are namespaced in the way the Mbed config system expects. + + Args: + input_data: The raw config JSON object parsed from the config file. + source_name: Optional default name to use for namespacing config settings. If the input_data contains a 'name' + field, that field is used as the namespace. + target_filters: List of filter string used when extracting data from target_overrides section of the config + data. + + Returns: + Prepared config source. + """ + data = input_data.copy() + namespace = data.pop("name", source_name) + for key in data: + data[key] = _sanitise_value(data[key]) + + if "config" in data: + data["config"] = _extract_config_settings(namespace, data["config"]) + + if "overrides" in data: + data["overrides"] = _extract_overrides(namespace, data["overrides"]) + + if "target_overrides" in data: + data["overrides"] = _extract_target_overrides( + namespace, data.pop("target_overrides"), target_filters if target_filters is not None else [] + ) + + return data + + +@dataclass +class ConfigSetting: + """Representation of a config setting. + + Auto converts any list values to sets for faster operations and de-duplication of values. + """ + + namespace: str + name: str + value: Any + help_text: Optional[str] = None + macro_name: Optional[str] = None + + def __post_init__(self) -> None: + """Convert the value to a set if applicable.""" + self.value = _sanitise_value(self.value) + + +@dataclass +class Override: + """Representation of a config override. + + Checks for _add or _remove modifiers and splits them from the name. + """ + + namespace: str + name: str + value: Any + modifier: Optional[str] = None + + def __post_init__(self) -> None: + """Parse modifiers and convert list values to sets.""" + if self.name.endswith("_add") or self.name.endswith("_remove"): + self.name, self.modifier = self.name.rsplit("_", maxsplit=1) + + self.value = _sanitise_value(self.value) + + +def _extract_config_settings(namespace: str, config_data: dict) -> List[ConfigSetting]: + settings = [] + for name, item in config_data.items(): + logger.debug("Extracting config setting from '%s': '%s'='%s'", namespace, name, item) + if isinstance(item, dict): + macro_name = item.get("macro_name") + help_text = item.get("help") + value = item.get("value") + else: + macro_name = None + help_text = None + value = item + + setting = ConfigSetting( + namespace=namespace, name=name, macro_name=macro_name, help_text=help_text, value=value, + ) + # If the config item is about a certain component or feature + # being present, avoid adding it to the mbed_config.cmake + # configuration file. Instead, applications should depend on + # the feature or component with target_link_libraries() and the + # component's CMake file (in the Mbed OS repo) will create + # any necessary macros or definitions. + if setting.name == "present": + continue + + settings.append(setting) + + return settings + + +def _extract_target_overrides( + namespace: str, override_data: dict, allowed_target_labels: Iterable[str] +) -> List[Override]: + valid_target_data = dict() + for target_type in override_data: + if target_type == "*" or target_type in allowed_target_labels: + valid_target_data.update(override_data[target_type]) + + return _extract_overrides(namespace, valid_target_data) + + +def _extract_overrides(namespace: str, override_data: dict) -> List[Override]: + overrides = [] + for name, value in override_data.items(): + try: + override_namespace, override_name = name.split(".") + if override_namespace and override_namespace not in [namespace, "target"] and namespace != "app": + raise InvalidConfigOverride( + "It is only possible to override config settings defined in an mbed_lib.json from mbed_app.json. " + f"An override was defined by the lib `{namespace}` that attempts to override " + f"`{override_namespace}.{override_name}`." + ) + except ValueError: + override_namespace = namespace + override_name = name + + overrides.append(Override(namespace=override_namespace, name=override_name, value=value)) + + return overrides + + +def _sanitise_value(val: Any) -> Any: + """Convert list values to sets and return scalar values and strings unchanged. + + For whatever reason, we allowed config settings to have values of any type available in the JSON spec. + The value type can be a list, nested list, str, int, you name it. + + When we process the config, we want to use sets instead of lists, this is for two reasons: + * To take advantage of set operations when we deal with "cumulative" settings. + * To prevent any duplicate settings ending up in the final config. + """ + if isinstance(val, list): + return set(flatten_nested(val)) + + return val diff --git a/tools/python/mbed_tools/build/_internal/find_files.py b/tools/python/mbed_tools/build/_internal/find_files.py new file mode 100644 index 0000000000..1dba384ab8 --- /dev/null +++ b/tools/python/mbed_tools/build/_internal/find_files.py @@ -0,0 +1,157 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Find files in MbedOS program directory.""" +from pathlib import Path +import fnmatch +from typing import Callable, Iterable, Optional, List, Tuple + +from mbed_tools.lib.json_helpers import decode_json_file + + +def find_files(filename: str, directory: Path) -> List[Path]: + """Proxy to `_find_files`, which applies legacy filtering rules.""" + # Temporary workaround, which replicates hardcoded ignore rules from old tools. + # Legacy list of ignored directories is longer, however "TESTS" and + # "TEST_APPS" were the only ones that actually exist in the MbedOS source. + # Ideally, this should be solved by putting an `.mbedignore` file in the root of MbedOS repo, + # similarly to what the code below pretends is happening. + legacy_ignore = MbedignoreFilter(("*/TESTS", "*/TEST_APPS")) + return _find_files(filename, directory, [legacy_ignore]) + + +def _find_files(filename: str, directory: Path, filters: Optional[List[Callable]] = None) -> List[Path]: + """Recursively find files by name under a given directory. + + This function automatically applies rules from .mbedignore files found during traversal. + + It is important to realise that applied filters are "greedy". The moment a directory is filtered out, + its children won't be traversed. + + Args: + filename: Name of the file to look for. + directory: Location where search starts. + filters: Optional list of exclude filters to apply. + """ + if filters is None: + filters = [] + + result: List[Path] = [] + + # Directories and files to process + children = list(directory.iterdir()) + + # If .mbedignore is one of the children, we need to add it to filter list, + # as it might contain rules for currently processed directory, as well as its descendants. + mbedignore = Path(directory, ".mbedignore") + if mbedignore in children: + filters = filters + [MbedignoreFilter.from_file(mbedignore)] + + # Remove files and directories that don't match current set of filters + filtered_children = filter_files(children, filters) + + for child in filtered_children: + if child.is_symlink(): + child = child.absolute().resolve() + + if child.is_dir(): + # If processed child is a directory, recurse with current set of filters + result += _find_files(filename, child, filters) + + if child.is_file() and child.name == filename: + # We've got a match + result.append(child) + + return result + + +def filter_files(files: Iterable[Path], filters: Iterable[Callable]) -> Iterable[Path]: + """Filter given paths to files using filter callables.""" + return [file for file in files if all(f(file) for f in filters)] + + +class RequiresFilter: + """Filter out mbed libraries not needed by application. + + The 'requires' config option in mbed_app.json can specify list of mbed + libraries (mbed_lib.json) that application requires. Apply 'requires' + filter to remove mbed_lib.json files not required by application. + """ + + def __init__(self, requires: Iterable[str]): + """Initialise the filter attributes. + + Args: + requires: List of required mbed libraries. + """ + self._requires = requires + + def __call__(self, path: Path) -> bool: + """Return True if no requires are specified or our lib name is in the list of required libs.""" + return decode_json_file(path).get("name", "") in self._requires or not self._requires + + +class LabelFilter: + """Filter out given paths using path labelling rules. + + If a path is labelled with given type, but contains label value which is + not allowed, it will be filtered out. + + An example of labelled path is "/mbed-os/rtos/source/TARGET_CORTEX/mbed_lib.json", + where label type is "TARGET" and label value is "CORTEX". + + For example, given a label type "FEATURE" and allowed values ["FOO"]: + - "/path/FEATURE_FOO/somefile.txt" will not be filtered out + - "/path/FEATURE_BAZ/somefile.txt" will be filtered out + - "/path/FEATURE_FOO/FEATURE_BAR/somefile.txt" will be filtered out + """ + + def __init__(self, label_type: str, allowed_label_values: Iterable[str]): + """Initialise the filter attributes. + + Args: + label_type: Type of the label to filter with. In filtered paths, it prefixes the value. + allowed_label_values: Values which are allowed for the given label type. + """ + self._label_type = label_type + self._allowed_labels = set(f"{label_type}_{label_value}" for label_value in allowed_label_values) + + def __call__(self, path: Path) -> bool: + """Return True if given path contains only allowed labels - should not be filtered out.""" + labels = set(part for part in path.parts if self._label_type in part) + return labels.issubset(self._allowed_labels) + + +class MbedignoreFilter: + """Filter out given paths based on rules found in .mbedignore files. + + Patterns in .mbedignore use unix shell-style wildcards (fnmatch). It means + that functionality, although similar is different to that found in + .gitignore and friends. + """ + + def __init__(self, patterns: Tuple[str, ...]): + """Initialise the filter attributes. + + Args: + patterns: List of patterns from .mbedignore to filter against. + """ + self._patterns = patterns + + def __call__(self, path: Path) -> bool: + """Return True if given path doesn't match .mbedignore patterns - should not be filtered out.""" + stringified = str(path) + return not any(fnmatch.fnmatch(stringified, pattern) for pattern in self._patterns) + + @classmethod + def from_file(cls, mbedignore_path: Path) -> "MbedignoreFilter": + """Return new instance with patterns read from .mbedignore file. + + Constructed patterns are rooted in the directory of .mbedignore file. + """ + lines = mbedignore_path.read_text().splitlines() + pattern_lines = (line for line in lines if line.strip() and not line.startswith("#")) + ignore_root = mbedignore_path.parent + patterns = tuple(str(ignore_root.joinpath(pattern)) for pattern in pattern_lines) + return cls(patterns) diff --git a/tools/python/mbed_tools/build/_internal/templates/__init__.py b/tools/python/mbed_tools/build/_internal/templates/__init__.py new file mode 100644 index 0000000000..6f2b0dde32 --- /dev/null +++ b/tools/python/mbed_tools/build/_internal/templates/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Integration with https://github.com/ARMmbed/mbed-tools.""" diff --git a/tools/python/mbed_tools/build/_internal/templates/mbed_config.tmpl b/tools/python/mbed_tools/build/_internal/templates/mbed_config.tmpl new file mode 100644 index 0000000000..3250a52370 --- /dev/null +++ b/tools/python/mbed_tools/build/_internal/templates/mbed_config.tmpl @@ -0,0 +1,96 @@ +# Copyright (C) 2020 Arm Mbed. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Automatically generated configuration file. +# DO NOT EDIT. Content may be overwritten. + +include_guard(GLOBAL) + +set(MBED_TOOLCHAIN "{{toolchain_name}}" CACHE STRING "") +set(MBED_TARGET "{{target_name}}" CACHE STRING "") +set(MBED_CPU_CORE "{{core}}" CACHE STRING "") +set(MBED_C_LIB "{{c_lib}}" CACHE STRING "") +set(MBED_PRINTF_LIB "{{printf_lib}}" CACHE STRING "") +set(MBED_OUTPUT_EXT "{{OUTPUT_EXT}}" CACHE STRING "") +set(MBED_GREENTEA_TEST_RESET_TIMEOUT "{{forced_reset_timeout}}" CACHE STRING "") + +list(APPEND MBED_TARGET_SUPPORTED_C_LIBS {% for supported_c_lib in supported_c_libs %} + {{supported_c_lib}} +{%- endfor %} +) + +list(APPEND MBED_TARGET_SUPPORTED_APPLICATION_PROFILES {% for supported_application_profile in supported_application_profiles %} + {{supported_application_profile}} +{%- endfor %} +) + +list(APPEND MBED_TARGET_LABELS{% for label in labels %} + {{label}} +{%- endfor %} +{% for extra_label in extra_labels %} + {{extra_label}} +{%- endfor %} +{% for component in components %} + {{component}} +{%- endfor %} +{% for feature in features %} + {{feature}} +{%- endfor %} +) + +# target +set(MBED_TARGET_DEFINITIONS{% for component in components %} + COMPONENT_{{component}}=1 +{%- endfor %} +{% for feature in features %} + FEATURE_{{feature}}=1 +{%- endfor %} +{% for device in device_has %} + DEVICE_{{device}}=1 +{%- endfor %} +{% for label in labels %} + TARGET_{{label}} +{%- endfor %} +{% for extra_label in extra_labels %} + TARGET_{{extra_label}} +{%- endfor %} +{% for form_factor in supported_form_factors %} + TARGET_FF_{{form_factor}} +{%- endfor %} +{% if mbed_rom_start is defined %} + MBED_ROM_START={{ mbed_rom_start | to_hex }} +{%- endif %} +{% if mbed_rom_size is defined %} + MBED_ROM_SIZE={{ mbed_rom_size | to_hex }} +{%- endif %} +{% if mbed_ram_start is defined %} + MBED_RAM_START={{ mbed_ram_start | to_hex }} +{%- endif %} +{% if mbed_ram_size is defined %} + MBED_RAM_SIZE={{ mbed_ram_size | to_hex }} +{%- endif %} + TARGET_LIKE_MBED + __MBED__=1 +) + +# config +set(MBED_CONFIG_DEFINITIONS +{% for setting in config %} + {%- if setting.macro_name -%} + {%- set setting_name = setting.macro_name -%} + {%- else -%} + {%- set setting_name = "MBED_CONF_{}_{}".format(setting.namespace.upper()|replace('-', '_'), setting.name.upper()|replace('-', '_')) -%} + {%- endif -%} + {%- if setting.value is sameas true or setting.value is sameas false -%} + {% set value = setting.value|int %} + {%- else -%} + {% set value = setting.value|replace("\"", "\\\"") -%} + {%- endif -%} + {%- if setting.value is not none -%} + "{{setting_name}}={{value}}" + {% endif -%} +{%- endfor -%} +{% for macro in macros %} + "{{macro|replace("\"", "\\\"")}}" +{%- endfor %} +) diff --git a/tools/python/mbed_tools/build/_internal/write_files.py b/tools/python/mbed_tools/build/_internal/write_files.py new file mode 100644 index 0000000000..c1e063fd0b --- /dev/null +++ b/tools/python/mbed_tools/build/_internal/write_files.py @@ -0,0 +1,27 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Writes out files to specified locations.""" +import pathlib +from mbed_tools.build.exceptions import InvalidExportOutputDirectory + + +def write_file(file_path: pathlib.Path, file_contents: str) -> None: + """Writes out a string to a file. + + If the intermediate directories to the output directory don't exist, + this function will create them. + + This function will overwrite any existing file of the same name in the + output directory. + + Raises: + InvalidExportOutputDirectory: it's not possible to export to the output directory provided + """ + output_directory = file_path.parent + if output_directory.is_file(): + raise InvalidExportOutputDirectory("Output directory cannot be a path to a file.") + + output_directory.mkdir(parents=True, exist_ok=True) + file_path.write_text(file_contents) diff --git a/tools/python/mbed_tools/build/build.py b/tools/python/mbed_tools/build/build.py new file mode 100644 index 0000000000..3fe17edae5 --- /dev/null +++ b/tools/python/mbed_tools/build/build.py @@ -0,0 +1,58 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Configure and build a CMake project.""" +import logging +import pathlib +import subprocess + +from typing import Optional + +from mbed_tools.build.exceptions import MbedBuildError + + +logger = logging.getLogger(__name__) + + +def build_project(build_dir: pathlib.Path, target: Optional[str] = None) -> None: + """Build a project using CMake to invoke Ninja. + + Args: + build_dir: Path to the CMake build tree. + target: The CMake target to build (e.g 'install') + """ + _check_ninja_found() + target_flag = ["--target", target] if target is not None else [] + _cmake_wrapper("--build", str(build_dir), *target_flag) + + +def generate_build_system(source_dir: pathlib.Path, build_dir: pathlib.Path, profile: str) -> None: + """Configure a project using CMake. + + Args: + source_dir: Path to the CMake source tree. + build_dir: Path to the CMake build tree. + profile: The Mbed build profile (develop, debug or release). + """ + _check_ninja_found() + _cmake_wrapper("-S", str(source_dir), "-B", str(build_dir), "-GNinja", f"-DCMAKE_BUILD_TYPE={profile}") + + +def _cmake_wrapper(*cmake_args: str) -> None: + try: + logger.debug("Running CMake with args: %s", cmake_args) + subprocess.run(["cmake", *cmake_args], check=True) + except FileNotFoundError: + raise MbedBuildError("Could not find CMake. Please ensure CMake is installed and added to PATH.") + except subprocess.CalledProcessError: + raise MbedBuildError("CMake invocation failed!") + + +def _check_ninja_found() -> None: + try: + subprocess.run(["ninja", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except FileNotFoundError: + raise MbedBuildError( + "Could not find the 'Ninja' build program. Please ensure 'Ninja' is installed and added to PATH." + ) diff --git a/tools/python/mbed_tools/build/config.py b/tools/python/mbed_tools/build/config.py new file mode 100644 index 0000000000..e40856410d --- /dev/null +++ b/tools/python/mbed_tools/build/config.py @@ -0,0 +1,63 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Parses the Mbed configuration system and generates a CMake config script.""" +import pathlib + +from typing import Any, Tuple + +from mbed_tools.lib.json_helpers import decode_json_file +from mbed_tools.project import MbedProgram +from mbed_tools.targets import get_target_by_name +from mbed_tools.build._internal.cmake_file import render_mbed_config_cmake_template +from mbed_tools.build._internal.config.assemble_build_config import Config, assemble_config +from mbed_tools.build._internal.write_files import write_file +from mbed_tools.build.exceptions import MbedBuildError + +CMAKE_CONFIG_FILE = "mbed_config.cmake" +MBEDIGNORE_FILE = ".mbedignore" + + +def generate_config(target_name: str, toolchain: str, program: MbedProgram) -> Tuple[Config, pathlib.Path]: + """Generate an Mbed config file after parsing the Mbed config system. + + Args: + target_name: Name of the target to configure for. + toolchain: Name of the toolchain to use. + program: The MbedProgram to configure. + + Returns: + Config object (UserDict). + Path to the generated config file. + """ + targets_data = _load_raw_targets_data(program) + target_build_attributes = get_target_by_name(target_name, targets_data) + config = assemble_config( + target_build_attributes, [program.root, program.mbed_os.root], program.files.app_config_file + ) + cmake_file_contents = render_mbed_config_cmake_template( + target_name=target_name, config=config, toolchain_name=toolchain, + ) + cmake_config_file_path = program.files.cmake_build_dir / CMAKE_CONFIG_FILE + write_file(cmake_config_file_path, cmake_file_contents) + mbedignore_path = program.files.cmake_build_dir / MBEDIGNORE_FILE + write_file(mbedignore_path, "*") + return config, cmake_config_file_path + + +def _load_raw_targets_data(program: MbedProgram) -> Any: + targets_data = decode_json_file(program.mbed_os.targets_json_file) + if program.files.custom_targets_json.exists(): + custom_targets_data = decode_json_file(program.files.custom_targets_json) + for custom_target in custom_targets_data: + if custom_target in targets_data: + raise MbedBuildError( + f"Error found in {program.files.custom_targets_json}.\n" + f"A target with the name '{custom_target}' already exists in targets.json. " + "Please give your custom target a unique name so it can be identified." + ) + + targets_data.update(custom_targets_data) + + return targets_data diff --git a/tools/python/mbed_tools/build/exceptions.py b/tools/python/mbed_tools/build/exceptions.py new file mode 100644 index 0000000000..a99be0cd51 --- /dev/null +++ b/tools/python/mbed_tools/build/exceptions.py @@ -0,0 +1,26 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Public exceptions raised by the package.""" +from mbed_tools.lib.exceptions import ToolsError + + +class MbedBuildError(ToolsError): + """Base public exception for the mbed-build package.""" + + +class InvalidExportOutputDirectory(MbedBuildError): + """It is not possible to export to the provided output directory.""" + + +class BinaryFileNotFoundError(MbedBuildError): + """The binary file (.bin/.hex) cannot be found in cmake_build directory.""" + + +class DeviceNotFoundError(MbedBuildError): + """The requested device is not connected to your system.""" + + +class InvalidConfigOverride(MbedBuildError): + """A given config setting was invalid.""" diff --git a/tools/python/mbed_tools/build/flash.py b/tools/python/mbed_tools/build/flash.py new file mode 100644 index 0000000000..8ee3282aa9 --- /dev/null +++ b/tools/python/mbed_tools/build/flash.py @@ -0,0 +1,64 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Flash binary onto the connected device.""" + +import shutil +import os +import pathlib +import platform + +from mbed_tools.build.exceptions import BinaryFileNotFoundError + + +def _flash_dev(disk: pathlib.Path, image_path: pathlib.Path) -> None: + """Flash device using copy method. + + Args: + disk: Device mount point. + image_path: Image file to be copied to device. + """ + shutil.copy(image_path, disk, follow_symlinks=False) + if not platform.system() == "Windows": + os.sync() + + +def _build_binary_file_path(program_path: pathlib.Path, build_dir: pathlib.Path, hex_file: bool) -> pathlib.Path: + """Build binary file name. + + Args: + program_path: Path to the Mbed project. + build_dir: Path to the CMake build folder. + hex_file: Use hex file. + + Returns: + Path to binary file. + + Raises: + BinaryFileNotFoundError: binary file not found in the path specified + """ + fw_fbase = build_dir / program_path.name + fw_file = fw_fbase.with_suffix(".hex" if hex_file else ".bin") + if not fw_file.exists(): + raise BinaryFileNotFoundError(f"Build program file (firmware) not found {fw_file}") + return fw_file + + +def flash_binary( + mount_point: pathlib.Path, program_path: pathlib.Path, build_dir: pathlib.Path, mbed_target: str, hex_file: bool +) -> pathlib.Path: + """Flash binary onto a device. + + Look through the connected devices and flash the binary if the connected and built target matches. + + Args: + mount_point: Mount point of the target device. + program_path: Path to the Mbed project. + build_dir: Path to the CMake build folder. + mbed_target: The name of the Mbed target to build for. + hex_file: Use hex file. + """ + fw_file = _build_binary_file_path(program_path, build_dir, hex_file) + _flash_dev(mount_point, fw_file) + return fw_file diff --git a/tools/python/mbed_tools/cli/__init__.py b/tools/python/mbed_tools/cli/__init__.py new file mode 100644 index 0000000000..312a2424a2 --- /dev/null +++ b/tools/python/mbed_tools/cli/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""mbed_tools command line interface.""" diff --git a/tools/python/mbed_tools/cli/build.py b/tools/python/mbed_tools/cli/build.py new file mode 100644 index 0000000000..60d2e06db7 --- /dev/null +++ b/tools/python/mbed_tools/cli/build.py @@ -0,0 +1,140 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Command to build/compile an Mbed project using CMake.""" +import os +import pathlib +import shutil + +from typing import Optional, Tuple + +import click + +from mbed_tools.build import build_project, generate_build_system, generate_config, flash_binary +from mbed_tools.devices import find_connected_device, find_all_connected_devices +from mbed_tools.project import MbedProgram +from mbed_tools.sterm import terminal + + +@click.command(name="compile", help="Build an Mbed project.") +@click.option( + "-t", + "--toolchain", + type=click.Choice(["ARM", "GCC_ARM"], case_sensitive=False), + required=True, + help="The toolchain you are using to build your app.", +) +@click.option("-m", "--mbed-target", required=True, help="A build target for an Mbed-enabled device, e.g. K64F.") +@click.option("-b", "--profile", default="develop", help="The build type (release, develop or debug).") +@click.option("-c", "--clean", is_flag=True, default=False, help="Perform a clean build.") +@click.option( + "-p", + "--program-path", + default=os.getcwd(), + help="Path to local Mbed program. By default it is the current working directory.", +) +@click.option( + "--mbed-os-path", type=click.Path(), default=None, help="Path to local Mbed OS directory.", +) +@click.option( + "--custom-targets-json", type=click.Path(), default=None, help="Path to custom_targets.json.", +) +@click.option( + "--app-config", type=click.Path(), default=None, help="Path to application configuration file.", +) +@click.option( + "-f", "--flash", is_flag=True, default=False, help="Flash the binary onto a device", +) +@click.option( + "-s", "--sterm", is_flag=True, default=False, help="Launch a serial terminal to the device.", +) +@click.option( + "--baudrate", + default=9600, + show_default=True, + help="Change the serial baud rate (ignored unless --sterm is also given).", +) +def build( + program_path: str, + profile: str, + toolchain: str, + mbed_target: str, + clean: bool, + flash: bool, + sterm: bool, + baudrate: int, + mbed_os_path: str, + custom_targets_json: str, + app_config: str, +) -> None: + """Configure and build an Mbed project using CMake and Ninja. + + If the CMake configuration step has already been run previously (i.e a CMake build tree exists), then just try to + build the project immediately using Ninja. + + Args: + program_path: Path to the Mbed project. + mbed_os_path: The path to the local Mbed OS directory. + profile: The Mbed build profile (debug, develop or release). + custom_targets_json: Path to custom_targets.json. + toolchain: The toolchain to use for the build. + mbed_target: The name of the Mbed target to build for. + app_config: the path to the application configuration file + clean: Perform a clean build. + flash: Flash the binary onto a device. + sterm: Open a serial terminal to the connected target. + baudrate: Change the serial baud rate (ignored unless --sterm is also given). + """ + mbed_target, target_id = _get_target_id(mbed_target) + + cmake_build_subdir = pathlib.Path(mbed_target.upper(), profile.lower(), toolchain.upper()) + if mbed_os_path is None: + program = MbedProgram.from_existing(pathlib.Path(program_path), cmake_build_subdir) + else: + program = MbedProgram.from_existing(pathlib.Path(program_path), cmake_build_subdir, pathlib.Path(mbed_os_path)) + build_tree = program.files.cmake_build_dir + if clean and build_tree.exists(): + shutil.rmtree(build_tree) + + click.echo("Configuring project and generating build system...") + if custom_targets_json is not None: + program.files.custom_targets_json = pathlib.Path(custom_targets_json) + if app_config is not None: + program.files.app_config_file = pathlib.Path(app_config) + config, _ = generate_config(mbed_target.upper(), toolchain, program) + generate_build_system(program.root, build_tree, profile) + + click.echo("Building Mbed project...") + build_project(build_tree) + + if flash or sterm: + if target_id is not None or sterm: + devices = [find_connected_device(mbed_target, target_id)] + else: + devices = find_all_connected_devices(mbed_target) + + if flash: + for dev in devices: + hex_file = "OUTPUT_EXT" in config and config["OUTPUT_EXT"] == "hex" + flashed_path = flash_binary(dev.mount_points[0].resolve(), program.root, build_tree, mbed_target, hex_file) + click.echo(f"Copied {str(flashed_path.resolve())} to {len(devices)} device(s).") + + if sterm: + dev = devices[0] + if dev.serial_port is None: + raise click.ClickException( + f"The connected device {dev.mbed_board.board_name} does not have an associated serial port." + " Reconnect the device and try again." + ) + + terminal.run(dev.serial_port, baudrate) + + +def _get_target_id(target: str) -> Tuple[str, Optional[int]]: + if "[" in target: + target_name, target_id = target.replace("]", "").split("[", maxsplit=1) + if target_id.isdigit() and int(target_id) >= 0: + return (target_name, int(target_id)) + raise click.ClickException("When using the format mbed-target[ID], ID must be a positive integer or 0.") + return (target, None) diff --git a/tools/python/mbed_tools/cli/configure.py b/tools/python/mbed_tools/cli/configure.py new file mode 100644 index 0000000000..24b49d81c3 --- /dev/null +++ b/tools/python/mbed_tools/cli/configure.py @@ -0,0 +1,86 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Command to generate the application CMake configuration script used by the build/compile system.""" +import pathlib + +import click + +from mbed_tools.project import MbedProgram +from mbed_tools.build import generate_config + + +@click.command( + help="Generate an Mbed OS config CMake file and write it to a .mbedbuild folder in the program directory." +) +@click.option( + "--custom-targets-json", type=click.Path(), default=None, help="Path to custom_targets.json.", +) +@click.option( + "-t", + "--toolchain", + type=click.Choice(["ARM", "GCC_ARM"], case_sensitive=False), + required=True, + help="The toolchain you are using to build your app.", +) +@click.option("-m", "--mbed-target", required=True, help="A build target for an Mbed-enabled device, eg. K64F") +@click.option("-b", "--profile", default="develop", help="The build type (release, develop or debug).") +@click.option("-o", "--output-dir", type=click.Path(), default=None, help="Path to output directory.") +@click.option( + "-p", + "--program-path", + type=click.Path(), + default=".", + help="Path to local Mbed program. By default is the current working directory.", +) +@click.option( + "--mbed-os-path", type=click.Path(), default=None, help="Path to local Mbed OS directory.", +) +@click.option( + "--app-config", type=click.Path(), default=None, help="Path to application configuration file.", +) +def configure( + toolchain: str, + mbed_target: str, + profile: str, + program_path: str, + mbed_os_path: str, + output_dir: str, + custom_targets_json: str, + app_config: str +) -> None: + """Exports a mbed_config.cmake file to build directory in the program root. + + The parameters set in the CMake file will be dependent on the combination of + toolchain and Mbed target provided and these can then control which parts of + Mbed OS are included in the build. + + This command will create the .mbedbuild directory at the program root if it doesn't + exist. + + Args: + custom_targets_json: the path to custom_targets.json + toolchain: the toolchain you are using (eg. GCC_ARM, ARM) + mbed_target: the target you are building for (eg. K64F) + profile: The Mbed build profile (debug, develop or release). + program_path: the path to the local Mbed program + mbed_os_path: the path to the local Mbed OS directory + output_dir: the path to the output directory + app_config: the path to the application configuration file + """ + cmake_build_subdir = pathlib.Path(mbed_target.upper(), profile.lower(), toolchain.upper()) + if mbed_os_path is None: + program = MbedProgram.from_existing(pathlib.Path(program_path), cmake_build_subdir) + else: + program = MbedProgram.from_existing(pathlib.Path(program_path), cmake_build_subdir, pathlib.Path(mbed_os_path)) + if custom_targets_json is not None: + program.files.custom_targets_json = pathlib.Path(custom_targets_json) + if output_dir is not None: + program.files.cmake_build_dir = pathlib.Path(output_dir) + if app_config is not None: + program.files.app_config_file = pathlib.Path(app_config) + + mbed_target = mbed_target.upper() + _, output_path = generate_config(mbed_target, toolchain, program) + click.echo(f"mbed_config.cmake has been generated and written to '{str(output_path.resolve())}'") diff --git a/tools/python/mbed_tools/cli/list_connected_devices.py b/tools/python/mbed_tools/cli/list_connected_devices.py new file mode 100644 index 0000000000..1e74d51ea6 --- /dev/null +++ b/tools/python/mbed_tools/cli/list_connected_devices.py @@ -0,0 +1,113 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Command to list all Mbed enabled devices connected to the host computer.""" +import click +import json +from operator import attrgetter +from typing import Iterable, List, Optional, Tuple +from tabulate import tabulate + +from mbed_tools.devices import get_connected_devices, Device +from mbed_tools.targets import Board + + +@click.command() +@click.option( + "--format", type=click.Choice(["table", "json"]), default="table", show_default=True, help="Set output format." +) +@click.option( + "--show-all", + "-a", + is_flag=True, + default=False, + help="Show all connected devices, even those which are not Mbed Boards.", +) +def list_connected_devices(format: str, show_all: bool) -> None: + """Prints connected devices.""" + connected_devices = get_connected_devices() + + if show_all: + devices = _sort_devices(connected_devices.identified_devices + connected_devices.unidentified_devices) + else: + devices = _sort_devices(connected_devices.identified_devices) + + output_builders = { + "table": _build_tabular_output, + "json": _build_json_output, + } + if devices: + output = output_builders[format](devices) + click.echo(output) + else: + click.echo("No connected Mbed devices found.") + + +def _sort_devices(devices: Iterable[Device]) -> Iterable[Device]: + """Sort devices by board name and then serial number (in case there are multiple boards with the same name).""" + return sorted(devices, key=attrgetter("mbed_board.board_name", "serial_number")) + + +def _get_devices_ids(devices: Iterable[Device]) -> List[Tuple[Optional[int], Device]]: + """Create tuple of ID and Device for each Device. ID is None when only one Device exists with a given board name.""" + devices_ids: List[Tuple[Optional[int], Device]] = [] + n = 0 + for device in devices: + board_name = device.mbed_board.board_name + if len([dev for dev in devices if dev.mbed_board.board_name == board_name]) > 1: + devices_ids.append((n, device)) + n += 1 + else: + devices_ids.append((None, device)) + n = 0 + return devices_ids + + +def _build_tabular_output(devices: Iterable[Device]) -> str: + headers = ["Board name", "Serial number", "Serial port", "Mount point(s)", "Build target(s)", "Interface Version"] + devices_data = [] + for id, device in _get_devices_ids(devices): + devices_data.append( + [ + device.mbed_board.board_name or "", + device.serial_number, + device.serial_port or "", + "\n".join(str(mount_point) for mount_point in device.mount_points), + "\n".join(_get_build_targets(device.mbed_board, id)), + device.interface_version, + ] + ) + return tabulate(devices_data, headers=headers, numalign="left") + + +def _build_json_output(devices: Iterable[Device]) -> str: + devices_data = [] + for id, device in _get_devices_ids(devices): + board = device.mbed_board + devices_data.append( + { + "serial_number": device.serial_number, + "serial_port": device.serial_port, + "mount_points": [str(m) for m in device.mount_points], + "interface_version": device.interface_version, + "mbed_board": { + "product_code": board.product_code, + "board_type": board.board_type, + "board_name": board.board_name, + "mbed_os_support": board.mbed_os_support, + "mbed_enabled": board.mbed_enabled, + "build_targets": _get_build_targets(board, id), + }, + } + ) + return json.dumps(devices_data, indent=4) + + +def _get_build_targets(board: Board, identifier: Optional[int]) -> List[str]: + if identifier is not None: + return [f"{board.board_type}_{variant}[{identifier}]" for variant in board.build_variant] + [ + f"{board.board_type}[{identifier}]" + ] + else: + return [f"{board.board_type}_{variant}" for variant in board.build_variant] + [board.board_type] diff --git a/tools/python/mbed_tools/cli/main.py b/tools/python/mbed_tools/cli/main.py new file mode 100644 index 0000000000..b34cda519b --- /dev/null +++ b/tools/python/mbed_tools/cli/main.py @@ -0,0 +1,84 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Main cli entry point.""" +import logging +import sys + +from typing import Union, Any + +import click + +from mbed_tools.lib.logging import set_log_level, MbedToolsHandler + +from mbed_tools.cli.configure import configure +from mbed_tools.cli.list_connected_devices import list_connected_devices +from mbed_tools.cli.project_management import new, import_, deploy +from mbed_tools.cli.build import build +from mbed_tools.cli.sterm import sterm + +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) +LOGGER = logging.getLogger(__name__) + + +class GroupWithExceptionHandling(click.Group): + """A click.Group which handles ToolsErrors and logging.""" + + def invoke(self, context: click.Context) -> None: + """Invoke the command group. + + Args: + context: The current click context. + """ + # Use the context manager to ensure tools exceptions (expected behaviour) are shown as messages to the user, + # but all other exceptions (unexpected behaviour) are shown as errors. + with MbedToolsHandler(LOGGER, context.params["traceback"]) as handler: + super().invoke(context) + + sys.exit(handler.exit_code) + + +def print_version(context: click.Context, param: Union[click.Option, click.Parameter], value: bool) -> Any: + """Print the version of mbed-tools.""" + if not value or context.resilient_parsing: + return + + # Mbed CE: changed this to be hardcoded for now. + version_string = "7.60.0" + click.echo(version_string) + context.exit() + + +@click.group(cls=GroupWithExceptionHandling, context_settings=CONTEXT_SETTINGS) +@click.option( + "--version", + is_flag=True, + callback=print_version, + expose_value=False, + is_eager=True, + help="Display versions of all Mbed Tools packages.", +) +@click.option( + "-v", + "--verbose", + default=0, + count=True, + help="Set the verbosity level, enter multiple times to increase verbosity.", +) +@click.option("-t", "--traceback", is_flag=True, show_default=True, help="Show a traceback when an error is raised.") +def cli(verbose: int, traceback: bool) -> None: + """Command line tool for interacting with Mbed OS.""" + set_log_level(verbose) + + +cli.add_command(configure, "configure") +cli.add_command(list_connected_devices, "detect") +cli.add_command(new, "new") +cli.add_command(deploy, "deploy") +cli.add_command(import_, "import") +cli.add_command(build, "compile") +cli.add_command(sterm, "sterm") + +if __name__ == '__main__': + cli() diff --git a/tools/python/mbed_tools/cli/project_management.py b/tools/python/mbed_tools/cli/project_management.py new file mode 100644 index 0000000000..a0b3b762fa --- /dev/null +++ b/tools/python/mbed_tools/cli/project_management.py @@ -0,0 +1,104 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Project management commands: new, import_, deploy and libs.""" +import os +import pathlib + +from typing import Any, List + +import click +import tabulate + +from mbed_tools.project import initialise_project, import_project, get_known_libs, deploy_project +from mbed_tools.project._internal import git_utils + + +@click.command() +@click.option("--create-only", "-c", is_flag=True, show_default=True, help="Create a program without fetching mbed-os.") +@click.argument("path", type=click.Path(resolve_path=True)) +def new(path: str, create_only: bool) -> None: + """Creates a new Mbed project at the specified path. Downloads mbed-os and adds it to the project. + + PATH: Path to the destination directory for the project. Will be created if it does not exist. + """ + click.echo(f"Creating a new Mbed program at path '{path}'.") + if not create_only: + click.echo("Downloading mbed-os and adding it to the project.") + + initialise_project(pathlib.Path(path), create_only) + + +@click.command() +@click.argument("url") +@click.argument("path", type=click.Path(), default="") +@click.option( + "--skip-resolve-libs", + "-s", + is_flag=True, + show_default=True, + help="Skip resolving program library dependencies after cloning.", +) +def import_(url: str, path: Any, skip_resolve_libs: bool) -> None: + """Clone an Mbed project and library dependencies. + + URL: The git url of the remote project to clone. + + PATH: Destination path for the clone. If not given the destination path is set to the project name in the cwd. + """ + click.echo(f"Cloning Mbed program '{url}'") + if not skip_resolve_libs: + click.echo("Resolving program library dependencies.") + + if path: + click.echo(f"Destination path is '{path}'") + path = pathlib.Path(path) + + dst_path = import_project(url, path, not skip_resolve_libs) + if not skip_resolve_libs: + libs = get_known_libs(dst_path) + _print_dependency_table(libs) + + +@click.command() +@click.argument("path", type=click.Path(), default=os.getcwd()) +@click.option( + "--force", + "-f", + is_flag=True, + show_default=True, + help="Forces checkout of all library repositories at specified commit in the .lib file, overwrites local changes.", +) +def deploy(path: str, force: bool) -> None: + """Checks out Mbed program library dependencies at the revision specified in the ".lib" files. + + Ensures all dependencies are resolved and the versions are synchronised to the version specified in the library + reference. + + PATH: Path to the Mbed project [default: CWD] + """ + click.echo("Checking out all libraries to revisions specified in .lib files. Resolving any unresolved libraries.") + root_path = pathlib.Path(path) + deploy_project(root_path, force) + libs = get_known_libs(root_path) + _print_dependency_table(libs) + + +def _print_dependency_table(libs: List) -> None: + click.echo("The following library dependencies were fetched: \n") + table = [] + for lib in libs: + table.append( + [ + lib.reference_file.stem, + lib.get_git_reference().repo_url, + lib.source_code_path, + git_utils.get_default_branch(git_utils.get_repo(lib.source_code_path)) + if not lib.get_git_reference().ref + else lib.get_git_reference().ref, + ] + ) + + headers = ("Library Name", "Repository URL", "Path", "Git Reference") + click.echo(tabulate.tabulate(table, headers=headers)) diff --git a/tools/python/mbed_tools/cli/sterm.py b/tools/python/mbed_tools/cli/sterm.py new file mode 100644 index 0000000000..30ab31c54c --- /dev/null +++ b/tools/python/mbed_tools/cli/sterm.py @@ -0,0 +1,58 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Command to launch a serial terminal to a connected Mbed device.""" +from typing import Any, Optional + +import click + +from mbed_tools.cli.build import _get_target_id +from mbed_tools.devices import find_connected_device, get_connected_devices +from mbed_tools.devices.exceptions import MbedDevicesError +from mbed_tools.sterm import terminal + + +@click.command( + help="Open a serial terminal to a connected Mbed Enabled device, or connect to a user-specified COM port." +) +@click.option( + "-p", + "--port", + type=str, + help="Communication port. Default: auto-detect. Specifying this will also ignore the -m/--mbed-target option.", +) +@click.option("-b", "--baudrate", type=int, default=9600, show_default=True, help="Communication baudrate.") +@click.option( + "-e", + "--echo", + default="on", + show_default=True, + type=click.Choice(["on", "off"], case_sensitive=False), + help="Switch local echo on/off.", +) +@click.option("-m", "--mbed-target", type=str, help="Mbed target to detect. Example: K64F, NUCLEO_F401RE, NRF51822...") +def sterm(port: str, baudrate: int, echo: str, mbed_target: str) -> None: + """Launches a serial terminal to a connected device.""" + if port is None: + port = _find_target_serial_port_or_default(mbed_target) + + terminal.run(port, baudrate, echo=True if echo == "on" else False) + + +def _get_connected_mbed_devices() -> Any: + connected_devices = get_connected_devices() + if not connected_devices.identified_devices: + raise MbedDevicesError("No Mbed enabled devices found.") + + return connected_devices.identified_devices + + +def _find_target_serial_port_or_default(target: Optional[str]) -> Any: + if target is None: + # just return the first valid device found + device, *_ = _get_connected_mbed_devices() + else: + target_name, target_id = _get_target_id(target) + device = find_connected_device(target_name.upper(), target_id) + return device.serial_port diff --git a/tools/python/mbed_tools/devices/__init__.py b/tools/python/mbed_tools/devices/__init__.py new file mode 100644 index 0000000000..b0e573ffe0 --- /dev/null +++ b/tools/python/mbed_tools/devices/__init__.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""API to detect any Mbed OS devices connected to the host computer. + +It is expected that this package will be used by developers of Mbed OS tooling rather than by users of Mbed OS. +This package uses the https://github.com/ARMmbed/mbed-targets interface to identify valid Mbed Enabled Devices. +Please see the documentation for mbed-targets for information on configuration options. + +For the command line interface to the API see the package https://github.com/ARMmbed/mbed-tools +""" +from mbed_tools.devices.devices import ( + get_connected_devices, + find_connected_device, + find_all_connected_devices, +) +from mbed_tools.devices.device import Device +from mbed_tools.devices import exceptions diff --git a/tools/python/mbed_tools/devices/_internal/__init__.py b/tools/python/mbed_tools/devices/_internal/__init__.py new file mode 100644 index 0000000000..868c3a2950 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Code not to be accessed by external applications.""" diff --git a/tools/python/mbed_tools/devices/_internal/base_detector.py b/tools/python/mbed_tools/devices/_internal/base_detector.py new file mode 100644 index 0000000000..84d8882b89 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/base_detector.py @@ -0,0 +1,18 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Interface for device detectors.""" +from abc import ABC, abstractmethod +from typing import List + +from mbed_tools.devices._internal.candidate_device import CandidateDevice + + +class DeviceDetector(ABC): + """Object in charge of finding USB devices.""" + + @abstractmethod + def find_candidates(self) -> List[CandidateDevice]: + """Returns CandidateDevices.""" + pass diff --git a/tools/python/mbed_tools/devices/_internal/candidate_device.py b/tools/python/mbed_tools/devices/_internal/candidate_device.py new file mode 100644 index 0000000000..9246479f17 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/candidate_device.py @@ -0,0 +1,95 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines CandidateDevice model used for device detection.""" +from dataclasses import dataclass +from typing import Optional, Tuple, Any, Union, cast +from pathlib import Path + + +class CandidateDeviceError(ValueError): + """Base exception raised by a CandidateDevice.""" + + +class USBDescriptorError(CandidateDeviceError): + """USB descriptor field was not found.""" + + +class FilesystemMountpointError(CandidateDeviceError): + """Filesystem mount point was not found.""" + + +class DataField: + """CandidateDevice data attribute descriptor.""" + + def __set_name__(self, owner: object, name: str) -> None: + """Sets the descriptor name, this is called by magic in the owners.__new__ method.""" + self.name = name + + def __get__(self, instance: object, owner: object = None) -> Any: + """Get the attribute value from the instance.""" + return instance.__dict__.setdefault(self.name, None) + + +class USBDescriptorHex(DataField): + """USB descriptor field which cannot be set to an empty value, or an invalid hex value.""" + + def __set__(self, instance: object, value: Any) -> None: + """Prevent setting the descriptor to an empty or invalid hex value.""" + try: + instance.__dict__[self.name] = _format_hex(value) + except ValueError: + raise USBDescriptorError(f"{self.name} cannot be an empty and must be valid hex.") + + +class USBDescriptorString(DataField): + """USB descriptor field which cannot be set to an empty value.""" + + def __set__(self, instance: object, value: str) -> None: + """Prevent setting the descriptor to a non-string or empty value.""" + if not value or not isinstance(value, str): + raise USBDescriptorError(f"{self.name} cannot be an empty field and must be a string.") + + instance.__dict__[self.name] = value + + +class FilesystemMountpoints(DataField): + """Data descriptor which must be set to a non-empty list or tuple.""" + + def __set__(self, instance: object, value: Union[tuple, list]) -> None: + """Prevent setting the descriptor to a non-sequence or empty sequence value.""" + if not value or not isinstance(value, (list, tuple)): + raise FilesystemMountpointError(f"{self.name} must be set to a non-empty list or tuple.") + + instance.__dict__[self.name] = tuple(value) + + +@dataclass(frozen=True, order=True) +class CandidateDevice: + """Valid candidate device connected to the host computer. + + We define a CandidateDevice as any USB mass storage device which mounts a filesystem. + The device may or may not present a serial port. + + Attributes: + product_id: USB device product ID. + vendor_id: USB device vendor ID. + serial_number: USB device serial number. + mount_points: Filesystem mount points associated with the device. + serial_port: Serial port associated with the device, this could be None. + """ + + product_id: str = cast(str, USBDescriptorHex()) + vendor_id: str = cast(str, USBDescriptorHex()) + serial_number: str = cast(str, USBDescriptorString()) + mount_points: Tuple[Path, ...] = cast(Tuple[Path], FilesystemMountpoints()) + serial_port: Optional[str] = None + + +def _format_hex(hex_value: str) -> str: + """Return hex value with a prefix. + + Accepts hex_value in prefixed (0xff) and unprefixed (ff) formats. + """ + return hex(int(hex_value, 16)) diff --git a/tools/python/mbed_tools/devices/_internal/darwin/__init__.py b/tools/python/mbed_tools/devices/_internal/darwin/__init__.py new file mode 100644 index 0000000000..7da3c781cd --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/darwin/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Darwin specific device detection.""" diff --git a/tools/python/mbed_tools/devices/_internal/darwin/device_detector.py b/tools/python/mbed_tools/devices/_internal/darwin/device_detector.py new file mode 100644 index 0000000000..3fbdf1bc6e --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/darwin/device_detector.py @@ -0,0 +1,130 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Device detector for Darwin.""" +import logging +import pathlib +import re +from typing import List, Tuple, Optional +from typing_extensions import TypedDict +from mbed_tools.devices._internal.base_detector import DeviceDetector +from mbed_tools.devices._internal.candidate_device import CandidateDevice +from mbed_tools.devices._internal.darwin import system_profiler, ioreg, diskutil + + +logger = logging.getLogger(__name__) + + +class CandidateDeviceData(TypedDict): + """CandidateDeviceData calculated from USBDevice.""" + + vendor_id: str + product_id: str + serial_number: str + mount_points: Tuple[pathlib.Path, ...] + serial_port: Optional[str] + + +class InvalidCandidateDeviceDataError(ValueError): + """Raised when CandidateDevice was given invalid data and it cannot be built.""" + + pass + + +class DarwinDeviceDetector(DeviceDetector): + """Darwin specific implementation of device detection.""" + + def find_candidates(self) -> List[CandidateDevice]: + """Return a list of CandidateDevices.""" + usb_devices_data = system_profiler.get_end_usb_devices_data() + candidates = [] + for device_data in usb_devices_data: + logging.debug(f"Building from: {device_data}.") + try: + candidate = _build_candidate(device_data) + except InvalidCandidateDeviceDataError: + pass + else: + logging.debug(f"Built candidate: {candidate}.") + candidates.append(candidate) + return candidates + + +def _build_candidate(device_data: system_profiler.USBDevice) -> CandidateDevice: + assembled_data = _assemble_candidate_data(device_data) + try: + return CandidateDevice(**assembled_data) + except ValueError as e: + logging.debug(f"Unable to build candidate. {e}") + raise InvalidCandidateDeviceDataError + + +def _assemble_candidate_data(device_data: system_profiler.USBDevice) -> CandidateDeviceData: + return { + "vendor_id": _format_vendor_id(device_data.get("vendor_id", "")), + "product_id": device_data.get("product_id", ""), + "serial_number": device_data.get("serial_num", ""), + "mount_points": _get_mount_points(device_data), + "serial_port": _get_serial_port(device_data), + } + + +def _format_vendor_id(vendor_id: str) -> str: + """Strips vendor name from vendor_id field. + + Example: + >>> _format_vendor_id("0x1234 (Nice Vendor Inc.)") # "0x1234" + """ + return vendor_id.split(maxsplit=1)[0] + + +def _get_mount_points(device_data: system_profiler.USBDevice) -> Tuple[pathlib.Path, ...]: + """Returns mount points for a given device, empty list if device has no mount points.""" + storage_identifiers = [media["bsd_name"] for media in device_data.get("Media", []) if "bsd_name" in media] + mount_points = [] + for storage_identifier in storage_identifiers: + mount_point = diskutil.get_mount_point(storage_identifier) + if mount_point: + mount_points.append(pathlib.Path(mount_point)) + else: + logging.debug(f"Couldn't determine mount point for device id: {storage_identifier}.") + return tuple(mount_points) + + +def _get_serial_port(device_data: system_profiler.USBDevice) -> Optional[str]: + """Returns serial port for a given device, None if serial port cannot be determined.""" + device_name = device_data.get("_name") + if not device_name: + logging.debug('Missing "_name" in "{device_data}", which is required for ioreg name.') + return None + + location_id = device_data.get("location_id") + if not location_id: + logging.debug('Missing "location_id" in "{device_data}", which is required for ioreg name.') + return None + + ioreg_name = _build_ioreg_device_name(device_name=device_name, location_id=location_id) + serial_port = ioreg.get_io_dialin_device(ioreg_name) + return serial_port + + +def _build_ioreg_device_name(device_name: str, location_id: str) -> str: + """Converts extracted `_name` and `location_id` attributes from `system_profiler` to a valid ioreg device name. + + `system_profiler` utility returns location ids in the form of `0xNNNNNNN`, with an optional suffix of ` / N`. + + Example: + >>> _build_ioreg_device_name("STM32 Foo", "0x123456 / 2") + "STM32 Foo@123456" + """ + pattern = r""" + 0x # hexadecimal prefix + (?P\d+) # location (i.e.: "123456" in "0x123456 / 2") + (\s\/\s\d+)? # suffix of location (" / 14") + """ + match = re.match(pattern, location_id, re.VERBOSE) + if match: + return f"{device_name}@{match['location']}" + else: + return device_name diff --git a/tools/python/mbed_tools/devices/_internal/darwin/diskutil.py b/tools/python/mbed_tools/devices/_internal/darwin/diskutil.py new file mode 100644 index 0000000000..e266570d73 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/darwin/diskutil.py @@ -0,0 +1,71 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Interactions with `diskutil`.""" +import plistlib +import subprocess +from typing import Dict, Iterable, List, Optional, cast +from typing_extensions import TypedDict + + +VolumeTree = Dict # mypy does not work with recursive types, which nested "Partitions" would require + + +class Volume(TypedDict, total=False): + """Representation of mounted volume.""" + + MountPoint: str # example: /Volumes/SomeName + DeviceIdentifier: str # example: disk2 + + +def get_all_external_disks_data() -> List[VolumeTree]: + """Returns parsed output of `diskutil` call, fetching only information of interest.""" + output = subprocess.check_output(["diskutil", "list", "-plist", "external"], stderr=subprocess.DEVNULL) + if output: + data: Dict = plistlib.loads(output) + return data.get("AllDisksAndPartitions", []) + return [] + + +def get_all_external_volumes_data() -> List[Volume]: + """Returns all external volumes data. + + Reduces structure returned by `diskutil` call to one which will only contain data about Volumes. + Useful for determining MountPoints and DeviceIdentifiers. + """ + data = get_all_external_disks_data() + return _filter_volumes(data) + + +def get_external_volume_data(device_identifier: str) -> Optional[Volume]: + """Returns external volume data for a given identifier.""" + data = get_all_external_volumes_data() + for device in data: + if device.get("DeviceIdentifier") == device_identifier: + return device + return None + + +def get_mount_point(device_identifier: str) -> Optional[str]: + """Returns mount point of a given device.""" + device_data = get_external_volume_data(device_identifier) + if device_data and "MountPoint" in device_data: + return device_data["MountPoint"] + return None + + +def _filter_volumes(data: Iterable[VolumeTree]) -> List[Volume]: + """Flattens the structure returned by `diskutil` call. + + Expected input will contain both partitioned an unpartitioned devices. + Partitioned devices list mounted partitions under an arbitrary key, + flattening the data helps finding actual end devices later on. + """ + devices = [] + for device in data: + if "Partitions" in device: + devices.extend(_filter_volumes(device["Partitions"])) + else: + devices.append(cast(Volume, device)) + return devices diff --git a/tools/python/mbed_tools/devices/_internal/darwin/ioreg.py b/tools/python/mbed_tools/devices/_internal/darwin/ioreg.py new file mode 100644 index 0000000000..afab905cdd --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/darwin/ioreg.py @@ -0,0 +1,41 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Interactions with `ioreg`.""" +import plistlib +import subprocess +from typing import Any, Dict, Iterable, List, Optional, cast +from xml.parsers.expat import ExpatError + + +def get_data(device_name: str) -> List[Dict]: + """Returns parsed output of `ioreg` call for a given device name.""" + output = subprocess.check_output(["ioreg", "-a", "-r", "-n", device_name, "-l"]) + if output: + try: + return cast(List[Dict], plistlib.loads(output)) + except ExpatError: + # Some devices seem to produce corrupt data + pass + return [] + + +def get_io_dialin_device(device_name: str) -> Optional[str]: + """Returns the value of "IODialinDevice" for a given device name.""" + ioreg_data = get_data(device_name) + dialin_device: Optional[str] = _find_first_property_value("IODialinDevice", ioreg_data) + return dialin_device + + +def _find_first_property_value(property_name: str, data: Iterable[Dict]) -> Any: + """Finds a first value of a given proprety name in data from `ioreg`, returns None if not found.""" + found_value = None + for item in data: + found_value = item.get( + property_name, + _find_first_property_value(property_name=property_name, data=item.get("IORegistryEntryChildren", [])), + ) + if found_value: + break + return found_value diff --git a/tools/python/mbed_tools/devices/_internal/darwin/system_profiler.py b/tools/python/mbed_tools/devices/_internal/darwin/system_profiler.py new file mode 100644 index 0000000000..a0b99ddf6f --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/darwin/system_profiler.py @@ -0,0 +1,77 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Interactions with `system_profiler`.""" +import plistlib +import re +import subprocess +from typing import Dict, Iterable, List, cast +from typing_extensions import TypedDict + +USBDeviceTree = Dict # mypy does not work with recursive types, which "_items" would require + + +class USBDeviceMedia(TypedDict, total=False): + """Representation of usb device storage.""" + + bsd_name: str + + +class USBDevice(TypedDict, total=False): + """Representation of usb device.""" + + _name: str + location_id: str + vendor_id: str + product_id: str + serial_num: str + Media: List[USBDeviceMedia] + + +def get_all_usb_devices_data() -> List[USBDeviceTree]: + """Returns parsed output of `system_profiler` call.""" + output = subprocess.check_output(["system_profiler", "-xml", "SPUSBDataType"], stderr=subprocess.DEVNULL) + if output: + return cast(List[USBDeviceTree], plistlib.loads(output)) + return [] + + +def get_end_usb_devices_data() -> List[USBDevice]: + """Returns only end devices from the output of `system_profiler` call.""" + data = get_all_usb_devices_data() + leaf_devices = _extract_leaf_devices(data) + end_devices = _filter_end_devices(leaf_devices) + return end_devices + + +def _extract_leaf_devices(data: Iterable[USBDeviceTree]) -> List[USBDevice]: + """Flattens the structure returned by `system_profiler` call. + + Expected input will contain a tree-like structures, this function will return their leaf nodes. + """ + end_devices = [] + for device in data: + if "_items" in device: + child_devices = _extract_leaf_devices(device["_items"]) + end_devices.extend(child_devices) + else: + end_devices.append(cast(USBDevice, device)) + return end_devices + + +def _filter_end_devices(data: Iterable[USBDevice]) -> List[USBDevice]: + """Removes devices that don't look like end devices. + + An end device is a device that shouldn't have child devices. + I.e.: a hub IS NOT an end device, a mouse IS an end device. + """ + return [device for device in data if not _is_hub(device) and not _is_bus(device)] + + +def _is_hub(data: USBDevice) -> bool: + return bool(re.match(r"USB\d.\d Hub", data.get("_name", ""))) + + +def _is_bus(data: USBDevice) -> bool: + return bool(re.match(r"USB\d\dBus", data.get("_name", ""))) diff --git a/tools/python/mbed_tools/devices/_internal/detect_candidate_devices.py b/tools/python/mbed_tools/devices/_internal/detect_candidate_devices.py new file mode 100644 index 0000000000..418cbc2b9b --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/detect_candidate_devices.py @@ -0,0 +1,38 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Detect Mbed devices connected to host computer.""" +import platform +from typing import Iterable + +from mbed_tools.devices._internal.candidate_device import CandidateDevice +from mbed_tools.devices._internal.base_detector import DeviceDetector +from mbed_tools.devices.exceptions import UnknownOSError + + +def detect_candidate_devices() -> Iterable[CandidateDevice]: + """Returns Candidates connected to host computer.""" + detector = _get_detector_for_current_os() + return detector.find_candidates() + + +def _get_detector_for_current_os() -> DeviceDetector: + """Returns DeviceDetector for current operating system.""" + if platform.system() == "Windows": + from mbed_tools.devices._internal.windows.device_detector import WindowsDeviceDetector + + return WindowsDeviceDetector() + if platform.system() == "Linux": + from mbed_tools.devices._internal.linux.device_detector import LinuxDeviceDetector + + return LinuxDeviceDetector() + if platform.system() == "Darwin": + from mbed_tools.devices._internal.darwin.device_detector import DarwinDeviceDetector + + return DarwinDeviceDetector() + + raise UnknownOSError( + f"We have detected the OS you are running is '{platform.system()}'. " + "Unfortunately we haven't implemented device detection support for this OS yet. Sorry!" + ) diff --git a/tools/python/mbed_tools/devices/_internal/exceptions.py b/tools/python/mbed_tools/devices/_internal/exceptions.py new file mode 100644 index 0000000000..b81daf66b5 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/exceptions.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Exceptions internal to the package.""" + +from mbed_tools.lib.exceptions import ToolsError + + +class SystemException(ToolsError): + """Exception with regards to the underlying operating system.""" + + +class NoBoardForCandidate(ToolsError): + """Raised when board data cannot be determined for a candidate.""" + + +class ResolveBoardError(ToolsError): + """There was an error resolving the board for a device.""" diff --git a/tools/python/mbed_tools/devices/_internal/file_parser.py b/tools/python/mbed_tools/devices/_internal/file_parser.py new file mode 100644 index 0000000000..b39622dc5d --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/file_parser.py @@ -0,0 +1,313 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Parses files found on Mbed enabled devices. + +There are a number of data files stored on an mbed enabled device's USB mass storage. + +The schema and content of these files are described in detail below. + +- MBED.HTM File +We support many flavours of MBED.HTM files. The list of examples below is not exhaustive. + + + + + + + mbed Website Shortcut + + + + +--- + + + + + + + mbed Website Shortcut + + + + + + +--- + + + + + # noqa + mbed Website Shortcut + + + + +--- + + + + + + + mbed Website Shortcut + + + + + + +--- + +- Segger.html File +This can be found on connected J-Link devices. An example is shown below. + + + + + + + +--- + +- Board.html File +This can be found on connected J-Link devices. An example is shown below. + + + + + NXP Product Page + + + + +""" +import logging +import pathlib +import re + +from dataclasses import dataclass +from typing import Optional, NamedTuple, Iterable, List + + +logger = logging.getLogger(__name__) + + +class OnlineId(NamedTuple): + """Used to identify the target against the os.mbed.com website. + + The target type and slug are used in the URI for the board and together they can be used uniquely identify a board. + + OnlineId(target_type="platform", slug="SOME-SLUG") -> https://os.mbed.com/platforms/SOME-SLUG + """ + + target_type: str + slug: str + + +@dataclass +class DeviceFileInfo: + """Information gathered from Mbed device files.""" + + product_code: Optional[str] + online_id: Optional[OnlineId] + interface_details: dict + + +def read_device_files(directory_paths: Iterable[pathlib.Path]) -> DeviceFileInfo: + """Read data from files contained on an mbed enabled device's USB mass storage device. + + If details.txt exists and it contains a product code, then we will use that code. If not then we try to grep the + code from the mbed.htm file. We extract an OnlineID from mbed.htm as we also make use of that information to find a + board entry in Mbed OS's various target databases and JSON files. + + Args: + directory_paths: Paths to the directories containing device files. + """ + device_file_paths = _get_device_file_paths(directory_paths) + if not device_file_paths: + paths = "\n".join(str(p) for p in directory_paths) + logger.warning( + f"No files were found in the device's mass storage device. The following paths were searched:\n{paths}." + "\nThis device may not be identifiable as Mbed enabled. Check the files exist, are not hidden and are not " + "corrupted." + ) + return DeviceFileInfo(None, None, {}) + + htm_file_contents = _read_htm_file_contents(device_file_paths) + details_txt_contents = _read_first_details_txt_contents(device_file_paths) + # details.txt is the "preferred" source of truth for the product_code + code = details_txt_contents.get("code") + if code is None: + # erk! well, let's get it from the mbed.htm file instead... + code = _extract_product_code_from_htm(htm_file_contents) + + online_id = _extract_online_id_from_htm(htm_file_contents) + if online_id is None: + # If no online ID available from .htm, may be a J-Link + online_id = _extract_online_id_jlink_html(device_file_paths) + details_txt_contents.update(_extract_version_jlink_html(device_file_paths)) + return DeviceFileInfo(code, online_id, details_txt_contents) + + +def _read_product_code(file_contents: str) -> Optional[str]: + """Returns product code parsed from the file contents, None if not found.""" + regex = r""" + (?:code|auth)= # attribute name + (?P[a-fA-F0-9]{4}) # product code + """ + match = re.search(regex, file_contents, re.VERBOSE) + if match: + return match["product_code"] + return None + + +def _read_online_id(file_contents: str) -> Optional[OnlineId]: + """Returns online id parsed from the files contents, None if not found.""" + regex = r""" + (?Pmodule|platform)s # module|platform + \/ # forward slash in the url + (?P[-\w]+) # permitted characters in a slug are letters and digits + """ + match = re.search(regex, file_contents, re.VERBOSE) + if match: + return OnlineId(target_type=match["target_type"], slug=match["slug"]) + return None + + +def _read_url_slug(file_contents: str) -> Optional[str]: + """Returns slug parsed from file contents, None if not found.""" + regex = r"""url=[^"]+\.[^"]+\/(?P[-\w]+)(\/|[\w.]+)?\"""" + match = re.search(regex, file_contents, re.VERBOSE) + if match: + return match["slug"] + return None + + +def _read_first_details_txt_contents(file_paths: Iterable[pathlib.Path]) -> dict: + for path in file_paths: + if _is_details_txt(path): + contents = _try_read_file_text(path) + if contents: + return _read_details_txt(contents) + + logger.warning(f"Could not find DETAILS.TXT in {file_paths!r}.") + return {} + + +def _read_details_txt(file_contents: str) -> dict: + """Parse the contents of a daplink-compatible device's details.txt. + + Args: + file_contents: The contents of the details.txt file. + """ + output = {} + for line in file_contents.splitlines(): + # Ignore any comments in the file contents + if line.startswith("#"): + continue + + key, sep, value = line.partition(":") + if key and value: + output[key.strip()] = value.strip() + + # Some forms of details.txt use Interface Version instead of Version as the key for the version number field + if "Interface Version" in output and "Version" not in output: + output["Version"] = output.pop("Interface Version") + + return output + + +def _extract_product_code_from_htm(all_files_contents: Iterable[str]) -> Optional[str]: + """Return first product code found in files contents, None if not found.""" + for contents in all_files_contents: + product_code = _read_product_code(contents) + if product_code: + return product_code + return None + + +def _extract_online_id_from_htm(all_files_contents: Iterable[str]) -> Optional[OnlineId]: + """Return first online ID found in files contents, None if not found.""" + for contents in all_files_contents: + online_id = _read_online_id(contents) + if online_id: + return online_id + return None + + +def _extract_online_id_jlink_html(file_paths: Iterable[pathlib.Path]) -> Optional[OnlineId]: + """Return online ID found in Board.html, None if not found.""" + contents = _get_board_html_contents(file_paths) + if contents is not None: + slug = _read_url_slug(contents) + if slug: + return OnlineId("jlink", slug) + return None + + +def _extract_version_jlink_html(file_paths: Iterable[pathlib.Path]) -> dict: + """Return dict with version found in Segger.html, empty if not found.""" + interface_data = {} + contents = _get_segger_html_content(file_paths) + if contents is not None: + segger_version = _read_url_slug(contents) + if segger_version: + interface_data["Version"] = segger_version + return interface_data + + +def _get_device_file_paths(directories: Iterable[pathlib.Path]) -> List[pathlib.Path]: + return [path for directory in directories for path in directory.iterdir() if not _is_hidden_file(path)] + + +def _try_read_file_text(file_path: pathlib.Path) -> Optional[str]: + try: + return file_path.read_text() + except OSError: + logger.warning(f"The file '{file_path}' could not be read from the device, target may not be identified.") + return None + + +def _read_htm_file_contents(all_files: Iterable[pathlib.Path]) -> List[str]: + htm_files_contents = [] + for file in all_files: + if _is_htm_file(file): + contents = _try_read_file_text(file) + if contents: + htm_files_contents.append(contents) + return htm_files_contents + + +def _get_segger_html_content(file_paths: Iterable[pathlib.Path]) -> Optional[str]: + for fp in file_paths: + if fp.name.lower() == "segger.html": + return _try_read_file_text(fp) + return None + + +def _get_board_html_contents(file_paths: Iterable[pathlib.Path]) -> Optional[str]: + for fp in file_paths: + if fp.name.lower() == "board.html": + return _try_read_file_text(fp) + return None + + +def _is_hidden_file(file: pathlib.Path) -> bool: + """Checks if the file is hidden.""" + return file.name.startswith(".") + + +def _is_htm_file(path: pathlib.Path) -> bool: + return path.suffix.lower() == ".htm" + + +def _is_details_txt(path: pathlib.Path) -> bool: + return path.name.lower() == "details.txt" diff --git a/tools/python/mbed_tools/devices/_internal/linux/__init__.py b/tools/python/mbed_tools/devices/_internal/linux/__init__.py new file mode 100644 index 0000000000..c4ae71eed7 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/linux/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Linux specific device detection.""" diff --git a/tools/python/mbed_tools/devices/_internal/linux/device_detector.py b/tools/python/mbed_tools/devices/_internal/linux/device_detector.py new file mode 100644 index 0000000000..628a5e20b1 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/linux/device_detector.py @@ -0,0 +1,60 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a device detector for Linux.""" +import logging +from pathlib import Path +from typing import Tuple, List, Optional, cast + +import psutil +import pyudev + +from mbed_tools.devices._internal.base_detector import DeviceDetector +from mbed_tools.devices._internal.candidate_device import CandidateDevice, FilesystemMountpointError + + +logger = logging.getLogger(__name__) + + +class LinuxDeviceDetector(DeviceDetector): + """Linux specific implementation of device detection.""" + + def find_candidates(self) -> List[CandidateDevice]: + """Return a list of CandidateDevices.""" + context = pyudev.Context() + candidates = [] + for disk in context.list_devices(subsystem="block", ID_BUS="usb"): + serial_number = disk.properties.get("ID_SERIAL_SHORT") + try: + candidates.append( + CandidateDevice( + mount_points=_find_fs_mounts_for_device(disk.properties.get("DEVNAME")), + product_id=disk.properties.get("ID_MODEL_ID"), + vendor_id=disk.properties.get("ID_VENDOR_ID"), + serial_number=serial_number, + serial_port=_find_serial_port_for_device(serial_number), + ) + ) + except FilesystemMountpointError: + logger.warning( + f"A USB block device was detected at path {disk.properties.get('DEVNAME')}. However, the" + " file system has failed to mount. Please disconnect and reconnect your device and try again." + "If this problem persists, try running fsck.vfat on your block device, as the file system may be " + "corrupted." + ) + continue + return candidates + + +def _find_serial_port_for_device(disk_serial_id: str) -> Optional[str]: + """Try to find a serial port associated with the given device.""" + for tty_dev in pyudev.Context().list_devices(subsystem="tty"): + if tty_dev.properties.get("ID_SERIAL_SHORT") == disk_serial_id: + return cast(str, tty_dev.properties.get("DEVNAME")) + return None + + +def _find_fs_mounts_for_device(device_file_path: str) -> Tuple[Path, ...]: + """Find the file system mount point for a block device file path.""" + return tuple(Path(part.mountpoint) for part in psutil.disk_partitions() if part.device == device_file_path) diff --git a/tools/python/mbed_tools/devices/_internal/resolve_board.py b/tools/python/mbed_tools/devices/_internal/resolve_board.py new file mode 100644 index 0000000000..5a8a12ce2d --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/resolve_board.py @@ -0,0 +1,91 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Resolve targets for `CandidateDevice`. + +Resolving a target involves looking up an `MbedTarget` from the `mbed-targets` API, using data found in the "htm file" +located on an "Mbed Enabled" device's USB MSD. + +For more information on the mbed-targets package visit https://github.com/ARMmbed/mbed-targets +""" +import logging + +from typing import Optional + +from mbed_tools.targets import ( + Board, + get_board_by_product_code, + get_board_by_online_id, + get_board_by_jlink_slug, +) +from mbed_tools.targets.exceptions import UnknownBoard, MbedTargetsError + +from mbed_tools.devices._internal.exceptions import NoBoardForCandidate, ResolveBoardError +from mbed_tools.devices._internal.file_parser import OnlineId + + +logger = logging.getLogger(__name__) + + +def resolve_board( + product_code: Optional[str] = None, online_id: Optional[OnlineId] = None, serial_number: str = "" +) -> Board: + """Resolves a board object from the platform database. + + We have multiple ways to identify boards from various metadata sources Mbed provides. This is because there are + many supported Mbed device families, each with slightly different ways of identifying themselves as Mbed enabled. + Because of this we need to try each input in turn, falling back to the next lookup method in the priority order if + the previous one was unsuccessful. + + The rules are as follows: + + 1. Use the product code from the mbed.htm file or details.txt if available + 2. Use online ID from the htm file or Board.html if available + 3. Try to use the first 4 chars of the USB serial number as the product code + """ + if product_code: + try: + return get_board_by_product_code(product_code) + except UnknownBoard: + logger.error(f"Could not identify a board with the product code: '{product_code}'.") + except MbedTargetsError as e: + logger.error( + f"There was an error looking up the product code `{product_code}` from the target database.\nError: {e}" + ) + raise ResolveBoardError() from e + + if online_id: + slug = online_id.slug + target_type = online_id.target_type + try: + if target_type == "jlink": + return get_board_by_jlink_slug(slug=slug) + else: + return get_board_by_online_id(slug=slug, target_type=target_type) + except UnknownBoard: + logger.error(f"Could not identify a board with the slug: '{slug}' and target type: '{target_type}'.") + except MbedTargetsError as e: + logger.error( + f"There was an error looking up the online ID `{online_id!r}` from the target database.\nError: {e}" + ) + raise ResolveBoardError() from e + + # Product code might be the first 4 characters of the serial number + product_code = serial_number[:4] + if product_code: + try: + return get_board_by_product_code(product_code) + except UnknownBoard: + # Most devices have a serial number so this may not be a problem + logger.info( + f"The device with the Serial Number: '{serial_number}' (Product Code: '{product_code}') " + f"does not appear to be an Mbed development board." + ) + except MbedTargetsError as e: + logger.error( + f"There was an error looking up the product code `{product_code}` from the target database.\nError: {e}" + ) + raise ResolveBoardError() from e + + raise NoBoardForCandidate diff --git a/tools/python/mbed_tools/devices/_internal/windows/__init__.py b/tools/python/mbed_tools/devices/_internal/windows/__init__.py new file mode 100644 index 0000000000..57e0af2d7e --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Windows specific device detection.""" diff --git a/tools/python/mbed_tools/devices/_internal/windows/component_descriptor.py b/tools/python/mbed_tools/devices/_internal/windows/component_descriptor.py new file mode 100644 index 0000000000..facd11da22 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/component_descriptor.py @@ -0,0 +1,152 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a generic Win32 component.""" +import logging +from abc import ABC, abstractmethod +from typing import List, Any, Generator, Optional, NamedTuple, cast + +import pythoncom +import win32com.client + +from mbed_tools.devices._internal.windows.component_descriptor_utils import ( + UNKNOWN_VALUE, + is_undefined_data_object, +) + +NAMED_TUPLE_FIELDS_ATTRIBUTE = "_fields" + +logger = logging.getLogger(__name__) + + +class ComponentDescriptor(ABC): + """Win32 component descriptor.""" + + def __init__(self, win32_definition: type, win32_class_name: str, win32_filter: Optional[str] = None): + """Initialiser. + + Args: + win32_definition: definition of the Windows component as defined in MSDN. + win32_class_name: Win32 class name of the component + win32_filter: Any extra filter to apply such as a subcategory of a Win32 class. + """ + self._win32_definition = win32_definition + self._win32_class_name = win32_class_name + self._win32_filter = win32_filter + + def set_data_values(self, fields_values: dict) -> None: + """Sets fields values based on what is defined in dictionary.""" + for k, v in fields_values.items(): + setattr(self, k, v) + + @property + def win32_definition(self) -> type: + """Gets descriptor definition.""" + return self._win32_definition + + @property + def win32_class_name(self) -> str: + """Returns the name of the Win32 Class.""" + return self._win32_class_name + + @property + def field_names(self) -> List[str]: + """Returns the names of all the fields of the descriptor.""" + return [k for k in getattr(self._win32_definition, NAMED_TUPLE_FIELDS_ATTRIBUTE)] + + @property + @abstractmethod + def component_id(self) -> str: + """Returns the device ID field.""" + + @property + def win32_filter(self) -> Optional[str]: + """Filter applied on a Win32 category. + + For instance, the current component can be a subclass/subcategory of a component exposed by Win32. + """ + return self._win32_filter + + def to_tuple(self) -> NamedTuple: + """Translates into named tuple.""" + return cast(NamedTuple, self.win32_definition(**{k: self.get(k) for k in self.field_names})) + + @property + def is_undefined(self) -> bool: + """Determines whether the structure is undefined or not i.e. none of the fields are actually defined.""" + return is_undefined_data_object(self.to_tuple()) + + def get(self, field_name: str) -> Any: + """Gets the field value.""" + try: + return getattr(self, field_name) + except AttributeError as e: + logger.debug(f"Attribute [{field_name}] is undefined on this instance {self}: {e}") + return UNKNOWN_VALUE + + def __str__(self) -> str: + """String representation.""" + values = {k: v for k, v in self.__dict__.items() if not k.startswith("_")} + return f"{self.__class__.__name__}({values})" + + +class Win32Wrapper: + """Wraps win32 objects and methods in order to simplify their use.""" + + def __init__(self) -> None: + """Wrapper initialisation.""" + # Setting pyWin32 so that it can be used across multiple threads + # See: + # https://stackoverflow.com/questions/37258257/why-does-this-script-not-work-with-threading-python + # https://gist.github.com/vlasenkov/f8fe40d5b2d9e43fd46ad8363067acce + # https://stackoverflow.com/questions/26764978/using-win32com-with-multithreading/27966218#27966218 + pythoncom.CoInitialize() + self.wmi = win32com.client.GetObject("winmgmts:") + + def _read_cdispatch_fields(self, win32_element: Any, element_fields_list: List[str]) -> dict: + """Reads all the fields from a cdispatch object returned by pywin32.""" + if not win32_element: + return dict() + return {k: self._read_cdispatch_field(win32_element, k) for k in element_fields_list} + + def _read_cdispatch_field(self, win32_element: Any, key: str) -> Any: + """Reads a specific field on a cdispatch object.""" + try: + return getattr(win32_element, key) + except AttributeError as e: + logger.debug(f"Failed getting an attribute value: {e}") + return UNKNOWN_VALUE + + def map_element(self, win32_element: Any, to_cls: type) -> ComponentDescriptor: + """Maps a win32 element into an element of the class `to_cls`.""" + instance = to_cls() + instance.set_data_values(self._read_cdispatch_fields(win32_element, instance.field_names)) + return cast(ComponentDescriptor, instance) + + def _get_list_iterator(self, win32_class_name: str, list_filter: Optional[str]) -> Generator[Any, None, None]: + if list_filter: + query = f"Select * from {win32_class_name} where {list_filter}" + return self.wmi.ExecQuery(query) # type: ignore + return self.wmi.InstancesOf(win32_class_name) # type: ignore + + def element_generator( + self, to_cls: type, win32_class_name: str, list_filter: Optional[str] + ) -> Generator["ComponentDescriptor", None, None]: + """Gets a generator over all elements currently registered in the system.""" + for element in self._get_list_iterator(win32_class_name, list_filter): + yield self.map_element(win32_element=element, to_cls=to_cls) + + +class ComponentDescriptorWrapper: + """Wraps a component descriptor.""" + + def __init__(self, cls: type): + """initialiser.""" + self._cls = cls + self._win32_wrapper = Win32Wrapper() + + def element_generator(self) -> Generator["ComponentDescriptor", None, None]: + """Gets a generator over all elements currently registered in the system.""" + instance = self._cls() + return self._win32_wrapper.element_generator(self._cls, instance.win32_class_name, instance.win32_filter) diff --git a/tools/python/mbed_tools/devices/_internal/windows/component_descriptor_utils.py b/tools/python/mbed_tools/devices/_internal/windows/component_descriptor_utils.py new file mode 100644 index 0000000000..20436d00c1 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/component_descriptor_utils.py @@ -0,0 +1,36 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Utilities with regards to Win32 component descriptors.""" +from typing import Any, NamedTuple, Union +from collections import OrderedDict + +UNKNOWN_VALUE = "Unknown" + + +def is_undefined_value(value: Any) -> bool: + """Checks whether a field value is considered as undefined or not.""" + return value in [None, UNKNOWN_VALUE, 0, False] + + +def is_undefined_data_object(data_object: NamedTuple) -> bool: + """Checks whether a data object is considered as undefined or not.""" + for f in data_object._fields: + if not is_undefined_value(getattr(data_object, f)): + is_undefined = False + break + else: + is_undefined = True + return is_undefined + + +def retain_value_or_default(value: Any) -> Any: + """Returns the value if set otherwise returns a default value.""" + return UNKNOWN_VALUE if is_undefined_value(value) else value + + +def data_object_to_dict(data_object: NamedTuple) -> dict: + """Gets a data object as a dictionary.""" + internal_dictionary: Union[dict, OrderedDict] = data_object._asdict() + return dict(internal_dictionary) if isinstance(internal_dictionary, OrderedDict) else internal_dictionary diff --git a/tools/python/mbed_tools/devices/_internal/windows/device_detector.py b/tools/python/mbed_tools/devices/_internal/windows/device_detector.py new file mode 100644 index 0000000000..338772db73 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/device_detector.py @@ -0,0 +1,46 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a device detector for Windows.""" +from pathlib import Path +from typing import List + +from mbed_tools.devices._internal.base_detector import DeviceDetector +from mbed_tools.devices._internal.candidate_device import CandidateDevice +from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader +from mbed_tools.devices._internal.windows.usb_data_aggregation import SystemUsbData, AggregatedUsbData + + +class WindowsDeviceDetector(DeviceDetector): + """Windows specific implementation of device detection.""" + + def __init__(self) -> None: + """Initialiser.""" + self._data_loader = SystemDataLoader() + + def find_candidates(self) -> List[CandidateDevice]: + """Return a generator of Candidates.""" + return [ + WindowsDeviceDetector.map_to_candidate(usb) + for usb in SystemUsbData(data_loader=self._data_loader).all() + if WindowsDeviceDetector.is_valid_candidate(usb) + ] + + @staticmethod + def map_to_candidate(usb_data: AggregatedUsbData) -> CandidateDevice: + """Maps a USB device to a candidate.""" + serial_port = next(iter(usb_data.get("serial_port")), None) + uid = usb_data.uid + return CandidateDevice( + product_id=uid.product_id, + vendor_id=uid.vendor_id, + mount_points=tuple(Path(disk.component_id) for disk in usb_data.get("disks")), + serial_number=uid.uid.presumed_serial_number, + serial_port=serial_port.port_name if serial_port else None, + ) + + @staticmethod + def is_valid_candidate(usb_data: AggregatedUsbData) -> bool: + """States whether the usb device is a valid candidate or not.""" + return usb_data.is_composite and usb_data.is_associated_with_disk diff --git a/tools/python/mbed_tools/devices/_internal/windows/device_instance_id.py b/tools/python/mbed_tools/devices/_internal/windows/device_instance_id.py new file mode 100644 index 0000000000..55beb5c839 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/device_instance_id.py @@ -0,0 +1,60 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Utility in charge of finding the instance ID of a device.""" +import win32con +import win32api +from mbed_tools.devices._internal.exceptions import SystemException +import logging + +from typing import Optional, Any + +logger = logging.getLogger(__name__) + + +class RegKey(object): + """Context manager in charge of opening and closing registry keys.""" + + def __init__(self, sub_registry_key: str) -> None: + """Initialiser.""" + access = win32con.KEY_READ | win32con.KEY_ENUMERATE_SUB_KEYS | win32con.KEY_QUERY_VALUE + try: + self._hkey = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, sub_registry_key, 0, access) + except win32api.error as e: + raise SystemException(f"Could not read key [{sub_registry_key}] in the registry: {e}") + + def __enter__(self) -> Any: + """Actions on entry.""" + return self._hkey + + def __exit__(self, type: Any, value: Any, traceback: Any) -> None: + """Actions on exit.""" + win32api.RegCloseKey(self._hkey) + self._hkey.close() + + +def get_children_instance_id(pnpid: str) -> Optional[str]: + """Gets the USB children instance ID from the plug and play ID. + + See https://docs.microsoft.com/en-us/windows-hardware/drivers/install/instance-ids. + """ + # Although the registry should not be accessed directly + # (See https://docs.microsoft.com/en-us/windows-hardware/drivers/install/hklm-system-currentcontrolset-enum-registry-tree), # noqa E501 + # and SetupDi functions/APIs should be used instead in a similar fashion to Miro utility + # (See https://github.com/cool-RR/Miro/blob/7b9ecd9bc0878e463f5a5e26e8b00b675e3f98ac/tv/windows/plat/usbutils.py) + # Most libraries seems to be reading the registry: + # - Pyserial: https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports_windows.py + # - Node serialport: https://github.com/serialport/node-serialport/blob/cd112ca5a3a3fe186e1ac6fa78eeeb5ea7396185/packages/bindings/src/serialport_win.cpp # noqa E501 + # - USB device forensics: https://github.com/woanware/usbdeviceforensics/blob/master/pyTskusbdeviceforensics.py + # For more details about the registry key actually looked at, See: + # - https://stackoverflow.com/questions/3331043/get-list-of-connected-usb-devices + # - https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/usb-device-specific-registry-settings + key_path = f"SYSTEM\\CurrentControlSet\\Enum\\{pnpid}" + with RegKey(key_path) as hkey: + try: + value = win32api.RegQueryValueEx(hkey, "ParentIdPrefix") + return str(value[0]) if value else None + except win32api.error as e: + logger.debug(f"Error occurred reading `ParentIdPrefix` field of key [{key_path}] in the registry: {e}") + return None diff --git a/tools/python/mbed_tools/devices/_internal/windows/disk_aggregation.py b/tools/python/mbed_tools/devices/_internal/windows/disk_aggregation.py new file mode 100644 index 0000000000..bbdb366d88 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/disk_aggregation.py @@ -0,0 +1,198 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Data aggregation about a disk on Windows. + +On Windows, information about disk drive is scattered around Physical disks, +Partitions and Logical Drives. +This file tries to reconcile all these pieces of information so that it is presented +as a single object: AggregatedDiskData. +""" +from typing import List, Optional, Callable +from typing import NamedTuple, cast + +from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor +from mbed_tools.devices._internal.windows.component_descriptor_utils import retain_value_or_default +from mbed_tools.devices._internal.windows.windows_identifier import WindowsUID +from mbed_tools.devices._internal.windows.disk_drive import DiskDrive +from mbed_tools.devices._internal.windows.disk_partition import DiskPartition +from mbed_tools.devices._internal.windows.disk_partition_logical_disk_relationships import ( + DiskPartitionLogicalDiskRelationship, +) +from mbed_tools.devices._internal.windows.logical_disk import LogicalDisk +from mbed_tools.devices._internal.windows.volume_set import VolumeInformation, get_volume_information +from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader, ComponentsLoader + + +class AggregatedDiskDataDefinition(NamedTuple): + """Data aggregation with regards to a disk.""" + + uid: WindowsUID # see WindowsUID + label: str # e.g. C: + description: str # e.g. Removal Disk + free_space: int + size: int + partition_name: str # e.g. Disk #0, Partition #2 + partition_type: str # 16-bit FAT + volume_information: VolumeInformation + caption: str # e.g. SAMSUNG MZNLN512HMJP-000L7 + physical_disk_name: str + model: str + interface_type: str # e.g. IDE + media_type: str # e.g. Fixed hard disk media + manufacturer: str + serial_number: str + status: str + pnp_device_id: str + + +class AggregatedDiskData(ComponentDescriptor): + """Disk information based on lots of different sources.""" + + def __init__(self) -> None: + """Initialiser.""" + super().__init__(AggregatedDiskDataDefinition, win32_class_name="DiskDataAggregation") + + @property + def component_id(self) -> str: + """Returns the ID field.""" + return cast(str, self.get("label")) + + +class DiskDataAggregator: + """Aggregator for any scattered data related to disks.""" + + def __init__( + self, + physical_disks: dict, + partition_disks: dict, + logical_partition_relationships: dict, + lookup_volume_information: Callable[[LogicalDisk], VolumeInformation], + ) -> None: + """Initialiser.""" + self._physical_disks = physical_disks + self._partition_disks = partition_disks + self._logical_partition_relationships = logical_partition_relationships + self._lookup_volume_information = lookup_volume_information + + def _get_corresponding_partition(self, logical_disk: LogicalDisk) -> DiskPartition: + """Determines the partition corresponding to a logical disk.""" + # Determines the partition ID + partition_id = self.logical_disk_partition_relationships.get(logical_disk.component_id) + return self._partition_disks.get(partition_id, DiskPartition()) if partition_id else DiskPartition() + + @property + def physical_disks(self) -> dict: + """Gets local cache of physical disks data.""" + return self._physical_disks + + @property + def partition_disks(self) -> dict: + """Gets local cache of disk partition data.""" + return self._partition_disks + + @property + def logical_disk_partition_relationships(self) -> dict: + """Gets local cache of relationships between logical disk and disk partitions.""" + return self._logical_partition_relationships + + def aggregate(self, logical_disk: LogicalDisk) -> AggregatedDiskData: + """Aggregates data about a disk from different sources.""" + corresponding_partition = self._get_corresponding_partition(logical_disk) + corresponding_volume_information = self._lookup_volume_information(logical_disk) + # Determines which physical disk the partition is on + # See https://superuser.com/questions/1147218/on-which-physical-drive-is-this-logical-drive + corresponding_physical = self._physical_disks.get(corresponding_partition.get("DiskIndex"), DiskDrive()) + aggregatedData = AggregatedDiskData() + aggregatedData.set_data_values( + dict( + uid=corresponding_physical.uid, + label=logical_disk.component_id, + description=logical_disk.get("Description"), + free_space=logical_disk.get("FreeSpace"), + size=logical_disk.get("Size"), + partition_name=corresponding_partition.component_id, + partition_type=corresponding_partition.get("Type"), + volume_information=corresponding_volume_information, + caption=corresponding_physical.get("Caption"), + physical_disk_name=corresponding_physical.get("DeviceID"), + model=corresponding_physical.get("Model"), + interface_type=corresponding_physical.get("InterfaceType"), + media_type=corresponding_physical.get("MediaType"), + manufacturer=corresponding_physical.get("Manufacturer"), + serial_number=retain_value_or_default(corresponding_physical.get("SerialNumber")), + status=corresponding_physical.get("Status"), + pnp_device_id=corresponding_physical.get("PNPDeviceID"), + ) + ) + return aggregatedData + + +class WindowsDiskDataAggregator(DiskDataAggregator): + """Disk Data aggregator for Windows.""" + + def __init__(self, data_loader: SystemDataLoader) -> None: + """Initialiser.""" + super().__init__( + physical_disks={ + d.Index: d # type: ignore + for d in ComponentsLoader(data_loader, DiskDrive).element_generator() + }, + partition_disks={ + p.component_id: p for p in ComponentsLoader(data_loader, DiskPartition).element_generator() + }, + logical_partition_relationships={ + r.logical_disk_id: r.disk_partition_id # type: ignore + for r in ComponentsLoader(data_loader, DiskPartitionLogicalDiskRelationship).element_generator() + }, + lookup_volume_information=lambda logical_disk: get_volume_information(logical_disk.component_id), + ) + + +class SystemDiskInformation: + """All information about disks on the current system.""" + + def __init__(self, data_loader: SystemDataLoader) -> None: + """Initialiser.""" + self._disk_data_by_serial_number: Optional[dict] = None + self._disk_data_by_label: Optional[dict] = None + self._data_loader = data_loader + + def _load_data(self) -> None: + aggregator = WindowsDiskDataAggregator(self._data_loader) + disk_data_by_serialnumber: dict = dict() # The type is enforced so that mypy is happy. + disk_data_by_label = dict() + for ld in ComponentsLoader(self._data_loader, LogicalDisk).element_generator(): + aggregation = aggregator.aggregate(cast(LogicalDisk, ld)) + key = aggregation.get("uid").presumed_serial_number + disk_data_list = disk_data_by_serialnumber.get(key, list()) + disk_data_list.append(aggregation) + disk_data_by_serialnumber[key] = disk_data_list + disk_data_by_label[aggregation.get("label")] = aggregation + self._disk_data_by_serial_number = disk_data_by_serialnumber + self._disk_data_by_label = disk_data_by_label + + @property + def disk_data_by_serial_number(self) -> dict: + """Gets system's disk data by serial number.""" + if not self._disk_data_by_serial_number: + self._load_data() + return self._disk_data_by_serial_number if self._disk_data_by_serial_number else dict() + + @property + def disk_data_by_label(self) -> dict: + """Gets system's disk data by label.""" + if not self._disk_data_by_label: + self._load_data() + return self._disk_data_by_label if self._disk_data_by_label else dict() + + def get_disk_information(self, uid: WindowsUID) -> List[AggregatedDiskData]: + """Gets all disk information for a given UID.""" + return self.disk_data_by_serial_number.get(uid.presumed_serial_number, list()) + + def get_disk_information_by_label(self, label: str) -> AggregatedDiskData: + """Gets all disk information for a given label.""" + return self.disk_data_by_label.get( + label.upper(), self.disk_data_by_label.get(label.lower(), AggregatedDiskData()) + ) diff --git a/tools/python/mbed_tools/devices/_internal/windows/disk_drive.py b/tools/python/mbed_tools/devices/_internal/windows/disk_drive.py new file mode 100644 index 0000000000..8f60395947 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/disk_drive.py @@ -0,0 +1,121 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a Disk drive.""" + +from typing import NamedTuple, cast, Optional, Tuple +import re + +from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor +from mbed_tools.devices._internal.windows.component_descriptor_utils import is_undefined_value, UNKNOWN_VALUE +from mbed_tools.devices._internal.windows.windows_identifier import WindowsUID + +PATTERN_UID = re.compile(r"[&#]?([0-9A-Za-z]{10,48})[&#]?") + + +class DiskDriveMsdnDefinition(NamedTuple): + """Msdn definition of a disk drive. + + See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-diskdrive + """ + + Availability: int + BytesPerSector: int + Capabilities: list + CapabilityDescriptions: list + Caption: str + CompressionMethod: str + ConfigManagerErrorCode: int + ConfigManagerUserConfig: bool + CreationClassName: str + DefaultBlockSize: int + Description: str + DeviceID: str + ErrorCleared: bool + ErrorDescription: str + ErrorMethodology: str + FirmwareRevision: str + Index: int + InstallDate: int + InterfaceType: str + LastErrorCode: int + Manufacturer: str + MaxBlockSize: int + MaxMediaSize: int + MediaLoaded: bool + MediaType: str + MinBlockSize: int + Model: str + Name: str + NeedsCleaning: bool + NumberOfMediaSupported: int + Partitions: int + PNPDeviceID: str + PowerManagementCapabilities: list + PowerManagementSupported: bool + SCSIBus: int + SCSILogicalUnit: int + SCSIPort: int + SCSITargetId: int + SectorsPerTrack: int + SerialNumber: str + Signature: int + Size: int + Status: str + StatusInfo: int + SystemCreationClassName: str + SystemName: str + TotalCylinders: int + TotalHeads: int + TotalSectors: int + TotalTracks: int + TracksPerCylinder: int + + +class DiskDrive(ComponentDescriptor): + """Disk Drive as defined in Windows API. + + See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-diskdrive + """ + + def __init__(self) -> None: + """Initialiser.""" + super().__init__(DiskDriveMsdnDefinition, win32_class_name="Win32_DiskDrive") + + @property + def component_id(self) -> str: + """Returns the device id field.""" + return cast(str, self.get("DeviceID")) + + @property + def uid(self) -> WindowsUID: + """Returns the disk UID.""" + return Win32DiskIdParser().parse(cast(str, self.get("PNPDeviceID")), self.get("SerialNumber")) + + +class Win32DiskIdParser: + """Parser of a standard Win32 Disk.""" + + def _parse_pnpid(self, pnpid: str) -> Tuple[str, str]: + if is_undefined_value(pnpid): + return (UNKNOWN_VALUE, UNKNOWN_VALUE) + parts = pnpid.split("\\") + if len(parts) >= 2: + raw_id = parts[-1] + match = PATTERN_UID.search(raw_id) + processed_uid = raw_id + if match: + processed_uid = match.group(1) + return (processed_uid.lower(), raw_id) + return (UNKNOWN_VALUE, UNKNOWN_VALUE) + + def parse(self, pnpid: str, serial_number: Optional[str]) -> WindowsUID: + """Parses the UID value based on multiple fields. + + For different boards, the ID is stored in different fields. + e.g. JLink serial number is irrelevant whereas it is the correct field for Daplink boards. + For others, the PNPDeviceID should be used instead. + """ + (uid, raw_uid) = self._parse_pnpid(pnpid) + return WindowsUID(uid=uid, raw_uid=raw_uid, serial_number=serial_number) diff --git a/tools/python/mbed_tools/devices/_internal/windows/disk_partition.py b/tools/python/mbed_tools/devices/_internal/windows/disk_partition.py new file mode 100644 index 0000000000..d71b6ed7c4 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/disk_partition.py @@ -0,0 +1,73 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a Disk partition.""" + +from typing import NamedTuple, cast + +from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor + + +class DiskPartitionMsdnDefinition(NamedTuple): + """Msdn definition of a disk partition. + + See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-diskpartition + """ + + AdditionalAvailability: int + Availability: int + PowerManagementCapabilities: list + IdentifyingDescriptions: list + MaxQuiesceTime: int + OtherIdentifyingInfo: int + StatusInfo: int + PowerOnHours: int + TotalPowerOnHours: int + Access: int + BlockSize: int + Bootable: bool + BootPartition: bool + Caption: str + ConfigManagerErrorCode: int + ConfigManagerUserConfig: bool + CreationClassName: str + Description: str + DeviceID: str + DiskIndex: int + ErrorCleared: bool + ErrorDescription: str + ErrorMethodology: str + HiddenSectors: int + Index: int + InstallDate: int + LastErrorCode: int + Name: str + NumberOfBlocks: int + PNPDeviceID: str + PowerManagementSupported: bool + PrimaryPartition: bool + Purpose: str + RewritePartition: bool + Size: int + StartingOffset: int + Status: str + SystemCreationClassName: str + SystemName: str + Type: str + + +class DiskPartition(ComponentDescriptor): + """Disk partition as defined in Windows API. + + See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-diskpartition + """ + + def __init__(self) -> None: + """Initialiser.""" + super().__init__(DiskPartitionMsdnDefinition, win32_class_name="Win32_DiskPartition") + + @property + def component_id(self) -> str: + """Returns the device id field.""" + return cast(str, self.get("DeviceID")) diff --git a/tools/python/mbed_tools/devices/_internal/windows/disk_partition_logical_disk_relationships.py b/tools/python/mbed_tools/devices/_internal/windows/disk_partition_logical_disk_relationships.py new file mode 100644 index 0000000000..d1efc8c328 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/disk_partition_logical_disk_relationships.py @@ -0,0 +1,52 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a Disk partition/Logical disk relationship.""" + +from typing import NamedTuple + +from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor + + +class DiskToPartitionMsdnDefinition(NamedTuple): + """Msdn definition of a disk partition - logical disk relationship. + + See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-logicaldisktopartition + """ + + EndingAddress: int + StartingAddress: int + Antecedent: str + Dependent: str + + +class DiskPartitionLogicalDiskRelationship(ComponentDescriptor): + """Disk partition as defined in Windows API. + + See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-logicaldisktopartition + """ + + def __init__(self) -> None: + """Initialiser.""" + super().__init__(DiskToPartitionMsdnDefinition, win32_class_name="Win32_LogicalDiskToPartition") + + @property + def component_id(self) -> str: + """Returns the device id field.""" + return f"{self.get('Antecedent')}->{self.get('Dependent')}" + + @staticmethod + def _parse_reference(ref: str) -> str: + """Parses a Win32 to only retain the value.""" + return ref.replace("'", "").replace('"', "").split("=")[1] + + @property + def logical_disk_id(self) -> str: + """Gets the logical disk id.""" + return DiskPartitionLogicalDiskRelationship._parse_reference(self.get("Dependent")) + + @property + def disk_partition_id(self) -> str: + """Gets the disk partition id.""" + return DiskPartitionLogicalDiskRelationship._parse_reference(self.get("Antecedent")) diff --git a/tools/python/mbed_tools/devices/_internal/windows/logical_disk.py b/tools/python/mbed_tools/devices/_internal/windows/logical_disk.py new file mode 100644 index 0000000000..7f9ba13847 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/logical_disk.py @@ -0,0 +1,59 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a Logical disk.""" + +from typing import NamedTuple, cast + +from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor + + +class LogicalDiskMsdnDefinition(NamedTuple): + """Msdn definition of a logical disk. + + See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-logicaldisk + """ + + Access: int + Availability: int + BlockSize: int + Caption: str + ConfigManagerErrorCode: int + ConfigManagerUserConfig: bool + CreationClassName: str + Description: str + DeviceID: str + ErrorCleared: bool + ErrorDescription: str + ErrorMethodology: str + FreeSpace: int + InstallDate: int + LastErrorCode: int + Name: str + NumberOfBlocks: int + PNPDeviceID: str + PowerManagementCapabilities: list + PowerManagementSupported: bool + Purpose: str + Size: int + Status: str + StatusInfo: int + SystemCreationClassName: str + SystemName: str + + +class LogicalDisk(ComponentDescriptor): + """Logical disk as defined in Windows API. + + See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-logicaldisk + """ + + def __init__(self) -> None: + """Initialiser.""" + super().__init__(LogicalDiskMsdnDefinition, win32_class_name="CIM_LogicalDisk") + + @property + def component_id(self) -> str: + """Returns the device id field.""" + return cast(str, self.get("DeviceID")) diff --git a/tools/python/mbed_tools/devices/_internal/windows/serial_port.py b/tools/python/mbed_tools/devices/_internal/windows/serial_port.py new file mode 100644 index 0000000000..2a77fe1421 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/serial_port.py @@ -0,0 +1,94 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a Serial Port. + +On Windows, Win32_SerialPort only represents physical serial Ports and hence, USB connections are not listed. +https://superuser.com/questions/835848/how-to-view-serial-com-ports-but-not-through-device-manager +https://stackoverflow.com/Questions/1388871/how-do-i-get-a-list-of-available-serial-ports-in-win32 +https://stackoverflow.com/questions/1205383/listing-serial-com-ports-on-windows +https://serverfault.com/questions/398469/what-are-the-minimum-permissions-to-read-the-wmi-class-msserial-portname +""" + +import re +from typing import NamedTuple, cast + +from mbed_tools.devices._internal.windows.component_descriptor import ( + ComponentDescriptor, + UNKNOWN_VALUE, +) + +CAPTION_PATTERN = re.compile(r"^.* [(](.*)[)]$") + + +def parse_caption(caption: str) -> str: + """Parses the caption string and returns the Port Name.""" + match = CAPTION_PATTERN.fullmatch(caption) + return match.group(1) if match else UNKNOWN_VALUE + + +class PnPEntityMsdnDefinition(NamedTuple): + """Msdn definition of a PnPEntity. + + See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-pnpentity + """ + + Availability: int + Caption: str + ClassGuid: str + CompatibleID: list + ConfigManagerErrorCode: int + ConfigManagerUserConfig: bool + CreationClassName: str + Description: str + DeviceID: str + ErrorCleared: bool + ErrorDescription: str + HardwareID: list + InstallDate: int + LastErrorCode: int + Manufacturer: str + Name: str + PNPClass: str + PNPDeviceID: str + PowerManagementCapabilities: int + PowerManagementSupported: bool + Present: bool + Service: str + Status: str + StatusInfo: int + SystemCreationClassName: str + SystemName: str + + +class SerialPort(ComponentDescriptor): + """Serial Port as defined in Windows API. + + As can be seen in Windows documentation, + https://docs.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors#ports--com---lpt-ports--, + ports are devices with ClassGuid = {4d36e978-e325-11ce-bfc1-08002be10318}. Hence the filter below. + """ + + def __init__(self) -> None: + """Initialiser.""" + super().__init__( + PnPEntityMsdnDefinition, + win32_class_name="Win32_PnPEntity", + win32_filter='ClassGuid="{4d36e978-e325-11ce-bfc1-08002be10318}"', + ) + + @property + def component_id(self) -> str: + """Returns the device id field.""" + return cast(str, self.get("DeviceID")) + + @property + def port_name(self) -> str: + """Gets the port name.""" + return parse_caption(self.get("Caption")) + + @property + def pnp_id(self) -> str: + """Gets the Plug and play id.""" + return cast(str, self.get("PNPDeviceID")) diff --git a/tools/python/mbed_tools/devices/_internal/windows/serial_port_data_loader.py b/tools/python/mbed_tools/devices/_internal/windows/serial_port_data_loader.py new file mode 100644 index 0000000000..3c9b34d278 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/serial_port_data_loader.py @@ -0,0 +1,39 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Loads serial port data.""" +from typing import Optional, Generator, cast, List + +from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader, ComponentsLoader +from mbed_tools.devices._internal.windows.usb_device_identifier import UsbIdentifier, parse_device_id +from mbed_tools.devices._internal.windows.serial_port import SerialPort + + +class SystemSerialPortInformation: + """All information about the serial ports of the current system.""" + + def __init__(self, data_loader: SystemDataLoader) -> None: + """Initialiser.""" + self._serial_port_by_usb_id: Optional[dict] = None + self._data_loader = data_loader + + def _load_data(self) -> None: + self._serial_port_by_usb_id = { + parse_device_id(p.pnp_id): p + for p in cast( + Generator[SerialPort, None, None], ComponentsLoader(self._data_loader, SerialPort).element_generator() + ) + } + + @property + def serial_port_data_by_id(self) -> dict: + """Gets system's serial ports by usb id.""" + if not self._serial_port_by_usb_id: + self._load_data() + return self._serial_port_by_usb_id if self._serial_port_by_usb_id else dict() + + def get_serial_port_information(self, usb_id: UsbIdentifier) -> List[SerialPort]: + """Gets all disk information for a given serial number.""" + port = self.serial_port_data_by_id.get(usb_id) + return [port] if port else list() diff --git a/tools/python/mbed_tools/devices/_internal/windows/system_data_loader.py b/tools/python/mbed_tools/devices/_internal/windows/system_data_loader.py new file mode 100644 index 0000000000..73968e55b4 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/system_data_loader.py @@ -0,0 +1,76 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Loads system data in parallel and all at once in order to improve performance.""" +from concurrent.futures import ThreadPoolExecutor +from typing import List, Tuple, Dict, Generator, Optional, cast + +from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptorWrapper, ComponentDescriptor +from mbed_tools.devices._internal.windows.disk_drive import DiskDrive +from mbed_tools.devices._internal.windows.disk_partition import DiskPartition +from mbed_tools.devices._internal.windows.disk_partition_logical_disk_relationships import ( + DiskPartitionLogicalDiskRelationship, +) +from mbed_tools.devices._internal.windows.logical_disk import LogicalDisk +from mbed_tools.devices._internal.windows.serial_port import SerialPort +from mbed_tools.devices._internal.windows.usb_controller import UsbController +from mbed_tools.devices._internal.windows.usb_hub import UsbHub + +# All Windows system data of interest in order to retrieve the information for DeviceCandidate. +SYSTEM_DATA_TYPES = [ + UsbHub, + UsbController, + DiskDrive, + DiskPartition, + LogicalDisk, + DiskPartitionLogicalDiskRelationship, + SerialPort, +] + + +def load_all(cls: type) -> Tuple[type, List[ComponentDescriptor]]: + """Loads all elements present in the system referring to a specific type.""" + return (cls, [element for element in ComponentDescriptorWrapper(cls).element_generator()]) + + +class SystemDataLoader: + """Object in charge of loading all system data with regards to Usb, Disk or serial port. + + It loads all the data in parallel and all at once in order to improve performance. + """ + + def __init__(self) -> None: + """Initialiser.""" + self._system_data: Optional[Dict[type, List[ComponentDescriptor]]] = None + + def _load(self) -> None: + """Loads all system data in parallel.""" + with ThreadPoolExecutor() as executor: + results = executor.map(load_all, SYSTEM_DATA_TYPES) + self._system_data = {k: v for (k, v) in results} + + @property + def system_data(self) -> Dict[type, List[ComponentDescriptor]]: + """Gets all system data.""" + if not self._system_data: + self._load() + return cast(Dict[type, List[ComponentDescriptor]], self._system_data) + + def get_system_data(self, cls: type) -> List[ComponentDescriptor]: + """Gets the system data for a particular type.""" + return self.system_data.get(cls, list()) + + +class ComponentsLoader: + """Loads system components.""" + + def __init__(self, data_loader: SystemDataLoader, cls: type) -> None: + """initialiser.""" + self._cls = cls + self._data_loader = data_loader + + def element_generator(self) -> Generator["ComponentDescriptor", None, None]: + """Gets a generator over all elements currently registered in the system.""" + for component in self._data_loader.get_system_data(self._cls): + yield component diff --git a/tools/python/mbed_tools/devices/_internal/windows/usb_controller.py b/tools/python/mbed_tools/devices/_internal/windows/usb_controller.py new file mode 100644 index 0000000000..3b2b5beca5 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/usb_controller.py @@ -0,0 +1,58 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a USB controller.""" + +from typing import NamedTuple, cast + +from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor + + +class UsbControllerMsdnDefinition(NamedTuple): + """Msdn definition of a USB controller. + + See https://docs.microsoft.com/en-gb/windows/win32/cimwin32prov/win32-usbcontroller?redirectedfrom=MSDN + Similar to https://docs.microsoft.com/en-gb/windows/win32/cimwin32prov/win32-usbcontrollerdevice + """ + + Availability: int + Caption: str + ConfigManagerErrorCode: int + ConfigManagerUserConfig: bool + CreationClassName: str + Description: str + DeviceID: str + ErrorCleared: bool + ErrorDescription: str + InstallDate: int + LastErrorCode: int + Manufacturer: str + MaxNumberControlled: int + Name: str + PNPDeviceID: str + PowerManagementCapabilities: list + PowerManagementSupported: bool + ProtocolSupported: int + Status: str + StatusInfo: int + SystemCreationClassName: str + SystemName: str + TimeOfLastReset: int + + +class UsbController(ComponentDescriptor): + """USB Controller as defined in Windows API. + + See https://docs.microsoft.com/en-gb/windows/win32/cimwin32prov/win32-usbcontroller?redirectedfrom=MSDN + Similar to https://docs.microsoft.com/en-gb/windows/win32/cimwin32prov/win32-usbcontrollerdevice + """ + + def __init__(self) -> None: + """Initialiser.""" + super().__init__(UsbControllerMsdnDefinition, win32_class_name="Win32_USBController") + + @property + def component_id(self) -> str: + """Returns the device id field.""" + return cast(str, self.get("DeviceID")) diff --git a/tools/python/mbed_tools/devices/_internal/windows/usb_data_aggregation.py b/tools/python/mbed_tools/devices/_internal/windows/usb_data_aggregation.py new file mode 100644 index 0000000000..7d66c76cbd --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/usb_data_aggregation.py @@ -0,0 +1,97 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Aggregation of all USB data given by Windows in various locations.""" +from typing import NamedTuple, List, cast + +from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor +from mbed_tools.devices._internal.windows.disk_aggregation import SystemDiskInformation, AggregatedDiskData +from mbed_tools.devices._internal.windows.serial_port import SerialPort +from mbed_tools.devices._internal.windows.serial_port_data_loader import SystemSerialPortInformation +from mbed_tools.devices._internal.windows.usb_device_identifier import UsbIdentifier +from mbed_tools.devices._internal.windows.usb_hub import UsbHub +from mbed_tools.devices._internal.windows.usb_hub_data_loader import SystemUsbDeviceInformation +from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader + + +class AggregatedUsbDataDefinition(NamedTuple): + """Data aggregation with regards to a usb device.""" + + usb_identifier: UsbIdentifier + disks: List[AggregatedDiskData] + serial_port: List[SerialPort] + # On Windows, each interface e.g. Composite, Mass storage, Port is defined as + # a separate independent UsbHub although they are related to the same device. + related_usb_interfaces: List[UsbHub] + + +class AggregatedUsbData(ComponentDescriptor): + """Usb information based on lots of different sources.""" + + def __init__(self) -> None: + """Initialiser.""" + super().__init__(AggregatedUsbDataDefinition, win32_class_name="AggregatedUsbData") + + @property + def component_id(self) -> str: + """Returns an id.""" + return str(self.uid) + + @property + def uid(self) -> UsbIdentifier: + """Returns the USB identifier.""" + return cast(UsbIdentifier, self.get("usb_identifier")) + + @property + def is_associated_with_disk(self) -> bool: + """States whether the usb device is associated with a disk.""" + return len(self.get("disks")) > 0 + + @property + def is_composite(self) -> bool: + """States whether the usb device is associated with multiple interfaces.""" + return len(self.get("related_usb_interfaces")) > 1 + + +class UsbDataAggregator: + """Aggregator of all data related to a USB device.""" + + def __init__( + self, + disk_data: SystemDiskInformation, + serial_data: SystemSerialPortInformation, + usb_data: SystemUsbDeviceInformation, + ) -> None: + """Initialiser.""" + self._disk_data = disk_data + self._serial_data = serial_data + self._usb_devices = usb_data + + def aggregate(self, usb_id: UsbIdentifier) -> AggregatedUsbData: + """Aggregates data about a USB device from different sources.""" + disk_data = self._disk_data.get_disk_information(usb_id.uid) + serial_data = self._serial_data.get_serial_port_information(usb_id) + usb_data = self._usb_devices.get_usb_devices(usb_id) + aggregated_data = AggregatedUsbData() + aggregated_data.set_data_values( + dict(usb_identifier=usb_id, disks=disk_data, serial_port=serial_data, related_usb_interfaces=usb_data,) + ) + return aggregated_data + + +class SystemUsbData: + """System in charge of gathering all the data related to USB devices.""" + + def __init__(self, data_loader: SystemDataLoader) -> None: + """Initialiser.""" + self._usb_devices = SystemUsbDeviceInformation(data_loader) + self._aggregator = UsbDataAggregator( + disk_data=SystemDiskInformation(data_loader), + serial_data=SystemSerialPortInformation(data_loader), + usb_data=self._usb_devices, + ) + + def all(self) -> List[AggregatedUsbData]: + """Gets all the system data about USB devices.""" + return [self._aggregator.aggregate(usb_id) for usb_id in self._usb_devices.usb_device_ids()] diff --git a/tools/python/mbed_tools/devices/_internal/windows/usb_device_identifier.py b/tools/python/mbed_tools/devices/_internal/windows/usb_device_identifier.py new file mode 100644 index 0000000000..85670c3227 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/usb_device_identifier.py @@ -0,0 +1,153 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a USB Identifier.""" + +import re +from typing import Dict, List, NamedTuple, Optional, Pattern, Any, cast + +from mbed_tools.devices._internal.windows.component_descriptor_utils import is_undefined_data_object +from mbed_tools.devices._internal.windows.windows_identifier import WindowsUID + +KEY_UID = "UID" + + +class UsbIdentifier(NamedTuple): + """Object describing the different elements present in the device ID. + + Attributes: + UID: Universal ID, either the serial number or device instance ID. + VID: Vendor ID, 4 digit. + PID: Product ID assigned to the devices, 4 digit. + REV: Revision code. + MI: Multiple Interface, a 2 digit interface number. + """ + + UID: Optional[str] = None + VID: Optional[str] = None + PID: Optional[str] = None + REV: Optional[str] = None + MI: Optional[str] = None + + @staticmethod + def get_patterns_dict() -> Dict[str, Pattern]: + """Returns a dictionary of all the regexes.""" + return {p: re.compile(f"^{p}_(.*)$") for p in UsbIdentifier._fields[1:]} + + @property + def uid(self) -> WindowsUID: + """Gets the USB ID.""" + return cast(WindowsUID, self.UID) + + def contains_genuine_serial_number(self) -> bool: + """Contains a genuine serial number and not an instance ID.""" + return self.uid.contains_genuine_serial_number() + + @property + def product_id(self) -> str: + """Returns the product id field.""" + return self.PID or "" + + @property + def vendor_id(self) -> str: + """Returns the product id field.""" + return self.VID or "" + + def __eq__(self, other: Any) -> bool: + """States whether the other id equals to self.""" + if not other or not isinstance(other, UsbIdentifier): + return False + if self.is_undefined: + return other.is_undefined + + return all([self.uid == other.uid, self.product_id == other.product_id, self.vendor_id == other.vendor_id]) + + def __hash__(self) -> int: + """Generates a hash.""" + return hash(self.uid) + hash(self.product_id) + hash(self.vendor_id) + + @property + def is_undefined(self) -> bool: + """States whether none of the elements present in DeviceId were defined.""" + return is_undefined_data_object(cast(NamedTuple, self)) + + +class Win32DeviceIdParser: + """Parser of a standard Win32 device ID. + + See https://docs.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers + """ + + def parse_uid(self, raw_id: str, serial_number: Optional[str] = None) -> WindowsUID: + """Parses the UID value. + + As described here: + https://docs.microsoft.com/it-it/windows-hardware/drivers/install/device-instance-ids + https://stackoverflow.com/questions/51513337/is-the-usb-instance-id-on-windows-unique-for-a-device + https://docs.microsoft.com/it-it/windows-hardware/drivers/install/instance-ids + the instance ID corresponds to the serial number information, if supported by the underlying bus, otherwise + it is generated by Windows. + + For some boards (e.g. ST boards), the ID may contain other information that we are not interested in + (e.g. MI value). This method tries to retrieve the actual ID. + """ + id_elements = raw_id.split("&") + if len(id_elements) <= 1: + # The instance ID is the serial number. + return WindowsUID(uid=raw_id.lower(), raw_uid=raw_id, serial_number=serial_number) + # The instance ID is generated by Windows and hence might contain other element than the ParentPrefixID. + # The following tries to only consider what may be the ParentPrefixID. + return WindowsUID(uid="&".join(id_elements[:-1]).lower(), raw_uid=raw_id, serial_number=serial_number) + + def record_id_element(self, element: str, valuable_information: dict, patterns_dict: dict) -> None: + """Stores recognised parts of the device ID based on patterns defined.""" + for k, p in patterns_dict.items(): + match = p.fullmatch(element) + if match: + valuable_information[k] = match.group(1) + + def split_id_elements(self, parts: List[str], serial_number: str = None) -> dict: + """Splits the different elements of an Device ID.""" + information = dict() + information[KEY_UID] = self.parse_uid(parts[-1], serial_number) + other_elements = parts[-2].split("&") + patterns_dict = UsbIdentifier.get_patterns_dict() + for element in other_elements: + self.record_id_element(element, information, patterns_dict) + return information + + def parse(self, id_string: Optional[str], serial_number: Optional[str] = None) -> "UsbIdentifier": + r"""Parses the device id string and retrieves the different elements of interest. + + See https://docs.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers + Format: \ + Ex. `USB\VID_2109&PID_8110\5&376ABA2D&0&21` + - ``: `USB\VID_2109&PID_8110` + - ``: `5&376ABA2D&0&21` + [Device instance IDs](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/device-instance-ids) + -> [Device IDs](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/device-ids) + -> [Hardware IDs](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/hardware-ids) + -> [Device identifier formats](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/device-identifier-formats) # noqa: E501 + -> [Identifiers for USB](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/identifiers-for-usb-devices) + - [Standard USB Identifiers](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers) + - [Special USB Identifiers](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/special-usb-identifiers) + - [Instance specific ID](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/instance-ids) + + Returns: + corresponding DeviceIdInformation. + """ + if not id_string or len(id_string.strip()) == 0: + return UsbIdentifier() + parts = id_string.split("\\") + if len(parts) < 2: + return UsbIdentifier() + return UsbIdentifier(**self.split_id_elements(parts, serial_number)) + + +def parse_device_id(id_string: Optional[str], serial_number: Optional[str] = None) -> UsbIdentifier: + """Parses the device id string and retrieves the different elements of interest. + + See https://docs.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers + """ + return Win32DeviceIdParser().parse(id_string, serial_number) diff --git a/tools/python/mbed_tools/devices/_internal/windows/usb_hub.py b/tools/python/mbed_tools/devices/_internal/windows/usb_hub.py new file mode 100644 index 0000000000..7999508c7c --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/usb_hub.py @@ -0,0 +1,68 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a USB hub.""" + +from typing import NamedTuple, cast + +from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor + + +class UsbHubMsdnDefinition(NamedTuple): + """Msdn definition of a Usb hub. + + See https://docs.microsoft.com/en-gb/previous-versions/windows/desktop/cimwin32a/win32-usbhub?redirectedfrom=MSDN + See also https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-usbdevice + """ + + Availability: int + Caption: str + ClassCode: int + ConfigManagerUserConfig: bool + CreationClassName: str + CurrentAlternateSettings: list + CurrentConfigValue: int + Description: str + ErrorCleared: bool + ErrorDescription: str + GangSwitched: bool + InstallDate: int + LastErrorCode: int + NumberOfConfigs: int + NumberOfPorts: int + PNPDeviceID: str + PowerManagementCapabilities: list + PowerManagementSupported: bool + ProtocolCode: int + Status: str + StatusInfo: int + SubclassCode: int + SystemCreationClassName: str + SystemName: str + USBVersion: int + ConfigManagerErrorCode: int + DeviceID: str + Name: str + + +class UsbHub(ComponentDescriptor): + """USB Hub as defined in Windows API. + + See https://docs.microsoft.com/en-gb/previous-versions/windows/desktop/cimwin32a/win32-usbhub?redirectedfrom=MSDN + Seems similar to https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-usbhub + """ + + def __init__(self) -> None: + """Initialiser.""" + super().__init__(UsbHubMsdnDefinition, win32_class_name="Win32_USBHub") + + @property + def component_id(self) -> str: + """Returns the device id field.""" + return cast(str, self.get("DeviceID")) + + @property + def pnp_id(self) -> str: + """Returns the plug and play id field.""" + return cast(str, self.get("PNPDeviceID")) diff --git a/tools/python/mbed_tools/devices/_internal/windows/usb_hub_data_loader.py b/tools/python/mbed_tools/devices/_internal/windows/usb_hub_data_loader.py new file mode 100644 index 0000000000..2e05df56bf --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/usb_hub_data_loader.py @@ -0,0 +1,87 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Loads System's USB hub.""" + +from typing import Dict, List, cast, Optional, Set, Generator + +from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor +from mbed_tools.devices._internal.windows.device_instance_id import get_children_instance_id +from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader, ComponentsLoader +from mbed_tools.devices._internal.windows.usb_controller import UsbController +from mbed_tools.devices._internal.windows.usb_device_identifier import parse_device_id, UsbIdentifier +from mbed_tools.devices._internal.windows.usb_hub import UsbHub + + +class SystemUsbDeviceInformation: + """Usb Hub cache for this system. + + On Windows, each interface e.g. Composite, Mass storage, Port is defined as + a separate independent UsbHub although they are related to the same device. + This cache tries to reduce the list of UsbHubs to only genuinely different devices. + """ + + def __init__(self, data_loader: SystemDataLoader) -> None: + """Initialiser.""" + self._cache: Optional[Dict[UsbIdentifier, List[UsbHub]]] = None + self._ids_cache: Optional[Set[UsbIdentifier]] = None + self._data_loader = data_loader + + def _list_usb_controller_ids(self) -> List[UsbIdentifier]: + return cast( + List[UsbIdentifier], + [ + parse_device_id(cast(UsbController, usbc).component_id) + for usbc in ComponentsLoader(self._data_loader, UsbController).element_generator() + ], + ) + + def _iterate_over_hubs(self) -> Generator[ComponentDescriptor, None, None]: + return ComponentsLoader(self._data_loader, UsbHub).element_generator() + + def _populate_id_cache_with_non_serialnumbers(self) -> None: + if not self._cache or not self._ids_cache: + return + for usb_id in self._cache: + if usb_id not in self._ids_cache: + self._ids_cache.add(usb_id) + + def _determine_potential_serial_number(self, usb_device: UsbHub) -> Optional[str]: + return get_children_instance_id(usb_device.pnp_id) + + def _load(self) -> None: + """Populates the cache.""" + self._cache = cast(Dict[UsbIdentifier, List[UsbHub]], dict()) + self._ids_cache = cast(Set[UsbIdentifier], set()) + controllers = self._list_usb_controller_ids() + for usb_device in self._iterate_over_hubs(): + usb_id = parse_device_id( + usb_device.component_id, serial_number=self._determine_potential_serial_number(cast(UsbHub, usb_device)) + ) + if usb_id in controllers: + continue + entry = self._cache.get(usb_id, list()) + entry.append(cast(UsbHub, usb_device)) + self._cache[usb_id] = entry + + if usb_id.contains_genuine_serial_number(): + self._ids_cache.add(usb_id) + self._populate_id_cache_with_non_serialnumbers() + + @property + def usb_devices(self) -> Dict[UsbIdentifier, List[UsbHub]]: + """Usb devices present in the system.""" + if not self._cache: + self._load() + return cast(Dict[UsbIdentifier, List[UsbHub]], self._cache) + + def get_usb_devices(self, uid: UsbIdentifier) -> List[UsbHub]: + """Gets all USB devices related to an identifier.""" + return self.usb_devices.get(uid, list()) + + def usb_device_ids(self) -> List[UsbIdentifier]: + """Gets system usb device IDs.""" + if not self._ids_cache: + self._load() + return cast(List[UsbIdentifier], self._ids_cache) diff --git a/tools/python/mbed_tools/devices/_internal/windows/volume_set.py b/tools/python/mbed_tools/devices/_internal/windows/volume_set.py new file mode 100644 index 0000000000..a48f857b38 --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/volume_set.py @@ -0,0 +1,89 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a Volume Set. + +CIM_VolumeSet should be the data model to use but does not seem to actually return the data that we are looking for: +https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-volumeset +Therefore, a specific data model needs to be constructed using other Windows methods. +""" + +from enum import Enum +from typing import NamedTuple, List +from mbed_tools.devices._internal.windows.component_descriptor import UNKNOWN_VALUE + +import logging +import win32.win32api +import win32.win32file + +logger = logging.getLogger(__name__) + + +class DriveType(Enum): + """Drive type as defined in Win32 API. + + See https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea. + """ + + DRIVE_UNKNOWN = 0 # The drive type cannot be determined. + DRIVE_NO_ROOT_DIR = 1 # The root path is invalid; for example, there is no volume mounted at the specified path. + DRIVE_REMOVABLE = ( + 2 + # The drive has removable media; for example, a floppy drive, thumb drive, or flash card reader. + ) + DRIVE_FIXED = 3 # The drive has fixed media; for example, a hard disk drive or flash drive. + DRIVE_REMOTE = 4 # The drive is a remote (network) drive. + DRIVE_CDROM = 5 # The drive is a CD-ROM drive. + DRIVE_RAMDISK = 6 # The drive is a RAM disk. + + +class VolumeInformation(NamedTuple): + """Volume information. + + See http://timgolden.me.uk/pywin32-docs/win32api__GetVolumeInformation_meth.html + See also http://timgolden.me.uk/python/win32_how_do_i/find-drive-types.html + """ + + Name: str + SerialNumber: int + MaxComponentLengthOfAFileName: int + SysFlags: int + FileSystem: str + UniqueName: str # As defined by GetVolumeNameForVolumeMountPoint + DriveType: DriveType # As defined by GetDriveType + + +def _get_windows_volume_information(volume: str) -> List[str]: + try: + return list(win32.win32api.GetVolumeInformation(volume)) + except Exception as e: + logger.debug(f"Cannot retrieve information about volume {volume}. Reason: {e}") + return [UNKNOWN_VALUE] * 5 + + +def _get_volume_name_for_mount_point(volume: str) -> str: + try: + return str(win32.win32file.GetVolumeNameForVolumeMountPoint(volume)) + except Exception as e: + logger.debug(f"Cannot retrieve the real name of volume {volume}. Reason: {e}") + return UNKNOWN_VALUE + + +def _get_drive_type(volume: str) -> DriveType: + try: + return DriveType(win32.win32file.GetDriveType(volume)) + except Exception as e: + logger.debug(f"Cannot retrieve the type of volume {volume}. Reason: {e}") + return DriveType.DRIVE_UNKNOWN + + +def get_volume_information(volume: str) -> VolumeInformation: + """Gets the volume information.""" + if not volume.endswith("\\"): + volume = f"{volume}\\" + values: list = _get_windows_volume_information(volume) + [ + _get_volume_name_for_mount_point(volume), + _get_drive_type(volume), # type: ignore + ] + return VolumeInformation(*values) diff --git a/tools/python/mbed_tools/devices/_internal/windows/windows_identifier.py b/tools/python/mbed_tools/devices/_internal/windows/windows_identifier.py new file mode 100644 index 0000000000..d8e794876b --- /dev/null +++ b/tools/python/mbed_tools/devices/_internal/windows/windows_identifier.py @@ -0,0 +1,134 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines a Windows identifier.""" +from functools import total_ordering +from typing import Any, Optional + +from mbed_tools.devices._internal.windows.component_descriptor_utils import is_undefined_value + +INSTANCE_ID_CHARACTER = "&" + + +def is_device_instance_id(value: Optional[str]) -> bool: + """Determines whether a value is a device instance ID generated by Windows or not. + + See https://docs.microsoft.com/it-it/windows-hardware/drivers/install/instance-ids + Typical IDs generated by Windows for a same device (but different interfaces) are, as follows: + - 8&2F125EC6&0&0003 + - 8&2f125ec6&0 + - 8&2F125EC6&0&0002 + """ + return is_undefined_value(value) or INSTANCE_ID_CHARACTER in str(value) + + +@total_ordering +class WindowsUID: + """Definition of a Windows Universal ID. + + UID in Windows can be either a serial number or an device instance ID depending + on how underlying interfaces were defined. + This object tries to hide the complexity of deciding which of the fields should be + considered as the UID and defining + equality between UIDs. + Indeed different components referring to a same device may have completely different UIDs : + e.g. 8&2F125EC6&0&0002, 8&2F125EC6&0&0003, 8&2f125ec6&0 and 345240562 + may all refer to the same component. + """ + + uid: str # Processed string which may be the closest to the Universal ID. + # This could be for the same device: + # - 345240562 + # - 8&2F125EC6&0 + # - 8&2F125EC6 + # - 8&2f125ec6 + raw_uid: str # The String from which the UID was derived. + # Raw form, as it can be a combination of other elements + # e.g. 8&2F125EC6&0&0003. In this case, the 0003 suffix corresponds to the MI value + # and has nothing to do with the instance ID per se. + serial_number: Optional[str] # a serial number. + + # A string which can be the serial number of the device defined by + # the underlying bus which may uniquely define the device. + # e.g. 345240562. However, for some devices, + # the serial number may not be accessible or defined or may actually be an instance ID. + + def __init__(self, uid: str, raw_uid: str, serial_number: Optional[str]) -> None: + """Initialiser.""" + self.uid = uid + self.raw_uid = raw_uid + self.serial_number = serial_number + + def __eq__(self, other: Any) -> bool: + """Defines the equality checker. + + The `equal` method does not follow a typical data model equal logic + because of the complexity of this class which tries to find out which of its + components is actually relevant and hence matters in determining equality. + e.g. + (uid="8&2f125ec6&0", raw_uid="8&2F125EC6&0&0003",serial_number="123456789") + is the same as (uid="123456789", raw_uid="123456789",serial_number="8&2f125ec6") + or (uid="8&2f125ec6&0", raw_uid="8&2F125EC6&0&000",serial_number=None) + """ + if not other or not isinstance(other, WindowsUID): + return False + if ( + self.uid == other.uid + or self.raw_uid == other.raw_uid + or self.serial_number == other.serial_number + or self.serial_number == other.uid + or self.uid == other.serial_number + ): + return True + # Due to the complexity of determining the UID on Windows, + # we can assume that we are dealing with the same ID if IDs are subsets of each other. + return str(self.uid).startswith(str(other.uid)) or str(other.uid).startswith(str(self.uid)) + + @property + def presumed_serial_number(self) -> str: + """Determines what may be the most likely value for the serial number of the component. + + From the different components at its disposal, the system tries to find what may be the serial number. + """ + if not is_device_instance_id(self.uid): + # the UID is not an instance ID and should therefore be the serial number. + return self.uid + return self.uid if is_device_instance_id(self.serial_number) else str(self.serial_number) + + @property + def instance_id(self) -> str: + """Determines what may be the value most likely to be the instance_id of a component as generated by Windows.""" + if is_device_instance_id(self.uid): + return self.uid + return self.uid if is_undefined_value(self.serial_number) else str(self.serial_number) + + def contains_genuine_serial_number(self) -> bool: + """Contains a genuine serial number and not an instance ID.""" + serial_number = self.presumed_serial_number + return not (is_undefined_value(serial_number) or is_device_instance_id(serial_number)) + + def __hash__(self) -> int: + """Calculates the hash of the UID. + + Due to the complexity of the `equal` method, this implementation of the `hash` + calculation breaks the hashing/equality rules of typical data objects. + In the present case, the WindowsUID is mostly used for dictionary lookup and therefore, + the actual use cases were tested. + In most cases, the instance ID of a device will be provided rather than the serial number and therefore, + this field will be considered as differentiators between UIDs. + """ + return hash(self.instance_id) + + def __repr__(self) -> str: + """String representation of the UID.""" + return f"WindowsUID({self.uid})" + + def __str__(self) -> str: + """String representation of the UID.""" + elements = [f"{k}={v!r}" for (k, v) in self.__dict__.items()] + return f"WindowsUID({', '.join(elements)})" + + def __lt__(self, other: "WindowsUID") -> bool: + """Defines less than.""" + return self.presumed_serial_number < other.presumed_serial_number diff --git a/tools/python/mbed_tools/devices/device.py b/tools/python/mbed_tools/devices/device.py new file mode 100644 index 0000000000..c7836ef9fc --- /dev/null +++ b/tools/python/mbed_tools/devices/device.py @@ -0,0 +1,103 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Data model definition for Device and ConnectedDevices.""" +from dataclasses import dataclass, field +from pathlib import Path +from typing import Tuple, Optional, List +from mbed_tools.targets import Board +from mbed_tools.devices._internal.detect_candidate_devices import CandidateDevice +from mbed_tools.devices._internal.resolve_board import resolve_board, NoBoardForCandidate, ResolveBoardError +from mbed_tools.devices._internal.file_parser import read_device_files +from mbed_tools.devices.exceptions import DeviceLookupFailed + + +@dataclass(frozen=True, order=True) +class Device: + """Definition of an Mbed Enabled Device. + + An Mbed Device is always a USB mass storage device, which sometimes also presents a USB serial port. + A valid Mbed Device must have a Board associated with it. + + Attributes: + mbed_board: The Board associated with this device. + serial_number: The serial number presented by the device to the USB subsystem. + serial_port: The serial port presented by this device, could be None. + mount_points: The filesystem mount points associated with this device. + """ + + mbed_board: Board + serial_number: str + serial_port: Optional[str] + mount_points: Tuple[Path, ...] + mbed_enabled: bool = False + interface_version: Optional[str] = None + + @classmethod + def from_candidate(cls, candidate: CandidateDevice) -> "Device": + """Contruct a Device from a CandidateDevice. + + We try to resolve a board using data files that may be stored on the CandidateDevice. + If this fails we set the board to `None` which means we couldn't verify this Device + as being an Mbed enabled device. + + Args: + candidate: The CandidateDevice we're using to create the Device. + """ + device_file_info = read_device_files(candidate.mount_points) + try: + mbed_board = resolve_board( + device_file_info.product_code, device_file_info.online_id, candidate.serial_number + ) + mbed_enabled = True + except NoBoardForCandidate: + # Create an empty Board to ensure the device is fully populated and rendering is simple + mbed_board = Board.from_offline_board_entry({}) + mbed_enabled = False + except ResolveBoardError: + raise DeviceLookupFailed( + f"Failed to resolve the board for candidate device {candidate!r}. There was a problem looking up the " + "board data in the database." + ) + + return Device( + serial_port=candidate.serial_port, + serial_number=candidate.serial_number, + mount_points=candidate.mount_points, + mbed_board=mbed_board, + mbed_enabled=mbed_enabled, + interface_version=device_file_info.interface_details.get("Version"), + ) + + +@dataclass(order=True) +class ConnectedDevices: + """Definition of connected devices which may be Mbed Boards. + + If a connected device is identified as an Mbed Board by using the HTM file on the USB mass storage device (or + sometimes by using the serial number), it will be included in the `identified_devices` list. + + However, if the device appears as if it could be an Mbed Board but it has not been possible to find a matching + entry in the database then it will be included in the `unidentified_devices` list. + + Attributes: + identified_devices: A list of devices that have been identified as MbedTargets. + unidentified_devices: A list of devices that could potentially be MbedTargets. + """ + + identified_devices: List[Device] = field(default_factory=list) + unidentified_devices: List[Device] = field(default_factory=list) + + def add_device(self, device: Device) -> None: + """Add a device to the connected devices. + + Args: + device: a Device object containing the device information. + """ + if not device.mbed_enabled: + # Keep a list of devices that could not be identified but are Mbed Boards + self.unidentified_devices.append(device) + else: + # Keep a list of devices that have been identified as Mbed Boards + self.identified_devices.append(device) diff --git a/tools/python/mbed_tools/devices/devices.py b/tools/python/mbed_tools/devices/devices.py new file mode 100644 index 0000000000..5736535b8c --- /dev/null +++ b/tools/python/mbed_tools/devices/devices.py @@ -0,0 +1,100 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""API for listing devices.""" + +from operator import attrgetter +from typing import List, Optional + +from mbed_tools.devices._internal.detect_candidate_devices import detect_candidate_devices + +from mbed_tools.devices.device import ConnectedDevices, Device +from mbed_tools.devices.exceptions import DeviceLookupFailed, NoDevicesFound + + +def get_connected_devices() -> ConnectedDevices: + """Returns Mbed Devices connected to host computer. + + Connected devices which have been identified as Mbed Boards and also connected devices which are potentially + Mbed Boards (but not could not be identified in the database) are returned. + """ + connected_devices = ConnectedDevices() + + for candidate_device in detect_candidate_devices(): + device = Device.from_candidate(candidate_device) + connected_devices.add_device(device) + + return connected_devices + + +def find_connected_device(target_name: str, identifier: Optional[int] = None) -> Device: + """Find a connected device matching the given target_name, if there is only one. + + Args: + target_name: The Mbed target name of the device. + identifier: Where multiple of the same Mbed device are connected, the associated [id]. + + Raise: + DeviceLookupFailed: Could not find device matching target_name. + + Returns: + The first Device found matching target_name. + """ + devices = find_all_connected_devices(target_name) + if identifier is None and len(devices) == 1: + return devices[0] + elif identifier is not None and len(devices) > identifier: + return devices[identifier] + + detected_targets = "\n".join( + f"target: {dev.mbed_board.board_type}[{i}]," f" port: {dev.serial_port}, mount point(s): {dev.mount_points}" + for i, dev in enumerate(devices) + ) + if identifier is None: + msg = ( + f"`Multiple matching, please select a connected target with [n] identifier.\n" + f"The following {target_name}s were detected:\n{detected_targets}" + ) + else: + msg = ( + f"`{target_name}[{identifier}]` is not a valid connected target.\n" + f"The following {target_name}s were detected:\n{detected_targets}" + ) + raise DeviceLookupFailed(msg) + + +def find_all_connected_devices(target_name: str) -> List[Device]: + """Find all connected devices matching the given target_name. + + Args: + target_name: The Mbed target name of the device. + + Raises: + NoDevicesFound: Could not find any connected devices. + DeviceLookupFailed: Could not find a connected device matching target_name. + + Returns: + List of Devices matching target_name. + """ + connected = get_connected_devices() + if not connected.identified_devices: + raise NoDevicesFound("No Mbed enabled devices found.") + + matching_devices = sorted( + [device for device in connected.identified_devices if device.mbed_board.board_type == target_name.upper()], + key=attrgetter("serial_number"), + ) + if matching_devices: + return matching_devices + + detected_targets = "\n".join( + f"target: {dev.mbed_board.board_type}, port: {dev.serial_port}, mount point(s): {dev.mount_points}" + for dev in connected.identified_devices + ) + msg = ( + f"Target '{target_name}' was not detected.\n" + "Check the device is connected by USB, and that the name is entered correctly.\n" + f"The following devices were detected:\n{detected_targets}" + ) + raise DeviceLookupFailed(msg) diff --git a/tools/python/mbed_tools/devices/exceptions.py b/tools/python/mbed_tools/devices/exceptions.py new file mode 100644 index 0000000000..570941d935 --- /dev/null +++ b/tools/python/mbed_tools/devices/exceptions.py @@ -0,0 +1,22 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Public exceptions raised by the package.""" +from mbed_tools.lib.exceptions import ToolsError + + +class MbedDevicesError(ToolsError): + """Base public exception for the mbed-devices package.""" + + +class DeviceLookupFailed(MbedDevicesError): + """Failed to look up data associated with the device.""" + + +class NoDevicesFound(MbedDevicesError): + """No Mbed Enabled devices were found.""" + + +class UnknownOSError(MbedDevicesError): + """The current OS is not supported.""" diff --git a/tools/python/mbed_tools/lib/__init__.py b/tools/python/mbed_tools/lib/__init__.py new file mode 100644 index 0000000000..23d6bf6ad2 --- /dev/null +++ b/tools/python/mbed_tools/lib/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Provides a library of common code.""" diff --git a/tools/python/mbed_tools/lib/exceptions.py b/tools/python/mbed_tools/lib/exceptions.py new file mode 100644 index 0000000000..3784f44257 --- /dev/null +++ b/tools/python/mbed_tools/lib/exceptions.py @@ -0,0 +1,9 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Exceptions raised by mbed tools.""" + + +class ToolsError(Exception): + """Base class for tools errors.""" diff --git a/tools/python/mbed_tools/lib/json_helpers.py b/tools/python/mbed_tools/lib/json_helpers.py new file mode 100644 index 0000000000..d1a26fe84f --- /dev/null +++ b/tools/python/mbed_tools/lib/json_helpers.py @@ -0,0 +1,22 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Helpers for json related functions.""" +import json +import logging + +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + + +def decode_json_file(path: Path) -> Any: + """Return the contents of json file.""" + try: + logger.debug(f"Loading JSON file {path}") + return json.loads(path.read_text()) + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON data in the file located at '{path}'") + raise diff --git a/tools/python/mbed_tools/lib/logging.py b/tools/python/mbed_tools/lib/logging.py new file mode 100644 index 0000000000..6dcf8841d4 --- /dev/null +++ b/tools/python/mbed_tools/lib/logging.py @@ -0,0 +1,87 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Helpers for logging errors according to severity of the exception.""" +from typing import Type, Optional, cast +from types import TracebackType +import logging +from mbed_tools.lib.exceptions import ToolsError + +LOGGING_FORMAT = "%(levelname)s: %(message)s" + +VERBOSITY_HELP = { + logging.CRITICAL: "-v", + logging.ERROR: "-v", + logging.WARNING: "-vv", + logging.INFO: "-vvv", + logging.DEBUG: "--traceback", +} + + +def _exception_message(err: BaseException, log_level: int, traceback: bool) -> str: + """Generate a user facing message with help on how to get more information from the logs.""" + error_msg = str(err) + if log_level != logging.DEBUG or not traceback: + cli_option = VERBOSITY_HELP.get(log_level, "-v") + error_msg += f"\n\nMore information may be available by using the command line option '{cli_option}'." + return error_msg + + +class MbedToolsHandler: + """Context Manager to catch Mbed Tools exceptions and generate a helpful user facing message.""" + + def __init__(self, logger: logging.Logger, traceback: bool = False): + """Keep track of the logger to use and whether or not a traceback should be generated.""" + self._logger = logger + self._traceback = traceback + self.exit_code = 0 + + def __enter__(self) -> "MbedToolsHandler": + """Return the Context Manager.""" + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + exc_traceback: Optional[TracebackType], + ) -> bool: + """Handle any raised exceptions, suppressing Tools errors and generating an error message instead.""" + if exc_type and issubclass(exc_type, ToolsError): + error_msg = _exception_message(cast(BaseException, exc_value), logging.root.level, self._traceback) + self._logger.error(error_msg, exc_info=self._traceback) + # Do not propagate exceptions derived from ToolsError + self.exit_code = 1 + return True + + # Propagate all other exceptions + return False + + +def log_exception(logger: logging.Logger, exception: Exception, show_traceback: bool = False) -> None: + """Logs an exception in both normal and verbose forms. + + Args: + logger: logger + exception: exception to log + show_traceback: show the full traceback. + """ + logger.error(exception, exc_info=show_traceback) + + +def set_log_level(verbose_count: int) -> None: + """Sets the log level. + + Args: + verbose_count: number of `-v` flags used + """ + if verbose_count > 2: + log_level = logging.DEBUG + elif verbose_count == 2: + log_level = logging.INFO + elif verbose_count == 1: + log_level = logging.WARNING + else: + log_level = logging.ERROR + logging.basicConfig(level=log_level, format=LOGGING_FORMAT) diff --git a/tools/python/mbed_tools/lib/python_helpers.py b/tools/python/mbed_tools/lib/python_helpers.py new file mode 100644 index 0000000000..9dc236e834 --- /dev/null +++ b/tools/python/mbed_tools/lib/python_helpers.py @@ -0,0 +1,28 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Helpers for python language related functions.""" +from typing import Iterable, List + + +def flatten_nested(input_iter: Iterable) -> List: + """Flatten a nested Iterable with arbitrary levels of nesting. + + If the input is an iterator then this function will exhaust it. + + Args: + input_iter: The input Iterable which may or may not be nested. + + Returns: + A flat list created from the input_iter. + If input_iter has no nesting its elements are appended to a list and returned. + """ + output = [] + for elem in input_iter: + if isinstance(elem, Iterable) and not isinstance(elem, str): + output += flatten_nested(elem) + else: + output.append(elem) + + return output diff --git a/tools/python/mbed_tools/project/__init__.py b/tools/python/mbed_tools/project/__init__.py new file mode 100644 index 0000000000..da693bdf87 --- /dev/null +++ b/tools/python/mbed_tools/project/__init__.py @@ -0,0 +1,13 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Creation and management of Mbed OS projects. + +* Creation of a new Mbed OS application. +* Cloning of an existing Mbed OS program. +* Deploy of a specific version of Mbed OS or library. +""" + +from mbed_tools.project.project import initialise_project, import_project, deploy_project, get_known_libs +from mbed_tools.project.mbed_program import MbedProgram diff --git a/tools/python/mbed_tools/project/_internal/__init__.py b/tools/python/mbed_tools/project/_internal/__init__.py new file mode 100644 index 0000000000..868c3a2950 --- /dev/null +++ b/tools/python/mbed_tools/project/_internal/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Code not to be accessed by external applications.""" diff --git a/tools/python/mbed_tools/project/_internal/git_utils.py b/tools/python/mbed_tools/project/_internal/git_utils.py new file mode 100644 index 0000000000..a84d582062 --- /dev/null +++ b/tools/python/mbed_tools/project/_internal/git_utils.py @@ -0,0 +1,147 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Wrappers for git operations.""" +from dataclasses import dataclass +from pathlib import Path + +import git +import logging + +from mbed_tools.project.exceptions import VersionControlError +from mbed_tools.project._internal.progress import ProgressReporter +from typing import Optional + +logger = logging.getLogger(__name__) + + +@dataclass +class GitReference: + """Git reference for a remote repository. + + Attributes: + repo_url: URL of the git repository. + ref: The reference commit sha, tag or branch. + """ + + repo_url: str + ref: str + + +def clone(url: str, dst_dir: Path, ref: Optional[str] = None, depth: int = 1) -> git.Repo: + """Clone a library repository. + + Args: + url: URL of the remote to clone. + dst_dir: Destination directory for the cloned repo. + ref: An optional git commit hash, branch or tag reference to checkout + depth: Truncate history to the specified number of commits. Defaults to + 1, to make a shallow clone. + + Raises: + VersionControlError: Cloning the repository failed. + """ + # Gitpython doesn't propagate the git error message when a repo is already + # cloned, so we cannot depend on git to handle the "already cloned" error. + # We must handle this ourselves instead. + if dst_dir.exists() and list(dst_dir.glob("*")): + raise VersionControlError(f"{dst_dir} exists and is not an empty directory.") + + clone_from_kwargs = {"url": url, "to_path": str(dst_dir), "progress": ProgressReporter(name=url), "depth": depth} + if ref: + clone_from_kwargs["branch"] = ref + + try: + return git.Repo.clone_from(**clone_from_kwargs) + except git.exc.GitCommandError as err: + raise VersionControlError(f"Cloning git repository from url '{url}' failed. Error from VCS: {err}") + + +def checkout(repo: git.Repo, ref: str, force: bool = False) -> None: + """Check out a specific reference in the given repository. + + Args: + repo: git.Repo object where the checkout will be performed. + ref: Git commit hash, branch or tag reference, must be a valid ref defined in the repo. + + Raises: + VersionControlError: Check out failed. + """ + try: + git_args = [ref] + ["--force"] if force else [ref] + repo.git.checkout(*git_args) + except git.exc.GitCommandError as err: + raise VersionControlError(f"Failed to check out revision '{ref}'. Error from VCS: {err}") + + +def fetch(repo: git.Repo, ref: str) -> None: + """Fetch from the repo's origin. + + Args: + repo: git.Repo object where the checkout will be performed. + ref: Git commit hash, branch or tag reference, must be a valid ref defined in the repo. + + Raises: + VersionControlError: Fetch failed. + """ + try: + repo.git.fetch("origin", ref) + except git.exc.GitCommandError as err: + raise VersionControlError(f"Failed to fetch. Error from VCS: {err}") + + +def init(path: Path) -> git.Repo: + """Initialise a git repository at the given path. + + Args: + path: Path where the repo will be initialised. + + Returns: + Initialised git.Repo object. + + Raises: + VersionControlError: initalising the repository failed. + """ + try: + return git.Repo.init(str(path)) + except git.exc.GitCommandError as err: + raise VersionControlError(f"Failed to initialise git repository at path '{path}'. Error from VCS: {err}") + + +def get_repo(path: Path) -> git.Repo: + """Get a git.Repo object from an existing repository path. + + Args: + path: Path to the git repository. + + Returns: + git.Repo object. + + Raises: + VersionControlError: No valid git repository at this path. + """ + try: + return git.Repo(str(path)) + except git.exc.InvalidGitRepositoryError: + raise VersionControlError( + "Could not find a valid git repository at this path. Please perform a `git init` command." + ) + + +def get_default_branch(repo: git.Repo) -> str: + """Get a default branch from an existing git.Repo. + + Args: + repo: git.Repo object + + Returns: + The default branch name as a string. + + Raises: + VersionControlError: Could not find the default branch name. + """ + try: + return str(repo.git.symbolic_ref("refs/remotes/origin/HEAD").rsplit("/", maxsplit=1)[-1]) + except git.exc.GitCommandError as err: + raise VersionControlError(f"Could not resolve default repository branch name. Error from VCS: {err}") diff --git a/tools/python/mbed_tools/project/_internal/libraries.py b/tools/python/mbed_tools/project/_internal/libraries.py new file mode 100644 index 0000000000..8c3f1a9944 --- /dev/null +++ b/tools/python/mbed_tools/project/_internal/libraries.py @@ -0,0 +1,133 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Objects for library reference handling.""" +import logging + +from dataclasses import dataclass +from pathlib import Path +from typing import Generator, List + +from mbed_tools.project._internal import git_utils +from mbed_tools.project.exceptions import VersionControlError + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True, order=True) +class MbedLibReference: + """Metadata associated with an Mbed library. + + An Mbed library is an external dependency of an MbedProgram. The MbedProgram is made aware of the library + dependency by the presence of a .lib file in the project tree, which we refer to as a library reference file. The + library reference file contains a URI where the dependency's source code can be fetched. + + Attributes: + reference_file: Path to the .lib reference file for this library. + source_code_path: Path to the source code if it exists in the local project. + """ + + reference_file: Path + source_code_path: Path + + def is_resolved(self) -> bool: + """Determines if the source code for this library is present in the source tree.""" + return self.source_code_path.exists() and self.source_code_path.is_dir() + + def get_git_reference(self) -> git_utils.GitReference: + """Get the source code location from the library reference file. + + Returns: + Data structure containing the contents of the library reference file. + """ + raw_ref = self.reference_file.read_text().strip() + url, sep, ref = raw_ref.partition("#") + + if url.endswith("/"): + url = url[:-1] + + return git_utils.GitReference(repo_url=url, ref=ref) + + +@dataclass +class LibraryReferences: + """Manages library references in an MbedProgram.""" + + root: Path + ignore_paths: List[str] + + def fetch(self) -> None: + """Recursively clone all dependencies defined in .lib files.""" + for lib in self.iter_unresolved(): + git_ref = lib.get_git_reference() + logger.info(f"Resolving library reference {git_ref.repo_url}.") + _clone_at_ref(git_ref.repo_url, lib.source_code_path, git_ref.ref) + + # Check if we find any new references after cloning dependencies. + if list(self.iter_unresolved()): + self.fetch() + + def checkout(self, force: bool) -> None: + """Check out all resolved libs to revision specified in .lib files.""" + for lib in self.iter_resolved(): + repo = git_utils.get_repo(lib.source_code_path) + git_ref = lib.get_git_reference() + + if not git_ref.ref: + git_ref.ref = git_utils.get_default_branch(repo) + + git_utils.fetch(repo, git_ref.ref) + git_utils.checkout(repo, "FETCH_HEAD", force=force) + + def iter_all(self) -> Generator[MbedLibReference, None, None]: + """Iterate all library references in the tree. + + Yields: + Iterator to library reference. + """ + for lib in self.root.rglob("*.lib"): + if not self._in_ignore_path(lib): + yield MbedLibReference(lib, lib.with_suffix("")) + + def iter_unresolved(self) -> Generator[MbedLibReference, None, None]: + """Iterate all unresolved library references in the tree. + + Yields: + Iterator to library reference. + """ + for lib in self.iter_all(): + if not lib.is_resolved(): + yield lib + + def iter_resolved(self) -> Generator[MbedLibReference, None, None]: + """Iterate all resolved library references in the tree. + + Yields: + Iterator to library reference. + """ + for lib in self.iter_all(): + if lib.is_resolved(): + yield lib + + def _in_ignore_path(self, lib_reference_path: Path) -> bool: + """Check if a library reference is in a path we want to ignore.""" + return any(p in lib_reference_path.parts for p in self.ignore_paths) + + +def _clone_at_ref(url: str, path: Path, ref: str) -> None: + if ref: + logger.info(f"Checking out revision {ref} for library {url}.") + try: + git_utils.clone(url, path, ref) + except VersionControlError: + # We weren't able to clone. Try again without the ref. + repo = git_utils.clone(url, path) + # We couldn't clone the ref and had to fall back to cloning + # just the default branch. Fetch the ref before checkout, so + # that we have it available locally. + logger.warning(f"No tag or branch with name {ref}. Fetching full repository.") + git_utils.fetch(repo, ref) + git_utils.checkout(repo, "FETCH_HEAD") + else: + git_utils.clone(url, path) diff --git a/tools/python/mbed_tools/project/_internal/progress.py b/tools/python/mbed_tools/project/_internal/progress.py new file mode 100644 index 0000000000..7ad4d5de02 --- /dev/null +++ b/tools/python/mbed_tools/project/_internal/progress.py @@ -0,0 +1,60 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Progress bar for git operations.""" +import sys + +from typing import Optional, Any + +from git import RemoteProgress +from tqdm import tqdm + + +class ProgressBar(tqdm): + """tqdm progress bar that can be used in a callback context.""" + + total: Any + + def update_progress(self, block_num: float = 1, block_size: float = 1, total_size: float = None) -> None: + """Update the progress bar. + + Args: + block_num: Number of the current block. + block_size: Size of the current block. + total_size: Total size of all expected blocks. + """ + if total_size is not None and self.total != total_size: + self.total = total_size + self.update(block_num * block_size - self.n) + + +class ProgressReporter(RemoteProgress): + """GitPython RemoteProgress subclass that displays a progress bar for git fetch and push operations.""" + + def __init__(self, *args: Any, name: str = "", **kwargs: Any) -> None: + """Initialiser. + + Args: + name: The name of the git repository to report progress on. + """ + self.name = name + super().__init__(*args, **kwargs) + + def update(self, op_code: int, cur_count: float, max_count: Optional[float] = None, message: str = "") -> None: + """Called whenever the progress changes. + + Args: + op_code: Integer describing the stage of the current operation. + cur_count: Current item count. + max_count: Maximum number of items expected. + message: Message string describing the number of bytes transferred in the WRITING operation. + """ + if self.BEGIN & op_code: + self.bar = ProgressBar(total=max_count, file=sys.stderr, leave=False) + + self.bar.desc = f"{self.name} {self._cur_line}" + self.bar.update_progress(block_num=cur_count, total_size=max_count) + + if self.END & op_code: + self.bar.close() diff --git a/tools/python/mbed_tools/project/_internal/project_data.py b/tools/python/mbed_tools/project/_internal/project_data.py new file mode 100644 index 0000000000..92e1c85abb --- /dev/null +++ b/tools/python/mbed_tools/project/_internal/project_data.py @@ -0,0 +1,159 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Objects representing Mbed program and library data.""" +import json +import logging + +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +from mbed_tools.project._internal.render_templates import ( + render_cmakelists_template, + render_main_cpp_template, + render_gitignore_template, +) + +logger = logging.getLogger(__name__) + +# Mbed program file names and constants. +APP_CONFIG_FILE_NAME = "mbed_app.json" +BUILD_DIR = "cmake_build" +CMAKELISTS_FILE_NAME = "CMakeLists.txt" +MAIN_CPP_FILE_NAME = "main.cpp" +MBED_OS_REFERENCE_FILE_NAME = "mbed-os.lib" +MBED_OS_DIR_NAME = "mbed-os" +TARGETS_JSON_FILE_PATH = Path("targets", "targets.json") +CUSTOM_TARGETS_JSON_FILE_NAME = "custom_targets.json" + +# Information written to mbed-os.lib +MBED_OS_REFERENCE_URL = "https://github.com/ARMmbed/mbed-os" +MBED_OS_REFERENCE_ID = "master" + +# For some reason Mbed OS expects the default mbed_app.json to contain some target_overrides +# for the K64F target. TODO: Find out why this wouldn't be defined in targets.json. +DEFAULT_APP_CONFIG = {"target_overrides": {"K64F": {"platform.stdio-baud-rate": 9600}}} + + +@dataclass +class MbedProgramFiles: + """Files defining an MbedProgram. + + This object holds paths to the various files which define an MbedProgram. + + MbedPrograms must contain an mbed-os.lib reference file, defining Mbed OS as a program dependency. A program can + optionally include an mbed_app.json file which defines application level config. + + Attributes: + app_config_file: Path to mbed_app.json file. This can be `None` if the program doesn't set any custom config. + mbed_os_ref: Library reference file for MbedOS. All programs require this file. + cmakelists_file: A top-level CMakeLists.txt containing build definitions for the application. + cmake_build_dir: The CMake build tree. + """ + + app_config_file: Optional[Path] + mbed_os_ref: Path + cmakelists_file: Path + cmake_build_dir: Path + custom_targets_json: Path + + @classmethod + def from_new(cls, root_path: Path) -> "MbedProgramFiles": + """Create MbedProgramFiles from a new directory. + + A "new directory" in this context means it doesn't already contain an Mbed program. + + Args: + root_path: The directory in which to create the program data files. + + Raises: + ValueError: A program .mbed or mbed-os.lib file already exists at this path. + """ + app_config = root_path / APP_CONFIG_FILE_NAME + mbed_os_ref = root_path / MBED_OS_REFERENCE_FILE_NAME + cmakelists_file = root_path / CMAKELISTS_FILE_NAME + main_cpp = root_path / MAIN_CPP_FILE_NAME + gitignore = root_path / ".gitignore" + cmake_build_dir = root_path / BUILD_DIR + custom_targets_json = root_path / CUSTOM_TARGETS_JSON_FILE_NAME + + if mbed_os_ref.exists(): + raise ValueError(f"Program already exists at path {root_path}.") + + app_config.write_text(json.dumps(DEFAULT_APP_CONFIG, indent=4)) + mbed_os_ref.write_text(f"{MBED_OS_REFERENCE_URL}#{MBED_OS_REFERENCE_ID}") + render_cmakelists_template(cmakelists_file, root_path.stem) + render_main_cpp_template(main_cpp) + render_gitignore_template(gitignore) + return cls( + app_config_file=app_config, + mbed_os_ref=mbed_os_ref, + cmakelists_file=cmakelists_file, + cmake_build_dir=cmake_build_dir, + custom_targets_json=custom_targets_json, + ) + + @classmethod + def from_existing(cls, root_path: Path, build_subdir: Path) -> "MbedProgramFiles": + """Create MbedProgramFiles from a directory containing an existing program. + + Args: + root_path: The path containing the MbedProgramFiles. + build_subdir: The subdirectory of BUILD_DIR to use for CMake build. + """ + app_config: Optional[Path] + app_config = root_path / APP_CONFIG_FILE_NAME + if not app_config.exists(): + logger.info("This program does not contain an mbed_app.json config file.") + app_config = None + + custom_targets_json = root_path / CUSTOM_TARGETS_JSON_FILE_NAME + mbed_os_file = root_path / MBED_OS_REFERENCE_FILE_NAME + + cmakelists_file = root_path / CMAKELISTS_FILE_NAME + if not cmakelists_file.exists(): + logger.warning("No CMakeLists.txt found in the program root.") + cmake_build_dir = root_path / BUILD_DIR / build_subdir + + return cls( + app_config_file=app_config, + mbed_os_ref=mbed_os_file, + cmakelists_file=cmakelists_file, + cmake_build_dir=cmake_build_dir, + custom_targets_json=custom_targets_json, + ) + + +@dataclass +class MbedOS: + """Metadata associated with a copy of MbedOS. + + This object holds information about MbedOS used by MbedProgram. + + Attributes: + root: The root path of the MbedOS source tree. + targets_json_file: Path to a targets.json file, which contains target data specific to MbedOS revision. + """ + + root: Path + targets_json_file: Path + + @classmethod + def from_existing(cls, root_path: Path, check_root_path_exists: bool = True) -> "MbedOS": + """Create MbedOS from a directory containing an existing MbedOS installation.""" + targets_json_file = root_path / TARGETS_JSON_FILE_PATH + + if check_root_path_exists and not root_path.exists(): + raise ValueError("The mbed-os directory does not exist.") + + if root_path.exists() and not targets_json_file.exists(): + raise ValueError("This MbedOS copy does not contain a targets.json file.") + + return cls(root=root_path, targets_json_file=targets_json_file) + + @classmethod + def from_new(cls, root_path: Path) -> "MbedOS": + """Create MbedOS from an empty or new directory.""" + return cls(root=root_path, targets_json_file=root_path / TARGETS_JSON_FILE_PATH) diff --git a/tools/python/mbed_tools/project/_internal/render_templates.py b/tools/python/mbed_tools/project/_internal/render_templates.py new file mode 100644 index 0000000000..cb8e1043a7 --- /dev/null +++ b/tools/python/mbed_tools/project/_internal/render_templates.py @@ -0,0 +1,56 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Render jinja templates required by the project package.""" +import datetime + +from pathlib import Path + +import jinja2 + +TEMPLATES_DIRECTORY = Path("_internal", "templates") + + +def render_cmakelists_template(cmakelists_file: Path, program_name: str) -> None: + """Render CMakeLists.tmpl with the copyright year and program name as the app target name. + + Args: + cmakelists_file: The path where CMakeLists.txt will be written. + program_name: The name of the program, will be used as the app target name. + """ + cmakelists_file.write_text( + render_jinja_template( + "CMakeLists.tmpl", {"program_name": program_name, "year": str(datetime.datetime.now().year)} + ) + ) + + +def render_main_cpp_template(main_cpp: Path) -> None: + """Render a basic main.cpp which prints a hello message and returns. + + Args: + main_cpp: Path where the main.cpp file will be written. + """ + main_cpp.write_text(render_jinja_template("main.tmpl", {"year": str(datetime.datetime.now().year)})) + + +def render_gitignore_template(gitignore: Path) -> None: + """Write out a basic gitignore file ignoring the build and config directory. + + Args: + gitignore: The path where the gitignore file will be written. + """ + gitignore.write_text(render_jinja_template("gitignore.tmpl", {})) + + +def render_jinja_template(template_name: str, context: dict) -> str: + """Render a jinja template. + + Args: + template_name: The name of the template being rendered. + context: Data to render into the jinja template. + """ + env = jinja2.Environment(loader=jinja2.PackageLoader("mbed_tools.project", str(TEMPLATES_DIRECTORY))) + template = env.get_template(template_name) + return template.render(context) diff --git a/tools/python/mbed_tools/project/_internal/templates/CMakeLists.tmpl b/tools/python/mbed_tools/project/_internal/templates/CMakeLists.tmpl new file mode 100644 index 0000000000..6e1fb1e5b6 --- /dev/null +++ b/tools/python/mbed_tools/project/_internal/templates/CMakeLists.tmpl @@ -0,0 +1,27 @@ +# Copyright (c) {{year}} ARM Limited. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.19.0) + +set(MBED_PATH ${CMAKE_CURRENT_SOURCE_DIR}/mbed-os CACHE INTERNAL "") +set(MBED_CONFIG_PATH ${CMAKE_CURRENT_BINARY_DIR} CACHE INTERNAL "") +set(APP_TARGET {{program_name}}) + +include(${MBED_PATH}/tools/cmake/app.cmake) + +project(${APP_TARGET}) + +add_subdirectory(${MBED_PATH}) + +add_executable(${APP_TARGET} + main.cpp +) + +target_link_libraries(${APP_TARGET} mbed-os) + +mbed_set_post_build(${APP_TARGET}) + +option(VERBOSE_BUILD "Have a verbose build process") +if(VERBOSE_BUILD) + set(CMAKE_VERBOSE_MAKEFILE ON) +endif() diff --git a/tools/python/mbed_tools/project/_internal/templates/__init__.py b/tools/python/mbed_tools/project/_internal/templates/__init__.py new file mode 100644 index 0000000000..6a5e5f9244 --- /dev/null +++ b/tools/python/mbed_tools/project/_internal/templates/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Package containing jinja templates used by mbed_tools.project.""" diff --git a/tools/python/mbed_tools/project/_internal/templates/gitignore.tmpl b/tools/python/mbed_tools/project/_internal/templates/gitignore.tmpl new file mode 100644 index 0000000000..f8fdeb6c7f --- /dev/null +++ b/tools/python/mbed_tools/project/_internal/templates/gitignore.tmpl @@ -0,0 +1,2 @@ +.mbedbuild +cmake_build/ diff --git a/tools/python/mbed_tools/project/_internal/templates/main.tmpl b/tools/python/mbed_tools/project/_internal/templates/main.tmpl new file mode 100644 index 0000000000..baa7dcc90b --- /dev/null +++ b/tools/python/mbed_tools/project/_internal/templates/main.tmpl @@ -0,0 +1,13 @@ +/* mbed Microcontroller Library + * Copyright (c) {{year}} ARM Limited + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "mbed.h" + + +int main() +{ + printf("Hello, Mbed!\n"); + return 0; +} diff --git a/tools/python/mbed_tools/project/exceptions.py b/tools/python/mbed_tools/project/exceptions.py new file mode 100644 index 0000000000..4c4db0b8e0 --- /dev/null +++ b/tools/python/mbed_tools/project/exceptions.py @@ -0,0 +1,27 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Public exceptions exposed by the package.""" + +from mbed_tools.lib.exceptions import ToolsError + + +class MbedProjectError(ToolsError): + """Base exception for mbed-project.""" + + +class VersionControlError(MbedProjectError): + """Raised when a source control management operation failed.""" + + +class ExistingProgram(MbedProjectError): + """Raised when a program already exists at a given path.""" + + +class ProgramNotFound(MbedProjectError): + """Raised when an expected program is not found.""" + + +class MbedOSNotFound(MbedProjectError): + """A valid copy of MbedOS was not found.""" diff --git a/tools/python/mbed_tools/project/mbed_program.py b/tools/python/mbed_tools/project/mbed_program.py new file mode 100644 index 0000000000..c3a95362fe --- /dev/null +++ b/tools/python/mbed_tools/project/mbed_program.py @@ -0,0 +1,171 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Mbed Program abstraction layer.""" +import logging + +from pathlib import Path +from typing import Dict +from urllib.parse import urlparse + +from mbed_tools.project.exceptions import ProgramNotFound, ExistingProgram, MbedOSNotFound +from mbed_tools.project._internal.project_data import ( + MbedProgramFiles, + MbedOS, + MBED_OS_REFERENCE_FILE_NAME, + MBED_OS_DIR_NAME, +) + +logger = logging.getLogger(__name__) + + +class MbedProgram: + """Represents an Mbed program. + + An `MbedProgram` consists of: + * A copy of, or reference to, `MbedOS` + * A set of `MbedProgramFiles` + * A collection of references to external libraries, defined in .lib files located in the program source tree + """ + + def __init__(self, program_files: MbedProgramFiles, mbed_os: MbedOS) -> None: + """Initialise the program attributes. + + Args: + program_files: Object holding paths to a set of files that define an Mbed program. + mbed_os: An instance of `MbedOS` holding paths to locations in the local copy of the Mbed OS source. + """ + self.files = program_files + self.root = self.files.mbed_os_ref.parent + self.mbed_os = mbed_os + + @classmethod + def from_new(cls, dir_path: Path) -> "MbedProgram": + """Create an MbedProgram from an empty directory. + + Creates the directory if it doesn't exist. + + Args: + dir_path: Directory in which to create the program. + + Raises: + ExistingProgram: An existing program was found in the path. + """ + if _tree_contains_program(dir_path): + raise ExistingProgram( + f"An existing Mbed program was found in the directory tree {dir_path}. It is not possible to nest Mbed " + "programs. Please ensure there is no mbed-os.lib file in the cwd hierarchy." + ) + + logger.info(f"Creating Mbed program at path '{dir_path.resolve()}'") + dir_path.mkdir(exist_ok=True) + program_files = MbedProgramFiles.from_new(dir_path) + logger.info(f"Creating git repository for the Mbed program '{dir_path}'") + mbed_os = MbedOS.from_new(dir_path / MBED_OS_DIR_NAME) + return cls(program_files, mbed_os) + + @classmethod + def from_existing( + cls, dir_path: Path, build_subdir: Path, mbed_os_path: Path = None, check_mbed_os: bool = True, + ) -> "MbedProgram": + """Create an MbedProgram from an existing program directory. + + Args: + dir_path: Directory containing an Mbed program. + build_subdir: The subdirectory for the CMake build tree. + mbed_os_path: Directory containing Mbed OS. + check_mbed_os: If True causes an exception to be raised if the Mbed OS source directory does not + exist. + + Raises: + ProgramNotFound: An existing program was not found in the path. + """ + if mbed_os_path is None: + program_root = _find_program_root(dir_path) + mbed_os_path = program_root / MBED_OS_DIR_NAME + else: + program_root = dir_path + + logger.info(f"Found existing Mbed program at path '{program_root}'") + program = MbedProgramFiles.from_existing(program_root, build_subdir) + + try: + mbed_os = MbedOS.from_existing(mbed_os_path, check_mbed_os) + except ValueError as mbed_os_err: + raise MbedOSNotFound( + f"Mbed OS was not found due to the following error: {mbed_os_err}" + "\nYou may need to resolve the mbed-os.lib reference. You can do this by performing a `deploy`." + ) + + return cls(program, mbed_os) + + +def parse_url(name_or_url: str) -> Dict[str, str]: + """Create a valid github/armmbed url from a program name. + + Args: + url: The URL, or a program name to turn into an URL. + + Returns: + Dictionary containing the remote url and the destination path for the clone. + """ + url_obj = urlparse(name_or_url) + if url_obj.hostname: + url = url_obj.geturl() + elif ":" in name_or_url.split("/", maxsplit=1)[0]: + # If non-standard and no slashes before first colon, git will recognize as scp ssh syntax + url = name_or_url + else: + url = f"https://github.com/armmbed/{url_obj.path}" + # We need to create a valid directory name from the url path section. + return {"url": url, "dst_path": url_obj.path.rsplit("/", maxsplit=1)[-1].replace("/", "")} + + +def _tree_contains_program(path: Path) -> bool: + """Check if the current path or its ancestors contain an mbed-os.lib file. + + Args: + path: The starting path for the search. The search walks up the tree from this path. + + Returns: + `True` if an mbed-os.lib file is located between `path` and filesystem root. + `False` if no mbed-os.lib file was found. + """ + try: + _find_program_root(path) + return True + except ProgramNotFound: + return False + + +def _find_program_root(cwd: Path) -> Path: + """Walk up the directory tree, looking for an mbed-os.lib file. + + Programs contain an mbed-os.lib file at the root of the source tree. + + Args: + cwd: The directory path to search for a program. + + Raises: + ProgramNotFound: No mbed-os.lib file found in the path. + + Returns: + Path containing the mbed-os.lib file. + """ + potential_root = cwd.absolute().resolve() + while str(potential_root) != str(potential_root.anchor): + logger.debug(f"Searching for mbed-os.lib file at path {potential_root}") + root_file = potential_root / MBED_OS_REFERENCE_FILE_NAME + if root_file.exists() and root_file.is_file(): + logger.debug(f"mbed-os.lib file found at {potential_root}") + return potential_root + + potential_root = potential_root.parent + + logger.debug("No mbed-os.lib file found.") + raise ProgramNotFound( + f"No program found from {cwd.resolve()} to {cwd.resolve().anchor}. Please set the directory to a program " + "directory containing an mbed-os.lib file. You can also set the directory to a program subdirectory if there " + "is an mbed-os.lib file at the root of your program's directory tree." + ) diff --git a/tools/python/mbed_tools/project/project.py b/tools/python/mbed_tools/project/project.py new file mode 100644 index 0000000000..df7fedda85 --- /dev/null +++ b/tools/python/mbed_tools/project/project.py @@ -0,0 +1,85 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Defines the public API of the package.""" +import pathlib +import logging + +from typing import List, Any + +from mbed_tools.project.mbed_program import MbedProgram, parse_url +from mbed_tools.project._internal.libraries import LibraryReferences +from mbed_tools.project._internal import git_utils + +logger = logging.getLogger(__name__) + + +def import_project(url: str, dst_path: Any = None, recursive: bool = False) -> pathlib.Path: + """Clones an Mbed project from a remote repository. + + Args: + url: URL of the repository to clone. + dst_path: Destination path for the repository. + recursive: Recursively clone all project dependencies. + + Returns: + The path the project was cloned to. + """ + git_data = parse_url(url) + url = git_data["url"] + if not dst_path: + dst_path = pathlib.Path(git_data["dst_path"]) + + git_utils.clone(url, dst_path) + if recursive: + libs = LibraryReferences(root=dst_path, ignore_paths=["mbed-os"]) + libs.fetch() + + return dst_path + + +def initialise_project(path: pathlib.Path, create_only: bool) -> None: + """Create a new Mbed project, optionally fetching and adding mbed-os. + + Args: + path: Path to the project folder. Created if it doesn't exist. + create_only: Flag which suppreses fetching mbed-os. If the value is `False`, fetch mbed-os from the remote. + """ + program = MbedProgram.from_new(path) + if not create_only: + libs = LibraryReferences(root=program.root, ignore_paths=["mbed-os"]) + libs.fetch() + + +def deploy_project(path: pathlib.Path, force: bool = False) -> None: + """Deploy a specific revision of the current Mbed project. + + This function also resolves and syncs all library dependencies to the revision specified in the library reference + files. + + Args: + path: Path to the Mbed project. + force: Force overwrite uncommitted changes. If False, the deploy will fail if there are uncommitted local + changes. + """ + libs = LibraryReferences(path, ignore_paths=["mbed-os"]) + libs.checkout(force=force) + if list(libs.iter_unresolved()): + logger.info("Unresolved libraries detected, downloading library source code.") + libs.fetch() + + +def get_known_libs(path: pathlib.Path) -> List: + """List all resolved library dependencies. + + This function will not resolve dependencies. This will only generate a list of resolved dependencies. + + Args: + path: Path to the Mbed project. + + Returns: + A list of known dependencies. + """ + libs = LibraryReferences(path, ignore_paths=["mbed-os"]) + return list(sorted(libs.iter_resolved())) diff --git a/tools/python/mbed_tools/sterm/__init__.py b/tools/python/mbed_tools/sterm/__init__.py new file mode 100644 index 0000000000..4a25a754bc --- /dev/null +++ b/tools/python/mbed_tools/sterm/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Package containing sterm functionality.""" diff --git a/tools/python/mbed_tools/sterm/terminal.py b/tools/python/mbed_tools/sterm/terminal.py new file mode 100644 index 0000000000..cfe5e85c97 --- /dev/null +++ b/tools/python/mbed_tools/sterm/terminal.py @@ -0,0 +1,134 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Serial terminal implementation based on pyserial.tools.miniterm. + +The Mbed serial terminal makes the following modifications to the default Miniterm. +* Custom help menu text +* Constrained set of menu keys +* CTRL-H to show help +* CTRL-B sends serial break to the target + +To start the terminal clients should call the "run" function, this is the entry point to the module. +""" +from typing import Any + +from serial import Serial +from serial.tools.miniterm import Miniterm + + +def run(port: str, baud: int, echo: bool = True) -> None: + """Run the serial terminal. + + This function is blocking as it waits for the terminal thread to finish executing before returning. + + Args: + port: The serial port to open a terminal on. + baud: Serial baud rate. + echo: Echo user input back to the console. + """ + term = SerialTerminal(Serial(port=port, baudrate=str(baud)), echo=echo) + term.start() + + try: + term.join(True) + except KeyboardInterrupt: + pass + finally: + term.join() + term.close() + + +class SerialTerminal(Miniterm): + """An implementation of Miniterm that implements the additional Mbed terminal functionality. + + Overrides the `writer` method to implement modified menu key handling behaviour. + Overrides the Miniterm::get_help_text method to return the Mbed custom help text. + Adds a `reset` method so users can send a reset signal to the device. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Set the rx/tx encoding and special characters.""" + super().__init__(*args, **kwargs) + self.exit_character = CTRL_C + self.menu_character = CTRL_T + self.reset_character = CTRL_B + self.help_character = CTRL_H + self.set_rx_encoding("UTF-8") + self.set_tx_encoding("UTF-8") + + def reset(self) -> None: + """Send a reset signal.""" + self.serial.sendBreak() + + def get_help_text(self) -> str: + """Return the text displayed when the user requests help.""" + return HELP_TEXT + + def writer(self) -> None: + """Implements terminal behaviour.""" + menu_active = False + while self.alive: + try: + input_key = self.console.getkey() + except KeyboardInterrupt: + input_key = self.exit_character + + if (menu_active and input_key in VALID_MENU_KEYS) or (input_key == self.help_character): + self.handle_menu_key(input_key) + menu_active = False + + elif input_key == self.menu_character: + menu_active = True + + elif input_key == self.reset_character: + self.reset() + + elif input_key == self.exit_character: + self.stop() + break + + else: + self._write_transformed_char(input_key) + + if self.echo: + self._echo_transformed_char(input_key) + + def _write_transformed_char(self, text: str) -> None: + for transformation in self.tx_transformations: + text = transformation.tx(text) + + self.serial.write(self.tx_encoder.encode(text)) + + def _echo_transformed_char(self, text: str) -> None: + for transformation in self.tx_transformations: + text = transformation.echo(text) + + self.console.write(text) + + +CTRL_B = "\x02" +CTRL_C = "\x03" +CTRL_H = "\x08" +CTRL_T = "\x14" +VALID_MENU_KEYS = ("p", "b", "\t", "\x01", "\x04", "\x05", "\x06", "\x0c", CTRL_C, CTRL_T) +HELP_TEXT = """--- Mbed Serial Terminal +--- Based on miniterm from pySerial +--- +--- Ctrl+b Send Break (reset target) +--- Ctrl+c Exit terminal +--- Ctrl+h Help +--- Ctrl+t Menu escape key, followed by: +--- p Change COM port +--- b Change baudrate +--- Tab Show detailed terminal info +--- Ctrl+a Change encoding (default UTF-8) +--- Ctrl+f Edit filters +--- Ctrl+e Toggle local echo +--- Ctrl+l Toggle EOL +--- Ctrl+r Toggle RTS +--- Ctrl+d Toggle DTR +--- Ctrl+c Send control character to remote +--- Ctrl+t Send control character to remote +""" diff --git a/tools/python/mbed_tools/targets/__init__.py b/tools/python/mbed_tools/targets/__init__.py new file mode 100644 index 0000000000..e6ba80a75b --- /dev/null +++ b/tools/python/mbed_tools/targets/__init__.py @@ -0,0 +1,33 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""An abstraction layer describing hardware supported by Mbed OS. + +Querying board database +----------------------- + +For the interface to query board database, look at `mbed_tools.targets.get_board`. + +Fetching target data +____________________ + +For the interface to extract target data from their definitions in Mbed OS, +look at `mbed_tools.targets.get_target`. + +Configuration +------------- + +For details about configuration of this module, look at `mbed_tools.targets.config`. +""" +from mbed_tools.targets import exceptions +from mbed_tools.targets.get_target import ( + get_target_by_name, + get_target_by_board_type, +) +from mbed_tools.targets.get_board import ( + get_board_by_product_code, + get_board_by_online_id, + get_board_by_jlink_slug, +) +from mbed_tools.targets.board import Board diff --git a/tools/python/mbed_tools/targets/_internal/__init__.py b/tools/python/mbed_tools/targets/_internal/__init__.py new file mode 100644 index 0000000000..868c3a2950 --- /dev/null +++ b/tools/python/mbed_tools/targets/_internal/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Code not to be accessed by external applications.""" diff --git a/tools/python/mbed_tools/targets/_internal/board_database.py b/tools/python/mbed_tools/targets/_internal/board_database.py new file mode 100644 index 0000000000..d909a0a53c --- /dev/null +++ b/tools/python/mbed_tools/targets/_internal/board_database.py @@ -0,0 +1,115 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Internal helper to retrieve target information from the online database.""" + +import pathlib +from http import HTTPStatus +import json +from json.decoder import JSONDecodeError +import logging +from typing import List, Optional, Dict, Any + +import requests + +from mbed_tools.targets._internal.exceptions import ResponseJSONError, BoardAPIError + +from mbed_tools.targets.env import env + + +INTERNAL_PACKAGE_DIR = pathlib.Path(__file__).parent +SNAPSHOT_FILENAME = "board_database_snapshot.json" + +logger = logging.getLogger(__name__) + + +def get_board_database_path() -> pathlib.Path: + """Return the path to the offline board database.""" + return pathlib.Path(INTERNAL_PACKAGE_DIR, "data", SNAPSHOT_FILENAME) + + +_BOARD_API = "https://os.mbed.com/api/v4/targets" + + +def get_offline_board_data() -> Any: + """Loads board data from JSON stored in offline snapshot. + + Returns: + The board database as retrieved from the local database snapshot. + + Raises: + ResponseJSONError: error decoding the local database JSON. + """ + boards_snapshot_path = get_board_database_path() + try: + return json.loads(boards_snapshot_path.read_text()) + except JSONDecodeError as json_err: + raise ResponseJSONError(f"Invalid JSON received from '{boards_snapshot_path}'.") from json_err + + +def get_online_board_data() -> List[dict]: + """Retrieves board data from the online API. + + Returns: + The board database as retrieved from the boards API + + Raises: + ResponseJSONError: error decoding the response JSON. + BoardAPIError: error retrieving data from the board API. + """ + board_data: List[dict] = [{}] + response = _get_request() + if response.status_code != HTTPStatus.OK: + warning_msg = _response_error_code_to_str(response) + logger.warning(warning_msg) + logger.debug(f"Response received from API:\n{response.text}") + raise BoardAPIError(warning_msg) + + try: + json_data = response.json() + except JSONDecodeError as json_err: + warning_msg = f"Invalid JSON received from '{_BOARD_API}'." + logger.warning(warning_msg) + logger.debug(f"Response received from API:\n{response.text}") + raise ResponseJSONError(warning_msg) from json_err + + try: + board_data = json_data["data"] + except KeyError as key_err: + warning_msg = f"JSON received from '{_BOARD_API}' is missing the 'data' field." + logger.warning(warning_msg) + keys_found = ", ".join(json_data.keys()) + logger.debug(f"Fields found in JSON Response: {keys_found}") + raise ResponseJSONError(warning_msg) from key_err + + return board_data + + +def _response_error_code_to_str(response: requests.Response) -> str: + if response.status_code == HTTPStatus.UNAUTHORIZED: + return ( + f"Authentication failed for '{_BOARD_API}'. Please check that the environment variable " + f"'MBED_API_AUTH_TOKEN' is correctly configured with a private access token." + ) + else: + return f"An HTTP {response.status_code} was received from '{_BOARD_API}'." + + +def _get_request() -> requests.Response: + """Make a GET request to the API, ensuring the correct headers are set.""" + header: Optional[Dict[str, str]] = None + mbed_api_auth_token = env.MBED_API_AUTH_TOKEN + if mbed_api_auth_token: + header = {"Authorization": f"Bearer {mbed_api_auth_token}"} + + try: + return requests.get(_BOARD_API, headers=header) + except requests.exceptions.ConnectionError as connection_error: + if isinstance(connection_error, requests.exceptions.SSLError): + logger.warning("Unable to verify an SSL certificate with requests.") + elif isinstance(connection_error, requests.exceptions.ProxyError): + logger.warning("Failed to connect to proxy. Please check your proxy configuration.") + + logger.warning("Unable to connect to the online database. Please check your internet connection.") + raise BoardAPIError("Failed to connect to the online database.") from connection_error diff --git a/tools/python/mbed_tools/targets/_internal/data/board_database_snapshot.json b/tools/python/mbed_tools/targets/_internal/data/board_database_snapshot.json new file mode 100644 index 0000000000..55339905df --- /dev/null +++ b/tools/python/mbed_tools/targets/_internal/data/board_database_snapshot.json @@ -0,0 +1,7011 @@ +[ + { + "board_type": "MTB_UBLOX_NINA_B1", + "board_name": "u-blox NINA-B1", + "product_code": "0455", + "target_type": "module", + "slug": "u-blox-nina-b1", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "MTB_MTS_XDOT", + "board_name": "Multitech xDOT", + "product_code": "0453", + "target_type": "module", + "slug": "multitech-xdot", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "ADV_WISE_1510", + "board_name": "Advantech WISE-1510", + "product_code": "0458", + "target_type": "module", + "slug": "advantech-wise-1510", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "MTS_DRAGONFLY_F411RE", + "board_name": "Multitech Dragonfly", + "product_code": "0454", + "target_type": "module", + "slug": "multitech-dragonfly", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "ADV_WISE_1530", + "board_name": "Advantech WISE-1530", + "product_code": "0459", + "target_type": "module", + "slug": "advantech-wise-1530", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "MTB_UBLOX_ODIN_W2", + "board_name": "u-blox ODIN-W2", + "product_code": "0450", + "target_type": "module", + "slug": "u-blox-odin-w2", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "MTB_ADV_WISE_1570", + "board_name": "Advantech WISE-1570", + "product_code": "0460", + "target_type": "module", + "slug": "advantech-wise-1570", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "MTB_MXCHIP_EMW3166", + "board_name": "MXChip EMW3166", + "product_code": "0451", + "target_type": "module", + "slug": "mxchip-emw3166", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "MTB_MURATA_ABZ", + "board_name": "Murata Type ABZ", + "product_code": "0456", + "target_type": "module", + "slug": "murata-type-abz", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "EP_AGORA", + "board_name": "Embedded Planet Agora", + "product_code": "2600", + "target_type": "module", + "slug": "ep-agora", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "MAX32670EVKIT", + "board_name": "MAX32670EVKIT", + "product_code": "0424", + "target_type": "platform", + "slug": "MAX32670EVKIT", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_U535RE_Q", + "board_name": "NUCLEO-U535RE-Q", + "product_code": "0838", + "target_type": "platform", + "slug": "ST-Nucleo-U535RE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_WBA55CG", + "board_name": "NUCLEO-WBA55CG", + "product_code": "0837", + "target_type": "platform", + "slug": "ST-Nucleo-WBA55CG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUMAKER_IOT_M467", + "board_name": "NuMaker-IoT-M467", + "product_code": "1313", + "target_type": "platform", + "slug": "NUMAKER-IOT-M467", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "MIMXRT1040_EVK", + "board_name": "MIMXRT1040-EVK", + "product_code": "0255", + "target_type": "platform", + "slug": "MIMXRT1040-EVK", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_H503RB", + "board_name": "NUCLEO-H503RB", + "product_code": "0832", + "target_type": "platform", + "slug": "ST-Nucleo-H503RB", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_H573II", + "board_name": "DISCO-H573II", + "product_code": "0831", + "target_type": "platform", + "slug": "ST-Discovery-H573II", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "MAX32660EVSYS", + "board_name": "MAX32660EVSYS", + "product_code": "0421", + "target_type": "platform", + "slug": "MAX32660EVSYS", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "MTS_DRAGONFLY_L496VG", + "board_name": "Multitech Dragonfly L496VG", + "product_code": "0313", + "target_type": "platform", + "slug": "MTS-DRAGONFLY-L496VG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.11" + ], + "mbed_enabled": [] + }, + { + "board_type": "B_G473E_ZEST1S", + "board_name": "B-G473E-ZEST1S", + "product_code": "0889", + "target_type": "platform", + "slug": "ST-B-G473E-ZEST1S", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "TMPM4NR", + "board_name": "AdBun-M4NR development board", + "product_code": "7022", + "target_type": "platform", + "slug": "TMPM4NR", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "TMPM4GR", + "board_name": "AdBun-M4GR", + "product_code": "7021", + "target_type": "platform", + "slug": "TMPM4GR", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_H563ZI", + "board_name": "NUCLEO-H563ZI", + "product_code": "0878", + "target_type": "platform", + "slug": "ST-Nucleo-H563ZI", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_U595ZJQ", + "board_name": "NUCLEO-U595ZJ-Q", + "product_code": "0877", + "target_type": "platform", + "slug": "ST-Nucleo-U595ZJ-Q", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_C0316", + "board_name": "DISCO-C0316", + "product_code": "0869", + "target_type": "platform", + "slug": "ST-Discovery-C0316", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_C0116", + "board_name": "DISCO-C0116", + "product_code": "0868", + "target_type": "platform", + "slug": "ST-Discovery-C0116", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_C031C6", + "board_name": "NUCLEO-C031C6", + "product_code": "0867", + "target_type": "platform", + "slug": "ST-Nucleo-C031C6", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "ARM_MPS2_M55", + "board_name": "arm_mps2_m55", + "product_code": "5010", + "target_type": "platform", + "slug": "arm_mps2_m55", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "TMPM4KN", + "board_name": "SBK-M4KN development board", + "product_code": "7020", + "target_type": "platform", + "slug": "TMPM4KN", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "TMPM3HQA", + "board_name": "AdBun-M3HQA development board", + "product_code": "7019", + "target_type": "platform", + "slug": "TMPM3HQA", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.6" + ], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_U599J", + "board_name": "ST Discovery STM32U599J", + "product_code": "0888", + "target_type": "platform", + "slug": "ST-Discovery-STM32U599J", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "EP_ATLAS", + "board_name": "Atlas Product Development Kit", + "product_code": "2603", + "target_type": "platform", + "slug": "ATLAS-DEV", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "MIMXRT1180_EVK", + "board_name": "MIMXRT1180-EVK", + "product_code": "0239", + "target_type": "platform", + "slug": "MIMXRT1180-EVK", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "MIMXRT1024_EVK", + "board_name": "MIMXRT1024-EVK", + "product_code": "0238", + "target_type": "platform", + "slug": "MIMXRT1024-EVK", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "B_U585I_IOT02A", + "board_name": "B-U585I-IOT02A", + "product_code": "0887", + "target_type": "platform", + "slug": "ST-Discovery-B-U585I-IOT02A", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "S1SBP6A", + "board_name": "SIDK S1SBP6A Bio-processor dev kit", + "product_code": "3703", + "target_type": "platform", + "slug": "S1SBP6A", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L010RB", + "board_name": "NUCLEO_L010RB", + "product_code": "0783", + "target_type": "platform", + "slug": "NUCLEO_L010RB", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_U575ZI_Q", + "board_name": "NUCLEO-U575ZI-Q", + "product_code": "0886", + "target_type": "platform", + "slug": "NUCLEO-U575ZI-Q", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "B_L4S5I_IOT01A", + "board_name": "DISCO-L4S5I (B-L4S5I-IOT01A)", + "product_code": "0885", + "target_type": "platform", + "slug": "B-L4S5I-IOT01A", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NU_M2354", + "board_name": "NUMAKER-M2354", + "product_code": "1312", + "target_type": "platform", + "slug": "NUMAKER-M2354", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.15", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "DISCO_WB5MMG", + "board_name": "DISCO-WB5MMG", + "product_code": "0884", + "target_type": "platform", + "slug": "DISCO-WB5MMG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_WB15CC", + "board_name": "NUCLEO-WB15CC", + "product_code": "0883", + "target_type": "platform", + "slug": "ST-NUCLEO-WB15CC", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_G491RE", + "board_name": "NUCLEO-G491RE", + "product_code": "0882", + "target_type": "platform", + "slug": "ST-NUCLEO-G491RE", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "STM32WL55C_DK", + "board_name": "STM32WL55C-DK", + "product_code": "0881", + "target_type": "platform", + "slug": "ST-STM32WL55C-DK", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "B_L462E_CELL1", + "board_name": "B-L462E-CELL1", + "product_code": "0880", + "target_type": "platform", + "slug": "ST-B-L462E-CELL1", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "CY8CKIT064B0S2_4343W", + "board_name": "PSoC 64 Secure Boot, Wi-Fi / BLE Pioneer Kit", + "product_code": "1910", + "target_type": "platform", + "slug": "CY8CKIT-064B0S2-4343W", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "HANI_IOT", + "board_name": "HANI-IOT", + "product_code": "0360", + "target_type": "platform", + "slug": "HANI-IOT", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "TMPM3U6", + "board_name": "AdBun-M3U6", + "product_code": "7018", + "target_type": "platform", + "slug": "TMPM3U6", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "TMPM3V6", + "board_name": "AdBun-M3V6", + "product_code": "7017", + "target_type": "platform", + "slug": "TMPM3V6", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "TMPM4K4", + "board_name": "SBK-M4K4 MB", + "product_code": "7016", + "target_type": "platform", + "slug": "TMPM4K4", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "GR_MANGO", + "board_name": "GR-MANGO", + "product_code": "5502", + "target_type": "platform", + "slug": "Renesas-GR-MANGO", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "FRDM-K32L3A6", + "board_name": "FRDM-K32L3A6", + "product_code": "0237", + "target_type": "platform", + "slug": "FRDM-K32L3A6", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "EP_AGORA", + "board_name": "Agora Product Development Kit", + "product_code": "2600", + "target_type": "platform", + "slug": "AGORA-DEV", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "DISCO_H735G", + "board_name": "Disco-H735G", + "product_code": "0875", + "target_type": "platform", + "slug": "Disco-H735G", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_G0B1RE", + "board_name": "Nucleo-G0B1RE", + "product_code": "0872", + "target_type": "platform", + "slug": "Nucleo-G0B1RE", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_H723ZG", + "board_name": "Nucleo-H723ZG", + "product_code": "0871", + "target_type": "platform", + "slug": "Nucleo-H723ZG", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "WIO_R410", + "board_name": "Seeed Wio SARA-R410", + "product_code": "9016", + "target_type": "platform", + "slug": "Seeed-Wio-SARA-R410", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUMAKER_IOT_M263A", + "board_name": "NuMaker-IoT-M263A", + "product_code": "1310", + "target_type": "platform", + "slug": "NUMAKER-IOT-M263A", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUMAKER_IOT_M252", + "board_name": "NuMaker-LoRaD-M252", + "product_code": "1309", + "target_type": "platform", + "slug": "NuMaker-LoRaD-M252", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.15", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_WL55JC", + "board_name": "NUCLEO-WL55JC", + "product_code": "0866", + "target_type": "platform", + "slug": "ST-Nucleo-WL55JC", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L4P5ZG", + "board_name": "NUCLEO-L4P5ZG", + "product_code": "0865", + "target_type": "platform", + "slug": "ST-Nucleo-L4P5ZG", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "B_G474E_DPOW1", + "board_name": "B-G474E-DPOW1", + "product_code": "0864", + "target_type": "platform", + "slug": "ST-B-G474E-DPOW1", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_L4P5G", + "board_name": "DISCO-L4P5G", + "product_code": "0863", + "target_type": "platform", + "slug": "ST-Discovery-L4P5G", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "WIO_EMW3166", + "board_name": "Seeed Wio EMW3166", + "product_code": "9017", + "target_type": "platform", + "slug": "Seeed-Wio-WiFi", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "ARM_MUSCA_S1", + "board_name": "Arm V2M-Musca-S1", + "product_code": "5009", + "target_type": "platform", + "slug": "Arm-Musca_S1", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_H743ZI2", + "board_name": "NUCLEO-H743ZI2", + "product_code": "0836", + "target_type": "platform", + "slug": "ST-Nucleo-H743ZI2", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "CY8CKIT_062_BLE", + "board_name": "PSoC 6 BLE Pioneer Kit", + "product_code": "1902", + "target_type": "platform", + "slug": "CY8CKIT-062-BLE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "CY8CPROTO_062_4343W", + "board_name": "PSoC 6 Wi-Fi BT Prototyping Kit", + "product_code": "1901", + "target_type": "platform", + "slug": "CY8CPROTO-062-4343W", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "RHOMBIO_L476DMW1K", + "board_name": "RHOMBIO L476DMW1K", + "product_code": "1500", + "target_type": "platform", + "slug": "RHOMBIO-L476DMW1K", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "CY8CKIT_062_WIFI_BT", + "board_name": "PSoC 6 WiFi-BT Pioneer Kit", + "product_code": "1900", + "target_type": "platform", + "slug": "CY8CKIT-062-WiFi-BT", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "GD32_E103VB", + "board_name": "GD32-E103VB", + "product_code": "1703", + "target_type": "platform", + "slug": "GD32-E103VB", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC55S69_NS", + "board_name": "LPCXpresso55S69", + "product_code": "0236", + "target_type": "platform", + "slug": "LPCXpresso55S69", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.14", + "Mbed OS 5.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "GD32_F450ZI", + "board_name": "GD32-F450ZI", + "product_code": "1702", + "target_type": "platform", + "slug": "GD32-F450ZI", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "GD32_F307VG", + "board_name": "GD32-F307VG", + "product_code": "1701", + "target_type": "platform", + "slug": "GD32-F307VG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "SDP_K1", + "board_name": "SDP-K1", + "product_code": "0604", + "target_type": "platform", + "slug": "SDP_K1", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_H7B3ZI", + "board_name": "NUCLEO-H7B3ZI", + "product_code": "0862", + "target_type": "platform", + "slug": "ST-Nucleo-H7B3ZI", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_H7A3ZI", + "board_name": "NUCLEO-H7A3ZI", + "product_code": "0861", + "target_type": "platform", + "slug": "ST-Nucleo-H7A3ZI", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_H7A3ZI_Q", + "board_name": "NUCLEO-H7A3ZI-Q", + "product_code": "0860", + "target_type": "platform", + "slug": "ST-Nucleo-H7A3ZI-Q", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_H7B3I", + "board_name": "DISCO-H7B3I", + "product_code": "0859", + "target_type": "platform", + "slug": "ST-Discovery-H7B3I", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_H750B", + "board_name": "DISCO-H750B", + "product_code": "0858", + "target_type": "platform", + "slug": "ST-Discovery-H750B", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_F7508", + "board_name": "DISCO-F7508", + "product_code": "0857", + "target_type": "platform", + "slug": "ST-Discovery-F7508", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_F7308", + "board_name": "DISCO-F7308", + "product_code": "0856", + "target_type": "platform", + "slug": "ST-Discovery-F7308", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "UBLOX_C030_R412M", + "board_name": "u-blox C030-R412M LTE CatM1/NB1 and 2G", + "product_code": "C036", + "target_type": "platform", + "slug": "ublox-C030-R412M", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "SDT32620B", + "board_name": "SDT32620B", + "product_code": "3101", + "target_type": "platform", + "slug": "SDT32620B", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "S5JS100", + "board_name": "S5JS100", + "product_code": "3701", + "target_type": "platform", + "slug": "S5JS100", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "MIMXRT1064-EVK", + "board_name": "MIMXRT1064-EVK", + "product_code": "0232", + "target_type": "platform", + "slug": "MIMXRT1064-EVK", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_L562QE", + "board_name": "DISCO-L562QE (STM32L562E-DK)", + "product_code": "0855", + "target_type": "platform", + "slug": "ST-Discovery-L562QE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_L552ZE_Q", + "board_name": "NUCLEO-L552ZE-Q", + "product_code": "0854", + "target_type": "platform", + "slug": "ST-Nucleo-L552ZE-Q", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_F301K8", + "board_name": "NUCLEO-F301K8", + "product_code": "0853", + "target_type": "platform", + "slug": "ST-Nucleo-F301K8", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_G031K8", + "board_name": "NUCLEO-G031K8", + "product_code": "0852", + "target_type": "platform", + "slug": "ST-Nucleo-G031K8", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.2" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_G431KB", + "board_name": "NUCLEO-G431KB", + "product_code": "0851", + "target_type": "platform", + "slug": "ST-Nucleo-G431KB", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_G431RB", + "board_name": "NUCLEO-G431RB", + "product_code": "0850", + "target_type": "platform", + "slug": "ST-Nucleo-G431RB", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_G070RB", + "board_name": "NUCLEO-G070RB", + "product_code": "0849", + "target_type": "platform", + "slug": "ST-Nucleo-G070RB", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUMAKER_IOT_M487", + "board_name": "NuMaker-IoT-M487", + "product_code": "1308", + "target_type": "platform", + "slug": "NUMAKER-IOT-M487", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "TMPM4G9", + "board_name": "AdBun-M4G9", + "product_code": "7015", + "target_type": "platform", + "slug": "TMPM4G9", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "SDT64B", + "board_name": "SDT64B", + "product_code": "3105", + "target_type": "platform", + "slug": "SDT64B", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "SDT52832B", + "board_name": "SDT52832B", + "product_code": "3104", + "target_type": "platform", + "slug": "SDT52832B", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "SDT51822B", + "board_name": "SDT51822B", + "product_code": "3103", + "target_type": "platform", + "slug": "SDT51822B", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "SDT32625B", + "board_name": "SDT32625B", + "product_code": "3102", + "target_type": "platform", + "slug": "SDT32625B", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "TT_M4G9", + "board_name": "ThunderSoft TT_M4G9", + "product_code": "8013", + "target_type": "platform", + "slug": "TT-M4G9", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "TT_M3HQ", + "board_name": "ThunderSoft TT_M3HQ", + "product_code": "8012", + "target_type": "platform", + "slug": "TT-M3HQ", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.11", + "Mbed OS 5.12" + ], + "mbed_enabled": [] + }, + { + "board_type": "WIO_BG96", + "board_name": "Seeed Wio LTE M1/NB1(BG96)", + "product_code": "9015", + "target_type": "platform", + "slug": "Seeed-Wio-BG96", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "MAX32625PICO", + "board_name": "MAX32625PICO_DAPLINK_UNKNOWN", + "product_code": "0444", + "target_type": "platform", + "slug": "MAX32625PICO_DAPLINK_UNKNOWN", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "UHURU_RAVEN", + "board_name": "Uhuru RAVEN", + "product_code": "9020", + "target_type": "platform", + "slug": "UH-ENE01", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "WIO_3G", + "board_name": "Seeed Wio 3G", + "product_code": "9014", + "target_type": "platform", + "slug": "Seeed-Wio-3G", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "DISCO_G071B", + "board_name": "DISCO-G071B", + "product_code": "0848", + "target_type": "platform", + "slug": "ST-Discovery-G071B", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_H745I", + "board_name": "DISCO-H745I", + "product_code": "0847", + "target_type": "platform", + "slug": "ST-Discovery-H745I", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_L412KB", + "board_name": "NUCLEO-L412KB", + "product_code": "0846", + "target_type": "platform", + "slug": "ST-Nucleo-L412KB", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_L412RB_P", + "board_name": "NUCLEO-L412RB-P", + "product_code": "0845", + "target_type": "platform", + "slug": "ST-Nucleo-L412RB-P", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_H755ZI_Q", + "board_name": "NUCLEO-H755ZI-Q", + "product_code": "0844", + "target_type": "platform", + "slug": "ST-Nucleo-H755ZI-Q", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_H745ZI_Q", + "board_name": "NUCLEO-H745ZI-Q", + "product_code": "0843", + "target_type": "platform", + "slug": "ST-Nucleo-H745ZI-Q", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_H753ZI", + "board_name": "NUCLEO-H753ZI", + "product_code": "0842", + "target_type": "platform", + "slug": "ST-Nucleo-H753ZI", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_G474RE", + "board_name": "NUCLEO-G474RE", + "product_code": "0841", + "target_type": "platform", + "slug": "ST-Nucleo-G474RE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "FUTURE_SEQUANA", + "board_name": "Future Electronics Sequana", + "product_code": "6000", + "target_type": "platform", + "slug": "Future-Sequana", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13" + ], + "mbed_enabled": [] + }, + { + "board_type": "MTB_USI_WM_BN_BM_22", + "board_name": "MTB_USI_WM_BN_BM_22", + "product_code": "0462", + "target_type": "platform", + "slug": "Reserved-0450-0490-for-MODULE13", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "MTB_LAIRD_BL652", + "board_name": "MTB_LAIRD_BL652", + "product_code": "0461", + "target_type": "platform", + "slug": "Reserved-0450-0490-for-MODULE12", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "TMPM3HQ", + "board_name": "AdBun-M3HQ", + "product_code": "7014", + "target_type": "platform", + "slug": "TMPM3HQ", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L4A6ZG", + "board_name": "NUCLEO-L4A6ZG", + "product_code": "0782", + "target_type": "platform", + "slug": "ST-Nucleo-L4A6ZG", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "SAKURAIO_EVB_01", + "board_name": "sakura.io Evaluation Board", + "product_code": "C008", + "target_type": "platform", + "slug": "SAKURAIO_EVB_01", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "MTS_DRAGONFLY_L471QG", + "board_name": "MultiTech Dragonfly Nano", + "product_code": "0312", + "target_type": "platform", + "slug": "MTS-Dragonfly-Nano", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "FF_LPC546XX", + "board_name": "L-Tek FF-LPC546XX", + "product_code": "8081", + "target_type": "platform", + "slug": "L-TEK-FF-LPC546XX", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_WB55RG", + "board_name": "NUCLEO-WB55RG", + "product_code": "0839", + "target_type": "platform", + "slug": "ST-Nucleo-WB55RG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L4R5ZI", + "board_name": "NUCLEO-L4R5ZI-P", + "product_code": "0781", + "target_type": "platform", + "slug": "ST-Nucleo-L4R5ZI-P", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_G071RB", + "board_name": "NUCLEO-G071RB", + "product_code": "0729", + "target_type": "platform", + "slug": "ST-Nucleo-G071RB", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "MBED_CONNECT_ODIN", + "board_name": "Mbed Connect Cloud", + "product_code": "2410", + "target_type": "platform", + "slug": "mbed-Connect-Cloud", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "ARM_MPS3", + "board_name": "ARM V2M-MPS3", + "product_code": "5005", + "target_type": "platform", + "slug": "ARM-MPS3", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "MIMXRT1050_EVK", + "board_name": "IMXRT1050-EVKB", + "product_code": "0227", + "target_type": "platform", + "slug": "MIMXRT1050-EVK", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "TMPM46B", + "board_name": "AdBun-M46B", + "product_code": "7013", + "target_type": "platform", + "slug": "TMPM46B", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "EFM32GG11_STK3701", + "board_name": "EFM32 Giant Gecko 11", + "product_code": "2042", + "target_type": "platform", + "slug": "EFM32-Giant-Gecko-11", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "FF1705_L151CC", + "board_name": "L-Tek FF1705", + "product_code": "8080", + "target_type": "platform", + "slug": "L-TEK-FF1705", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "SILICA_SENSOR_NODE", + "board_name": "Avnet Silica ST Sensor Node", + "product_code": "0766", + "target_type": "platform", + "slug": "Silica-Sensor-Node", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "EV_COG_AD4050LZ", + "board_name": "EV-COG-AD4050LZ", + "product_code": "0603", + "target_type": "platform", + "slug": "EV-COG-AD4050LZ", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "EV_COG_AD3029LZ", + "board_name": "EV-COG-AD3029LZ", + "product_code": "0602", + "target_type": "platform", + "slug": "EV-COG-AD3029LZ", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "PLACEHOLDER", + "board_name": "MAX35103EVKIT2", + "product_code": "0419", + "target_type": "platform", + "slug": "MAX35103EVKIT2", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC546XX", + "board_name": "NXP LPCXpresso54628", + "product_code": "1058", + "target_type": "platform", + "slug": "LPCXpresso54628", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "LPC546XX", + "board_name": "NXP LPCXpresso54608", + "product_code": "1056", + "target_type": "platform", + "slug": "LPCXpresso54608", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "MTM_MTCONNECT04S", + "board_name": "MtM+ MtConnect04S", + "product_code": "C005", + "target_type": "platform", + "slug": "MtConnect04S", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "UBLOX_C030_R3121", + "board_name": "u-blox C030-R3121 IoT Starter Kit", + "product_code": "C035", + "target_type": "platform", + "slug": "ublox-C030-R3121", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "ARM_CM3DS_MPS2", + "board_name": "ARM Cortex-M3 DesignStart", + "product_code": "5004", + "target_type": "platform", + "slug": "ARM-CM3DS", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "DISCO_L4R9I", + "board_name": "DISCO-L4R9I", + "product_code": "0774", + "target_type": "platform", + "slug": "DISCO-L4R9I", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L4R5ZI", + "board_name": "NUCLEO-L4R5ZI", + "product_code": "0776", + "target_type": "platform", + "slug": "NUCLEO-L4R5ZI", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L433RC_P", + "board_name": "NUCLEO-L433RC-P", + "product_code": "0779", + "target_type": "platform", + "slug": "NUCLEO-L433RC-P", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L452RE_P", + "board_name": "NUCLEO-L452RE-P", + "product_code": "0829", + "target_type": "platform", + "slug": "NUCLEO-L452RE-P", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUMAKER_PFM_NANO130", + "board_name": "NuMaker-PFM-NANO130", + "product_code": "1306", + "target_type": "platform", + "slug": "NUMAKER-PFM-NANO130", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUC240", + "board_name": "NUMAKER_PFM_NUC240", + "product_code": "1307", + "target_type": "platform", + "slug": "NUMAKER_PFM_NUC240", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "MAX32620FTHR", + "board_name": "MAX32620FTHR", + "product_code": "0418", + "target_type": "platform", + "slug": "MAX32620FTHR", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "TB_SENSE_12", + "board_name": "Thunderboard Sense 2", + "product_code": "2041", + "target_type": "platform", + "slug": "thunderboard-sense-2", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NU_PFM_M2351_NPSA_NS", + "board_name": "NuMaker-PFM-M2351", + "product_code": "1305", + "target_type": "platform", + "slug": "NUMAKER-PFM-M2351", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "THINGOO_101", + "board_name": "Thingoo 101", + "product_code": "2300", + "target_type": "platform", + "slug": "Thingoo-101", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "UBLOX_C030_N211", + "board_name": "u-blox C030-N211 IoT Starter Kit", + "product_code": "C031", + "target_type": "platform", + "slug": "ublox-C030-N211", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "UBLOX_C030_U201", + "board_name": "u-blox C030-U201 IoT Starter Kit", + "product_code": "C030", + "target_type": "platform", + "slug": "ublox-C030-U201", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "REALTEK_RTL8195AM", + "board_name": "Realtek RTL8195AM", + "product_code": "4600", + "target_type": "platform", + "slug": "Realtek-RTL8195AM", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "TMPM3H6", + "board_name": "AdBun-M3H6", + "product_code": "7012", + "target_type": "platform", + "slug": "TMPM3H6", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "STM32F103T8", + "board_name": "MLINKER_F103T8", + "product_code": "8011", + "target_type": "platform", + "slug": "MLINKER_F103T8", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "STM32L476VG", + "board_name": "MLINKER_L476QE ", + "product_code": "8010", + "target_type": "platform", + "slug": "MLINKER_L476QE", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_L496ZG_P", + "board_name": "NUCLEO-L496ZG-P", + "product_code": "0828", + "target_type": "platform", + "slug": "ST-Nucleo-L496ZG-P", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "TMPM066", + "board_name": "AdBun-M066", + "product_code": "7011", + "target_type": "platform", + "slug": "TMPM066", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.5", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "GR_LYCHEE", + "board_name": "GR-LYCHEE", + "product_code": "5501", + "target_type": "platform", + "slug": "Renesas-GR-LYCHEE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NRF52840_DK", + "board_name": "Nordic nRF52840-DK", + "product_code": "1102", + "target_type": "platform", + "slug": "Nordic-nRF52840-DK", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "MAX32625PICO", + "board_name": "MAX32625PICO", + "product_code": "0416", + "target_type": "platform", + "slug": "MAX32625PICO", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC54114", + "board_name": "NXP LPCXpresso54114", + "product_code": "1054", + "target_type": "platform", + "slug": "LPCXpresso54114", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUMAKER_PFM_M487", + "board_name": "NuMaker-PFM-M487", + "product_code": "1304", + "target_type": "platform", + "slug": "NUMAKER-PFM-M487", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "MAX32625MBED", + "board_name": "MAX32625MBED", + "product_code": "0415", + "target_type": "platform", + "slug": "MAX32625MBED", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "DELTA_DFCM_NNN50", + "board_name": "Delta DFCM-NNN50", + "product_code": "4502", + "target_type": "platform", + "slug": "Delta-DFCM-NNN50", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.4" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "UBLOX_EVK_NINA_B1", + "board_name": "u-blox EVK-NINA-B1", + "product_code": "1237", + "target_type": "platform", + "slug": "u-blox-EVK-NINA-B1", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "UBLOX_EVA_NINA", + "board_name": "NINA-B1 Maker boards with DAPLink compatible interface", + "product_code": "1238", + "target_type": "platform", + "slug": "u-blox-NINA-B1", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "MAX32630FTHR", + "board_name": "MAX32630FTHR", + "product_code": "0409", + "target_type": "platform", + "slug": "MAX32630FTHR", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "RO359B", + "board_name": "Arch RO359B", + "product_code": "1022", + "target_type": "platform", + "slug": "Arch-RO359B", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_L072CZ_LRWAN1", + "board_name": "DISCO-L072CZ-LRWAN1", + "product_code": "0833", + "target_type": "platform", + "slug": "ST-Discovery-LRWAN1", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L486RG", + "board_name": "NUCLEO-L486RG", + "product_code": "0827", + "target_type": "platform", + "slug": "ST-Nucleo-L486RG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.2" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_F756ZG", + "board_name": "NUCLEO-F756ZG", + "product_code": "0879", + "target_type": "platform", + "slug": "ST-Nucleo-F756ZG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.2" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_F439ZI", + "board_name": "NUCLEO-F439ZI", + "product_code": "0797", + "target_type": "platform", + "slug": "ST-Nucleo-F439ZI", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.2" + ], + "mbed_enabled": [] + }, + { + "board_type": "MAX32625NEXPAQ", + "board_name": "MAX32625NEXPAQ", + "product_code": "0408", + "target_type": "platform", + "slug": "MAX32625NEXPAQ", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "RBLAB_BLENANO2", + "board_name": "RedBear BLENANO 2", + "product_code": "1093", + "target_type": "platform", + "slug": "RedBear-BLE-NANO2", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "RBLAB_BLEND2", + "board_name": "RedBear BLEND2", + "product_code": "1091", + "target_type": "platform", + "slug": "RedBear-BLEND2", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "KL26Z", + "board_name": "FRDM-KL26Z", + "product_code": "0260", + "target_type": "platform", + "slug": "FRDM-KL26Z", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "KL82Z", + "board_name": "FRDM-KL82Z", + "product_code": "0218", + "target_type": "platform", + "slug": "FRDM-KL82Z", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "K82F", + "board_name": "FRDM-K82F", + "product_code": "0217", + "target_type": "platform", + "slug": "FRDM-K82F", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "K64F", + "board_name": "mbed 6LoWPAN Border Router Ethernet", + "product_code": "7404", + "target_type": "platform", + "slug": "mbed-6LoWPAN-Border-Router-Ethernet", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_L475VG_IOT01A", + "board_name": "DISCO-L475VG-IOT01A (B-L475E-IOT01A)", + "product_code": "0764", + "target_type": "platform", + "slug": "ST-Discovery-L475E-IOT01A", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "DISCO_F413ZH", + "board_name": "DISCO-F413ZH", + "product_code": "0743", + "target_type": "platform", + "slug": "ST-Discovery-F413H", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "DISCO_F723E", + "board_name": "DISCO-F723E", + "product_code": "0811", + "target_type": "platform", + "slug": "ST-Discovery-F723E", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_L496AG", + "board_name": "DISCO-L496AG", + "product_code": "0822", + "target_type": "platform", + "slug": "ST-Discovery-L496AG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "K66F", + "board_name": "FRDM-K66F", + "product_code": "0311", + "target_type": "platform", + "slug": "FRDM-K66F", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "KL43Z", + "board_name": "FRDM-KL43Z", + "product_code": "0262", + "target_type": "platform", + "slug": "FRDM-KL43Z", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "DELTA_DFBM_NQ620", + "board_name": "Delta DFBM-NQ620", + "product_code": "4501", + "target_type": "platform", + "slug": "Delta-DFBM-NQ620", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "MAX32620HSP", + "board_name": "MAX32620HSP", + "product_code": "0407", + "target_type": "platform", + "slug": "MAX32620HSP", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8" + ], + "mbed_enabled": [] + }, + { + "board_type": "K64F", + "board_name": "mbed 6LoWPAN Border Router USB", + "product_code": "7403", + "target_type": "platform", + "slug": "mbed-6LoWPAN-Border-Router-USB", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "K64F", + "board_name": "mbed 6LoWPAN Border Router HAT", + "product_code": "7402", + "target_type": "platform", + "slug": "mbed-6LoWPAN-Border-Router-HAT", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_F722ZE", + "board_name": "NUCLEO-F722ZE", + "product_code": "0812", + "target_type": "platform", + "slug": "ST-Nucleo-F722ZE", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_L452RE", + "board_name": "NUCLEO-L452RE", + "product_code": "0821", + "target_type": "platform", + "slug": "ST-Nucleo-L452RE", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_L496ZG", + "board_name": "NUCLEO-L496ZG", + "product_code": "0823", + "target_type": "platform", + "slug": "ST-Nucleo-L496ZG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F413ZH", + "board_name": "NUCLEO-F413ZH", + "product_code": "0742", + "target_type": "platform", + "slug": "ST-Nucleo-F413ZH", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15" + ], + "mbed_enabled": [] + }, + { + "board_type": "SSCI_CHIBIBIT", + "board_name": "chibi:bit", + "product_code": "1021", + "target_type": "platform", + "slug": "SSCI-CHIBIBIT", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUMAKER_PFM_M453", + "board_name": "NuMaker-PFM-M453", + "product_code": "1303", + "target_type": "platform", + "slug": "Nuvoton-M453", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "THUNDERBOARD_SENSE", + "board_name": "ThunderBoard Sense", + "product_code": "2045", + "target_type": "platform", + "slug": "ThunderBoard-Sense", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_H743ZI", + "board_name": "NUCLEO-H743ZI", + "product_code": "0813", + "target_type": "platform", + "slug": "ST-Nucleo-H743ZI", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_H747I", + "board_name": "DISCO-H747I", + "product_code": "0814", + "target_type": "platform", + "slug": "ST-Discovery-H747I", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "WIZWIKI_W7500PECO", + "board_name": "WIZwiki-W7500PECO", + "product_code": "2204", + "target_type": "platform", + "slug": "WIZwiki-W7500PECO", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "VK_RZ_A1H", + "board_name": "VK-RZ/A1H", + "product_code": "C002", + "target_type": "platform", + "slug": "VK-RZA1H", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "ARM_BEETLE_SOC", + "board_name": "ARM Beetle IoT Evaluation Platform", + "product_code": "5002", + "target_type": "platform", + "slug": "ARM-IoTEP-Beetle", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8" + ], + "mbed_enabled": [] + }, + { + "board_type": "LPC812", + "board_name": "CoCo-ri-Co!", + "product_code": "C000", + "target_type": "platform", + "slug": "CoCo-ri-Co", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC1768", + "board_name": "LPCXpresso1769", + "product_code": "1011", + "target_type": "platform", + "slug": "LPCXpresso1769", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "KL27Z", + "board_name": "FRDM-KL27Z", + "product_code": "0261", + "target_type": "platform", + "slug": "FRDM-KL27Z", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "HEXIWEAR", + "board_name": "Hexiwear", + "product_code": "0214", + "target_type": "platform", + "slug": "Hexiwear", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "KW41Z", + "board_name": "FRDM-KW41Z", + "product_code": "0201", + "target_type": "platform", + "slug": "FRDM-KW41Z", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUMAKER_PFM_NUC472", + "board_name": "NuMaker-PFM-NUC472", + "product_code": "1302", + "target_type": "platform", + "slug": "Nuvoton-NUC472", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "XDOT_L151CC", + "board_name": "MultiTech xDot", + "product_code": "0350", + "target_type": "platform", + "slug": "MTS-xDot-L151CC", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "UBLOX_EVK_ODIN_W2", + "board_name": "u-blox EVK-ODIN-W2", + "product_code": "1236", + "target_type": "platform", + "slug": "ublox-EVK-ODIN-W2", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "SKT1150L", + "board_name": "SKT1150L ", + "product_code": "1400", + "target_type": "platform", + "slug": "SKT1150L", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC810", + "board_name": "LPC810", + "product_code": "1020", + "target_type": "platform", + "slug": "LPC810", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "PLACEHOLDER", + "board_name": "NZ32-SC151", + "product_code": "6660", + "target_type": "platform", + "slug": "NZ32-SC151", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_F767ZI", + "board_name": "NUCLEO-F767ZI", + "product_code": "0818", + "target_type": "platform", + "slug": "ST-Nucleo-F767ZI", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "EFM32PG12_STK3402", + "board_name": "EFM32 Pearl Gecko 12", + "product_code": "2040", + "target_type": "platform", + "slug": "EFM32-Pearl-Gecko-12", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "EFM32PG_STK3401", + "board_name": "EFM32 Pearl Gecko", + "product_code": "2035", + "target_type": "platform", + "slug": "EFM32-Pearl-Gecko", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "TY51822R3", + "board_name": "Switch Science mbed TY51822r3", + "product_code": "1019", + "target_type": "platform", + "slug": "Switch-Science-mbed-TY51822r3", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NCS36510", + "board_name": "DVK-NCS36510-MBED-GEVB", + "product_code": "1200", + "target_type": "platform", + "slug": "NCS36510", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NUC472-NUTINY", + "board_name": "NUC472-NUTINY", + "product_code": "1300", + "target_type": "platform", + "slug": "NUC472-NUTINY", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "B96B_F446VE", + "board_name": "B96B-F446VE", + "product_code": "0840", + "target_type": "platform", + "slug": "ST-B96B-F446VE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F429ZI", + "board_name": "NUCLEO-F429ZI", + "product_code": "0796", + "target_type": "platform", + "slug": "ST-Nucleo-F429ZI", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F207ZG", + "board_name": "NUCLEO-F207ZG", + "product_code": "0835", + "target_type": "platform", + "slug": "ST-Nucleo-F207ZG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F446ZE", + "board_name": "NUCLEO-F446ZE", + "product_code": "0778", + "target_type": "platform", + "slug": "ST-Nucleo-F446ZE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F303ZE", + "board_name": "NUCLEO-F303ZE", + "product_code": "0747", + "target_type": "platform", + "slug": "ST-Nucleo-F303ZE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "DISCO_F746NG", + "board_name": "DISCO-F746NG", + "product_code": "0815", + "target_type": "platform", + "slug": "ST-Discovery-F746NG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "ARM_IOTSS_BEID", + "board_name": "CoreLink SSE-100 (IOT Subsystem for Cortex-M)", + "product_code": "5001", + "target_type": "platform", + "slug": "ARM-IoTSS-M3", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [] + }, + { + "board_type": "SAMD21J18A", + "board_name": "Atmel ATSAMD21-XPRO", + "product_code": "0915", + "target_type": "platform", + "slug": "SAMD21-XPRO", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "SAML21J18A", + "board_name": "Atmel ATSAML21-XPRO-B", + "product_code": "0910", + "target_type": "platform", + "slug": "SAML21-XPRO", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "SAMD21G18A", + "board_name": "Atmel ATSAMW25-XPRO", + "product_code": "0905", + "target_type": "platform", + "slug": "SAMW25-XPRO", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "SAMR21G18A", + "board_name": "Atmel ATSAMR21-XPRO", + "product_code": "0900", + "target_type": "platform", + "slug": "SAMR21-XPRO", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NRF52_DK", + "board_name": "Nordic nRF52-DK", + "product_code": "1101", + "target_type": "platform", + "slug": "Nordic-nRF52-DK", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "WIZWIKI_W7500P", + "board_name": "WIZwiki-W7500P", + "product_code": "2203", + "target_type": "platform", + "slug": "WIZwiki-W7500P", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6" + ], + "mbed_enabled": [] + }, + { + "board_type": "WIZWIKI_W7500ECO", + "board_name": "WIZwiki-W7500ECO", + "product_code": "2202", + "target_type": "platform", + "slug": "WIZwiki-W7500ECO", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "DISCO_F407VG", + "board_name": "DISCO_F407VG", + "product_code": "0830", + "target_type": "platform", + "slug": "ST-Discovery-F407VG", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_F0308", + "board_name": "DISCO-F0308", + "product_code": "0726", + "target_type": "platform", + "slug": "ST-Discovery-F0308", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_F411E", + "board_name": "DISCO-F411E", + "product_code": "0741", + "target_type": "platform", + "slug": "ST-Discovery-F411E", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_F303VC", + "board_name": "DISCO-F303VC", + "product_code": "0746", + "target_type": "platform", + "slug": "ST-Discovery-F303C", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_F072B", + "board_name": "DISCO-F072B", + "product_code": "0731", + "target_type": "platform", + "slug": "ST-Discovery-F072B", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_F769NI", + "board_name": "DISCO-F769NI", + "product_code": "0817", + "target_type": "platform", + "slug": "ST-Discovery-F769NI", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "MTS_MDOT_F411RE", + "board_name": "MultiTech mDot Box/EVB", + "product_code": "0320", + "target_type": "platform", + "slug": "mdotevb", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1" + ], + "mbed_enabled": [] + }, + { + "board_type": "NUCLEO_F746ZG", + "board_name": "NUCLEO-F746ZG", + "product_code": "0816", + "target_type": "platform", + "slug": "ST-Nucleo-F746ZG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F412ZG", + "board_name": "NUCLEO-F412ZG", + "product_code": "0826", + "target_type": "platform", + "slug": "ST-Nucleo-F412ZG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "DISCO_F412ZG", + "board_name": "DISCO-F412ZG", + "product_code": "0825", + "target_type": "platform", + "slug": "ST-Discovery-F412ZG", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NRF51_MICROBIT", + "board_name": "BBC micro:bit", + "product_code": "9900", + "target_type": "platform", + "slug": "Microbit", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F031K6", + "board_name": "NUCLEO-F031K6", + "product_code": "0791", + "target_type": "platform", + "slug": "ST-Nucleo-F031K6", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "ARCH_LINK", + "board_name": "Seeed Arch Link", + "product_code": "9013", + "target_type": "platform", + "slug": "Seeed-Arch-Link", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "DISCO_F469NI", + "board_name": "DISCO-F469NI", + "product_code": "0788", + "target_type": "platform", + "slug": "ST-Discovery-F469NI", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F410RB", + "board_name": "NUCLEO-F410RB", + "product_code": "0744", + "target_type": "platform", + "slug": "ST-Nucleo-F410RB", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F446RE", + "board_name": "NUCLEO-F446RE", + "product_code": "0777", + "target_type": "platform", + "slug": "ST-Nucleo-F446RE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "WIZWIKI_W7500", + "board_name": "WIZwiki-W7500", + "product_code": "2201", + "target_type": "platform", + "slug": "WIZwiki-W7500", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "KW24D", + "board_name": "FRDM-KW24D512", + "product_code": "0250", + "target_type": "platform", + "slug": "FRDM-KW24D512", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "XBED_LPC1768", + "board_name": "Smeshlink xbed LPC1768", + "product_code": "2100", + "target_type": "platform", + "slug": "xbed-LPC1768", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_L476VG", + "board_name": "DISCO-L476VG", + "product_code": "0820", + "target_type": "platform", + "slug": "ST-Discovery-L476VG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "DISCO_F746NG", + "board_name": "DEPRECATED-DISCO-F746NG", + "product_code": "1814", + "target_type": "platform", + "slug": "DEPRECATED-ST-Discovery-F746NG", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_L053C8", + "board_name": "DISCO-L053C8", + "product_code": "0805", + "target_type": "platform", + "slug": "ST-Discovery-L053C8", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "DISCO_F334C8", + "board_name": "DISCO-F334C8", + "product_code": "0810", + "target_type": "platform", + "slug": "ST-Discovery-F334C8", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "CM0_ARM", + "board_name": "Cortex M0 ARM", + "product_code": "9999", + "target_type": "platform", + "slug": "Cortex-M0-ARM", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "CM0_UARM", + "board_name": "Cortex M0 uARM", + "product_code": "9998", + "target_type": "platform", + "slug": "Cortex-M0-uARM", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "CM0P_ARM", + "board_name": "Cortex M0p ARM", + "product_code": "9997", + "target_type": "platform", + "slug": "Cortex-M0p-ARM", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "CM0P_UARM", + "board_name": "Cortex M0p uARM", + "product_code": "9996", + "target_type": "platform", + "slug": "Cortex-M0p-uARM", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "CM3_ARM", + "board_name": "Cortex M3 ARM", + "product_code": "9995", + "target_type": "platform", + "slug": "Cortex-M3-ARM", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "CM3_UARM", + "board_name": "Cortex M3 uARM", + "product_code": "9994", + "target_type": "platform", + "slug": "Cortex-M3-uARM", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "CM4_ARM", + "board_name": "Cortex M4 ARM", + "product_code": "9993", + "target_type": "platform", + "slug": "Cortex-M4-ARM", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "CM4_UARM", + "board_name": "Cortex M4 uARM", + "product_code": "9992", + "target_type": "platform", + "slug": "Cortex-M4-uARM", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "CM4F_ARM", + "board_name": "Cortex M4F ARM", + "product_code": "9991", + "target_type": "platform", + "slug": "Cortex-M4F-ARM", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "MAX32600MBED", + "board_name": "MAX32600MBED", + "product_code": "0405", + "target_type": "platform", + "slug": "MAX32600mbed", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.2", + "Mbed OS 6.3" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "MAXWSNENV", + "board_name": "MAXWSNENV", + "product_code": "0400", + "target_type": "platform", + "slug": "MAXWSNENV", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6" + ], + "mbed_enabled": [] + }, + { + "board_type": "DISCO_F429ZI", + "board_name": "DISCO-F429ZI", + "product_code": "0795", + "target_type": "platform", + "slug": "ST-Discovery-F429ZI", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L031K6", + "board_name": "NUCLEO-L031K6", + "product_code": "0790", + "target_type": "platform", + "slug": "ST-Nucleo-L031K6", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F042K6", + "board_name": "NUCLEO-F042K6", + "product_code": "0785", + "target_type": "platform", + "slug": "ST-Nucleo-F042K6", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L011K4", + "board_name": "NUCLEO-L011K4", + "product_code": "0780", + "target_type": "platform", + "slug": "ST-Nucleo-L011K4", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F303K8", + "board_name": "NUCLEO-F303K8", + "product_code": "0775", + "target_type": "platform", + "slug": "ST-Nucleo-F303K8", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L432KC", + "board_name": "NUCLEO-L432KC", + "product_code": "0770", + "target_type": "platform", + "slug": "ST-Nucleo-L432KC", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "TEENSY3_1", + "board_name": "Teensy 3.1", + "product_code": "7778", + "target_type": "platform", + "slug": "Teensy-3-1", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "CM4F_UARM", + "board_name": "Cortex M4F uARM", + "product_code": "9990", + "target_type": "platform", + "slug": "Cortex-M4F-uARM", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "DELTA_DFCM_NNN40", + "board_name": "Delta DFCM-NNN40 (EOL)", + "product_code": "4500", + "target_type": "platform", + "slug": "Delta-DFCM-NNN40", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "SEEED_TINY_BLE", + "board_name": "Seeed Tiny BLE", + "product_code": "9012", + "target_type": "platform", + "slug": "Seeed-Tiny-BLE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "MOTE_L152RC", + "board_name": "NAMote72", + "product_code": "4100", + "target_type": "platform", + "slug": "NAMote-72", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.4", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NRF51822_Y5_MBUG", + "board_name": "y5 nRF51822 mbug", + "product_code": "4005", + "target_type": "platform", + "slug": "Y5-NRF51822-MBUG", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC11U35_Y5_MBUG", + "board_name": "y5 LPC11U35 mbug", + "product_code": "4000", + "target_type": "platform", + "slug": "Y5-LPC11U35-MBUG", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC4088_DM", + "board_name": "EA LPC4088 Display Module", + "product_code": "1062", + "target_type": "platform", + "slug": "EA-LPC4088-Display-Module", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [] + }, + { + "board_type": "HOME_GATEWAY_6LOWPAN", + "board_name": "HOME GATEWAY 6LOWPAN", + "product_code": "5020", + "target_type": "platform", + "slug": "HOME-GATEWAY-6LOWPAN", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "MTS_MDOT_F411RE", + "board_name": "MultiTech mDot", + "product_code": "0315", + "target_type": "platform", + "slug": "MTS-mDot-F411", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L476RG", + "board_name": "NUCLEO-L476RG", + "product_code": "0765", + "target_type": "platform", + "slug": "ST-Nucleo-L476RG", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L073RZ", + "board_name": "NUCLEO-L073RZ", + "product_code": "0760", + "target_type": "platform", + "slug": "ST-Nucleo-L073RZ", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F070RB", + "board_name": "NUCLEO-F070RB", + "product_code": "0755", + "target_type": "platform", + "slug": "ST-Nucleo-F070RB", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "MTS_DRAGONFLY_F411RE", + "board_name": "MultiTech Dragonfly", + "product_code": "0310", + "target_type": "platform", + "slug": "MTS-Dragonfly", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "SSCI824", + "board_name": "Switch Science mbed LPC824", + "product_code": "1018", + "target_type": "platform", + "slug": "Switch-Science-mbed-LPC824", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC11U24", + "board_name": "Robotiky", + "product_code": "1045", + "target_type": "platform", + "slug": "Robotiky", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "WALLBOT_BLE", + "board_name": "JKSoft Wallbot BLE", + "product_code": "1140", + "target_type": "platform", + "slug": "JKSoft-Wallbot-BLE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NRF51822_SBK", + "board_name": "Nordic Smart Beacon Kit", + "product_code": "1130", + "target_type": "platform", + "slug": "Nordic-Smart-Beacon-Kit", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "K64F", + "board_name": "Ethernet IoT Starter Kit", + "product_code": "0245", + "target_type": "platform", + "slug": "IBMEthernetKit", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NRF51822_OTA", + "board_name": "Nordic nRF51822 FOTA", + "product_code": "1075", + "target_type": "platform", + "slug": "Nordic-nRF51822-FOTA", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NRF51_DK_OTA", + "board_name": "Nordic nRF51-DK FOTA", + "product_code": "1105", + "target_type": "platform", + "slug": "Nordic-nRF51-DK-FOTA", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "NRF51_DONGLE", + "board_name": "Nordic nRF51-Dongle", + "product_code": "1120", + "target_type": "platform", + "slug": "Nordic-nRF51-Dongle", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "RBLAB_BLENANO", + "board_name": "RedBearLab BLE Nano", + "product_code": "1095", + "target_type": "platform", + "slug": "RedBearLab-BLE-Nano", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [] + }, + { + "board_type": "RZ_A1H", + "board_name": "GR-PEACH", + "product_code": "5500", + "target_type": "platform", + "slug": "Renesas-GR-PEACH", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NRF51_DK", + "board_name": "Nordic nRF51-DK", + "product_code": "1100", + "target_type": "platform", + "slug": "Nordic-nRF51-DK", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "MTS_GAMBIT", + "board_name": "MTS Gambit", + "product_code": "0300", + "target_type": "platform", + "slug": "MTS-Gambit", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "RIOT", + "board_name": "RIoT", + "product_code": "RIoT", + "target_type": "platform", + "slug": "RIoT", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "K22F", + "board_name": "FRDM-K22F", + "product_code": "0231", + "target_type": "platform", + "slug": "FRDM-K22F", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC824", + "board_name": "LPCXpresso824-MAX", + "product_code": "0824", + "target_type": "platform", + "slug": "LPCXpresso824-MAX", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "K20D50M", + "board_name": "FRDM-K20D50M", + "product_code": "0230", + "target_type": "platform", + "slug": "FRDM-K20D50M", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "ARCH_MAX", + "board_name": "Seeed Arch Max", + "product_code": "9011", + "target_type": "platform", + "slug": "Seeed-Arch-Max", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "EFM32HG_STK3400", + "board_name": "EFM32 USB-enabled Happy Gecko", + "product_code": "2005", + "target_type": "platform", + "slug": "EFM32-Happy-Gecko", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [] + }, + { + "board_type": "EFM32ZG_STK3200", + "board_name": "EFM32 Zero Gecko", + "product_code": "2030", + "target_type": "platform", + "slug": "EFM32-Zero-Gecko", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [] + }, + { + "board_type": "EFM32LG_STK3600", + "board_name": "EFM32 Leopard Gecko", + "product_code": "2020", + "target_type": "platform", + "slug": "EFM32-Leopard-Gecko", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "EFM32GG_STK3700", + "board_name": "EFM32 Giant Gecko", + "product_code": "2015", + "target_type": "platform", + "slug": "EFM32-Giant-Gecko", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "EFM32WG_STK3800", + "board_name": "EFM32 Wonder Gecko", + "product_code": "2010", + "target_type": "platform", + "slug": "EFM32-Wonder-Gecko", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "RBLAB_NRF51822", + "board_name": "RedBearLab nRF51822", + "product_code": "1090", + "target_type": "platform", + "slug": "RedBearLab-nRF51822", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [] + }, + { + "board_type": "ARM_MPS2", + "board_name": "ARM MPS2+ FPGA Prototyping System", + "product_code": "5000", + "target_type": "platform", + "slug": "ARM-MPS2", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "MICRONFCBOARD", + "board_name": "MicroNFCBoard", + "product_code": "1034", + "target_type": "platform", + "slug": "MicroNFCBoard", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "OC_MBUINO", + "board_name": "mBuino", + "product_code": "1080", + "target_type": "platform", + "slug": "mBuino", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "HRM1017", + "board_name": "mbed HRM1017", + "product_code": "1017", + "target_type": "platform", + "slug": "mbed-HRM1017", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F091RC", + "board_name": "NUCLEO-F091RC", + "product_code": "0750", + "target_type": "platform", + "slug": "ST-Nucleo-F091RC", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F303RE", + "board_name": "NUCLEO-F303RE", + "product_code": "0745", + "target_type": "platform", + "slug": "ST-Nucleo-F303RE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC4337", + "board_name": "LPCXpresso4337", + "product_code": "4337", + "target_type": "platform", + "slug": "LPCXpresso4337", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F411RE", + "board_name": "NUCLEO-F411RE", + "product_code": "0740", + "target_type": "platform", + "slug": "ST-Nucleo-F411RE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "ARCH_GPRS", + "board_name": "Seeed Arch GPRS V2", + "product_code": "9010", + "target_type": "platform", + "slug": "Seeed-Arch-GPRS", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "ARCH_BLE", + "board_name": "Seeed Arch BLE", + "product_code": "9009", + "target_type": "platform", + "slug": "Seeed-Arch-BLE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "XADOW_M0", + "board_name": "Seeed Xadow M0", + "product_code": "9008", + "target_type": "platform", + "slug": "Seeed-Xadow-M0", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC11U68", + "board_name": "LPCXpresso11U68", + "product_code": "1168", + "target_type": "platform", + "slug": "LPCXpresso11U68", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC11U35_501", + "board_name": "TG-LPC11U35-501", + "product_code": "9007", + "target_type": "platform", + "slug": "TG-LPC11U35-501", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC11U24", + "board_name": "RenBED LPC11U24", + "product_code": "3001", + "target_type": "platform", + "slug": "RenBED-LPC11U24", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC1549", + "board_name": "LPCXpresso1549", + "product_code": "1549", + "target_type": "platform", + "slug": "LPCXpresso1549", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC4330_M4", + "board_name": "Bambino-210E", + "product_code": "1605", + "target_type": "platform", + "slug": "Micromint-Bambino-210E", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC4330_M4", + "board_name": "Bambino-210", + "product_code": "1600", + "target_type": "platform", + "slug": "Micromint-Bambino-210", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "KL05Z", + "board_name": "FRDM-KL05Z", + "product_code": "0210", + "target_type": "platform", + "slug": "FRDM-KL05Z", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NRF51822", + "board_name": "Nordic nRF51822", + "product_code": "1070", + "target_type": "platform", + "slug": "Nordic-nRF51822", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [] + }, + { + "board_type": "K64F", + "board_name": "FRDM-K64F", + "product_code": "0240", + "target_type": "platform", + "slug": "FRDM-K64F", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Advanced", + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F334R8", + "board_name": "NUCLEO-F334R8", + "product_code": "0735", + "target_type": "platform", + "slug": "ST-Nucleo-F334R8", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F072RB", + "board_name": "NUCLEO-F072RB", + "product_code": "0730", + "target_type": "platform", + "slug": "ST-Nucleo-F072RB", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F030R8", + "board_name": "NUCLEO-F030R8", + "product_code": "0725", + "target_type": "platform", + "slug": "ST-Nucleo-F030R8", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F401RE", + "board_name": "NUCLEO-F401RE", + "product_code": "0720", + "target_type": "platform", + "slug": "ST-Nucleo-F401RE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L053R8", + "board_name": "NUCLEO-L053R8", + "product_code": "0715", + "target_type": "platform", + "slug": "ST-Nucleo-L053R8", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_L152RE", + "board_name": "NUCLEO-L152RE", + "product_code": "0710", + "target_type": "platform", + "slug": "ST-Nucleo-L152RE", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F302R8", + "board_name": "NUCLEO-F302R8", + "product_code": "0705", + "target_type": "platform", + "slug": "ST-Nucleo-F302R8", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "ARCH_PRO", + "board_name": "Seeeduino-Arch-Pro", + "product_code": "9004", + "target_type": "platform", + "slug": "Seeeduino-Arch-Pro", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "KL46Z", + "board_name": "FRDM-KL46Z", + "product_code": "0220", + "target_type": "platform", + "slug": "FRDM-KL46Z", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "NUCLEO_F103RB", + "board_name": "NUCLEO-F103RB", + "product_code": "0700", + "target_type": "platform", + "slug": "ST-Nucleo-F103RB", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC1768", + "board_name": "mbed NXP LPC1768 (exp)", + "product_code": "7777", + "target_type": "platform", + "slug": "mbed-LPC1768-exp", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC11U35_401", + "board_name": "EA LPC11U35 QuickStart Board", + "product_code": "1061", + "target_type": "platform", + "slug": "EA-LPC11U35", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "UBLOX_C027", + "board_name": "u-blox C027", + "product_code": "1234", + "target_type": "platform", + "slug": "u-blox-C027", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC1114", + "board_name": "mbed LPC1114FN28", + "product_code": "1114", + "target_type": "platform", + "slug": "LPC1114FN28", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC11U24", + "board_name": "Seeeduino-Arch", + "product_code": "9006", + "target_type": "platform", + "slug": "Seeeduino-Arch", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC1347", + "board_name": "WiFi DipCortex", + "product_code": "9003", + "target_type": "platform", + "slug": "WiFi-DipCortex", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC11U24", + "board_name": "BlueBoard-LPC11U24", + "product_code": "9002", + "target_type": "platform", + "slug": "BlueBoard-LPC11U24", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC1347", + "board_name": "DipCortex M3", + "product_code": "9001", + "target_type": "platform", + "slug": "DipCortex-M3", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC11U24", + "board_name": "DipCortex M0", + "product_code": "", + "target_type": "platform", + "slug": "DipCortex-M0", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + }, + { + "board_type": "LPC4088", + "board_name": "EA LPC4088 QuickStart Board", + "product_code": "1060", + "target_type": "platform", + "slug": "EA-LPC4088", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.4" + ], + "mbed_enabled": [] + }, + { + "board_type": "LPC812", + "board_name": "NXP LPC800-MAX", + "product_code": "1050", + "target_type": "platform", + "slug": "NXP-LPC800-MAX", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "KL25Z", + "board_name": "FRDM-KL25Z", + "product_code": "0200", + "target_type": "platform", + "slug": "KL25Z", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC11U24", + "board_name": "mbed LPC11U24", + "product_code": "1040", + "target_type": "platform", + "slug": "mbed-LPC11U24", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC1768", + "board_name": "mbed LPC1768", + "product_code": "1010", + "target_type": "platform", + "slug": "mbed-LPC1768", + "build_variant": [], + "mbed_os_support": [ + "Mbed OS 2", + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.4", + "Mbed OS 5.5", + "Mbed OS 5.6", + "Mbed OS 5.7", + "Mbed OS 5.8", + "Mbed OS 5.9", + "Mbed OS 6.0", + "Mbed OS 6.1", + "Mbed OS 6.10", + "Mbed OS 6.11", + "Mbed OS 6.12", + "Mbed OS 6.13", + "Mbed OS 6.14", + "Mbed OS 6.15", + "Mbed OS 6.2", + "Mbed OS 6.3", + "Mbed OS 6.4", + "Mbed OS 6.5", + "Mbed OS 6.6", + "Mbed OS 6.7", + "Mbed OS 6.8", + "Mbed OS 6.9" + ], + "mbed_enabled": [ + "Baseline" + ] + }, + { + "board_type": "LPC2368", + "board_name": "mbed NXP LPC2368", + "product_code": "1000", + "target_type": "platform", + "slug": "mbed-LPC2368", + "build_variant": [], + "mbed_os_support": [], + "mbed_enabled": [] + } +] \ No newline at end of file diff --git a/tools/python/mbed_tools/targets/_internal/data/targets_metadata.json b/tools/python/mbed_tools/targets/_internal/data/targets_metadata.json new file mode 100644 index 0000000000..ee96f63245 --- /dev/null +++ b/tools/python/mbed_tools/targets/_internal/data/targets_metadata.json @@ -0,0 +1,166 @@ +{ + "CORE_LABELS": { + "Cortex-M0": [ + "M0", + "CORTEX_M", + "LIKE_CORTEX_M0", + "CORTEX" + ], + "Cortex-M0+": [ + "M0P", + "CORTEX_M", + "LIKE_CORTEX_M0", + "CORTEX" + ], + "Cortex-M1": [ + "M1", + "CORTEX_M", + "LIKE_CORTEX_M1", + "CORTEX" + ], + "Cortex-M3": [ + "M3", + "CORTEX_M", + "LIKE_CORTEX_M3", + "CORTEX" + ], + "Cortex-M4": [ + "M4", + "CORTEX_M", + "RTOS_M4_M7", + "LIKE_CORTEX_M4", + "CORTEX" + ], + "Cortex-M4F": [ + "M4", + "CORTEX_M", + "RTOS_M4_M7", + "LIKE_CORTEX_M4", + "CORTEX" + ], + "Cortex-M7": [ + "M7", + "CORTEX_M", + "RTOS_M4_M7", + "LIKE_CORTEX_M7", + "CORTEX" + ], + "Cortex-M7F": [ + "M7", + "CORTEX_M", + "RTOS_M4_M7", + "LIKE_CORTEX_M7", + "CORTEX" + ], + "Cortex-M7FD": [ + "M7", + "CORTEX_M", + "RTOS_M4_M7", + "LIKE_CORTEX_M7", + "CORTEX" + ], + "Cortex-A5": [ + "A5", + "CORTEX_A", + "LIKE_CORTEX_A5", + "CORTEX" + ], + "Cortex-A9": [ + "A9", + "CORTEX_A", + "LIKE_CORTEX_A9", + "CORTEX" + ], + "Cortex-M23": [ + "M23", + "CORTEX_M", + "LIKE_CORTEX_M23", + "CORTEX" + ], + "Cortex-M23-NS": [ + "M23", + "M23_NS", + "CORTEX_M", + "LIKE_CORTEX_M23", + "CORTEX" + ], + "Cortex-M33": [ + "M33", + "CORTEX_M", + "LIKE_CORTEX_M33", + "CORTEX" + ], + "Cortex-M33-NS": [ + "M33", + "M33_NS", + "CORTEX_M", + "LIKE_CORTEX_M33", + "CORTEX" + ], + "Cortex-M33F": [ + "M33", + "CORTEX_M", + "LIKE_CORTEX_M33", + "CORTEX" + ], + "Cortex-M33F-NS": [ + "M33", + "M33_NS", + "CORTEX_M", + "LIKE_CORTEX_M33", + "CORTEX" + ], + "Cortex-M33FE": [ + "M33", + "CORTEX_M", + "LIKE_CORTEX_M33", + "CORTEX" + ], + "Cortex-M33FE-NS": [ + "M33", + "M33_NS", + "CORTEX_M", + "LIKE_CORTEX_M33", + "CORTEX" + ], + "Cortex-M55": [ + "M55", + "CORTEX_M", + "LIKE_CORTEX_M55", + "CORTEX" + ], + "Cortex-M55-NS": [ + "M55", + "M55_NS", + "CORTEX_M", + "LIKE_CORTEX_M55", + "CORTEX" + ], + "Cortex-M55F": [ + "M55", + "CORTEX_M", + "LIKE_CORTEX_M55", + "CORTEX" + ], + "Cortex-M55F-NS": [ + "M55", + "M55_NS", + "CORTEX_M", + "LIKE_CORTEX_M55", + "CORTEX" + ], + "Cortex-M55FE": [ + "M55", + "CORTEX_M", + "LIKE_CORTEX_M55", + "CORTEX" + ], + "Cortex-M55FE-NS": [ + "M55", + "M55_NS", + "CORTEX_M", + "LIKE_CORTEX_M55", + "CORTEX" + ] + } +} diff --git a/tools/python/mbed_tools/targets/_internal/exceptions.py b/tools/python/mbed_tools/targets/_internal/exceptions.py new file mode 100644 index 0000000000..8319bf0af4 --- /dev/null +++ b/tools/python/mbed_tools/targets/_internal/exceptions.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Exceptions used internally by the mbed-targets package.""" + +from mbed_tools.targets.exceptions import BoardDatabaseError, MbedTargetsError + + +class BoardAPIError(BoardDatabaseError): + """API request failed.""" + + +class ResponseJSONError(BoardDatabaseError): + """HTTP response JSON parsing failed.""" + + +class TargetsJsonConfigurationError(MbedTargetsError): + """The target definition is invalid.""" diff --git a/tools/python/mbed_tools/targets/_internal/target_attributes.py b/tools/python/mbed_tools/targets/_internal/target_attributes.py new file mode 100644 index 0000000000..9cb4209f4c --- /dev/null +++ b/tools/python/mbed_tools/targets/_internal/target_attributes.py @@ -0,0 +1,134 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Internal helper to retrieve target attribute information. + +This information is parsed from the targets.json configuration file +found in the mbed-os repo. +""" +import logging +import pathlib +from typing import Dict, Any, Set, Optional + +from mbed_tools.lib.exceptions import ToolsError +from mbed_tools.lib.json_helpers import decode_json_file + +from mbed_tools.targets._internal.targets_json_parsers.accumulating_attribute_parser import ( + get_accumulating_attributes_for_target, +) +from mbed_tools.targets._internal.targets_json_parsers.overriding_attribute_parser import ( + get_overriding_attributes_for_target, + get_labels_for_target, +) + +INTERNAL_PACKAGE_DIR = pathlib.Path(__file__).parent +MBED_OS_METADATA_FILE = pathlib.Path(INTERNAL_PACKAGE_DIR, "data", "targets_metadata.json") + +logger = logging.getLogger(__name__) + + +class TargetAttributesError(ToolsError): + """Target attributes error.""" + + +class ParsingTargetsJSONError(TargetAttributesError): + """targets.json parsing failed.""" + + +class TargetNotFoundError(TargetAttributesError): + """Target definition not found in targets.json.""" + + +def get_target_attributes(targets_json_data: dict, target_name: str) -> dict: + """Retrieves attribute data taken from targets.json for a single target. + + Args: + targets_json_data: target definitions from targets.json + target_name: the name of the target (often a Board's board_type). + + Returns: + A dictionary representation of the attributes for the target. + + Raises: + ParsingTargetJSONError: error parsing targets.json + TargetNotFoundError: there is no target attribute data found for that target. + """ + target_attributes = _extract_target_attributes(targets_json_data, target_name) + target_attributes["labels"] = get_labels_for_target(targets_json_data, target_name).union( + _extract_core_labels(target_attributes.get("core", None)) + ) + target_attributes["extra_labels"] = set(target_attributes.get("extra_labels", [])) + target_attributes["features"] = set(target_attributes.get("features", [])) + target_attributes["components"] = set(target_attributes.get("components", [])) + target_attributes["macros"] = set(target_attributes.get("macros", [])) + target_attributes["config"] = _apply_config_overrides( + target_attributes.get("config", {}), target_attributes.get("overrides", {}) + ) + return target_attributes + + +def _extract_target_attributes(all_targets_data: Dict[str, Any], target_name: str) -> dict: + """Extracts the definition for a particular target from all the targets in targets.json. + + Args: + all_targets_data: a dictionary representation of the raw targets.json data. + target_name: the name of the target. + + Returns: + A dictionary representation the target definition. + + Raises: + TargetNotFoundError: no target definition found in targets.json. + """ + if target_name not in all_targets_data: + raise TargetNotFoundError(f"Target attributes for {target_name} not found.") + + # All target definitions are assumed to be public unless specifically set as public=false + if not all_targets_data[target_name].get("public", True): + raise TargetNotFoundError(f"Target attributes for {target_name} not found.") + + target_attributes = get_overriding_attributes_for_target(all_targets_data, target_name) + accumulated_attributes = get_accumulating_attributes_for_target(all_targets_data, target_name) + target_attributes.update(accumulated_attributes) + return target_attributes + + +def _extract_core_labels(target_core: Optional[str]) -> Set[str]: + """Find the labels associated with the target's core. + + Args: + target_core: the target core, set as a build attribute + + Returns: + A list of labels associated with the target's core, or an empty set + if either core is undefined or no labels found for the core. + """ + if target_core: + mbed_os_metadata = decode_json_file(MBED_OS_METADATA_FILE) + return set(mbed_os_metadata["CORE_LABELS"].get(target_core, [])) + return set() + + +def _apply_config_overrides(config: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, Any]: + """Returns the config attribute with any overrides applied. + + Args: + config: the cumulative config settings defined for a target + overrides: the values that need to be changed in the config settings for this target + + Returns: + The config settings with the overrides applied. + + Raises: + TargetsJsonConfigurationError: overrides can't be applied to config settings that aren't already defined + """ + config = config.copy() + for key in overrides: + try: + config[key]["value"] = overrides[key] + except KeyError: + logger.warning( + f"Cannot apply override {key}={overrides[key]}, there is no config setting defined matching that name." + ) + return config diff --git a/tools/python/mbed_tools/targets/_internal/targets_json_parsers/__init__.py b/tools/python/mbed_tools/targets/_internal/targets_json_parsers/__init__.py new file mode 100644 index 0000000000..b563e97ac3 --- /dev/null +++ b/tools/python/mbed_tools/targets/_internal/targets_json_parsers/__init__.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Package for parsing Mbed OS library's targets.json. + +targets.json contains the attribute data for all supported targets. + +Targets are defined using inheritance. Multiple inheritance is allowed (though not encouraged). + +Attributes come in two types - overriding and accumulating. These +use two completely different ways of inheriting so each has its own parser. + +The combined results of these two parsers makes a complete set of attributes for +a target as defined in targets.json. + +For more information about targets.json structure see +https://os.mbed.com/docs/mbed-os/latest/reference/adding-and-configuring-targets.html +""" diff --git a/tools/python/mbed_tools/targets/_internal/targets_json_parsers/accumulating_attribute_parser.py b/tools/python/mbed_tools/targets/_internal/targets_json_parsers/accumulating_attribute_parser.py new file mode 100644 index 0000000000..778bd364cc --- /dev/null +++ b/tools/python/mbed_tools/targets/_internal/targets_json_parsers/accumulating_attribute_parser.py @@ -0,0 +1,219 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Functions for parsing the inheritance for accumulating attributes. + +Accumulating attributes are both defined and can be added to and removed from further down the hierarchy. +The hierarchy is also slightly different to the other fields as it is determined as 'breadth-first' in +multiple inheritance, so targets at a lower level will always take precedence over targets at a higher level. +""" +import itertools +from collections import deque +from typing import Dict, List, Any, Deque + +ACCUMULATING_ATTRIBUTES = ("extra_labels", "macros", "device_has", "features", "components") +MODIFIERS = ("add", "remove") +ALL_ACCUMULATING_ATTRIBUTES = ACCUMULATING_ATTRIBUTES + tuple( + f"{attr}_{suffix}" for attr, suffix in itertools.product(ACCUMULATING_ATTRIBUTES, MODIFIERS) +) + + +def get_accumulating_attributes_for_target(all_targets_data: Dict[str, Any], target_name: str) -> Dict[str, Any]: + """Parses the data for all targets and returns the accumulating attributes for the specified target. + + Args: + all_targets_data: a dictionary representation of the contents of targets.json + target_name: the name of the target to find the attributes of + + Returns: + A dictionary containing all the accumulating attributes for the chosen target + """ + accumulating_order = _targets_accumulate_hierarchy(all_targets_data, target_name) + return _determine_accumulated_attributes(accumulating_order) + + +def _targets_accumulate_hierarchy(all_targets_data: Dict[str, Any], target_name: str) -> List[dict]: + """List all ancestors of a target in order of accumulation inheritance (breadth-first). + + Using a breadth-first traverse of the inheritance tree, return a list of targets in the + order of inheritance, starting with the target itself and finishing with its highest ancestor. + + Eg. An inheritance tree diagram for target "A" below + + D E + | | + B C + |_____| + | + A + + Would give us and inheritance order of [A, B, C, D, E] + + Args: + all_targets_data: a dictionary representation of all the data in a targets.json file + target_name: the name of the target we want to calculate the attributes for + + Returns: + A list of dicts representing each target in the hierarchy. + """ + targets_in_order: List[dict] = [] + + still_to_visit: Deque[dict] = deque() + still_to_visit.appendleft(all_targets_data[target_name]) + + while still_to_visit: + current_target = still_to_visit.popleft() + targets_in_order.append(current_target) + for parent_target in current_target.get("inherits", []): + still_to_visit.append(all_targets_data[parent_target]) + + return targets_in_order + + +def _add_attribute_element( + accumulator: Dict[str, Any], attribute_name: str, elements_to_add: List[Any] +) -> Dict[str, Any]: + """Adds an attribute element to an attribute. + + Args: + accumulator: a store of attributes to be updated + attribute_name: name of the attribute to update + elements_to_add: element to add to the attribute list + + Returns: + The accumulator object with the new elements added + """ + for element in elements_to_add: + accumulator[attribute_name].append(element) + return accumulator + + +def _element_matches(element_to_remove: str, element_to_check: str) -> bool: + """Checks if an element meets the criteria to be removed from list. + + Some attribute elements (eg. macros) can be defined with a number value + eg. MACRO_SOMETHING=5. If we are then instructed to remove + MACRO_SOMETHING then this element needs to be recognised and removed + in addition to exact matches. + + Args: + element_to_remove: the element as taken from list to be removed from an attribute + element_to_check: an element that currently makes up part of an attribute definition + + Returns: + A boolean reflecting whether the element is a match and should be removed + """ + return element_to_check == element_to_remove or element_to_check.startswith(f"{element_to_remove}=") + + +def _remove_attribute_element( + accumulator: Dict[str, Any], attribute_name: str, elements_to_remove: List[Any] +) -> Dict[str, Any]: + """Removes an attribute element from an attribute. + + Args: + accumulator: a store of attributes to be updated + attribute_name: name of the attribute to update + elements_to_remove: element to remove from the attribute list + + Returns: + The accumulator object with the desired elements removed + """ + existing_elements = accumulator[attribute_name] + combinations_to_check = itertools.product(existing_elements, elements_to_remove) + checked_elements_to_remove = [ + existing_element + for existing_element, element in combinations_to_check + if _element_matches(element, existing_element) + ] + + for element in checked_elements_to_remove: + accumulator[attribute_name].remove(element) + return accumulator + + +def _calculate_attribute_elements( + attribute_name: str, starting_state: Dict[str, Any], applicable_accumulation_order: List[dict] +) -> Dict[str, Any]: + """Adds and removes elements for an attribute based on the definitions encountered in the hierarchy. + + This is done via modifying attributes eg. "extra_labels" can be modified by + "extra_labels_add" and "extra_labels_remove". + + Args: + attribute_name: name of the attribute to update + starting_state: the list of elements that defines the starting state of the attribute + applicable_accumulation_order: the targets in the inheritance tree that can contain add and remove modifiers + + Returns: + The elements calculated for an attribute according to the target's inheritance + """ + accumulator = starting_state + for target in reversed(applicable_accumulation_order): + + add_modifier = f"{attribute_name}_add" + if add_modifier in target: + to_add = target[add_modifier] + _add_attribute_element(accumulator, attribute_name, to_add) + + remove_modifier = f"{attribute_name}_remove" + if remove_modifier in target: + to_remove = target[remove_modifier] + _remove_attribute_element(accumulator, attribute_name, to_remove) + + return accumulator + + +def _calculate_attribute_for_target( + attribute_name: str, target: Dict[str, Any], targets_in_order: List[dict] +) -> Dict[str, Any]: + """Finds a single accumulated attribute for a target from its list of ancestors. + + Args: + attribute_name: the name of the accumulating attribute + target: the target we are collecting data for + targets_in_order: the full inheritance hierarchy for a target + + Returns: + A dictionary representation of a single accumulating attribute for that target + """ + starting_state = {attribute_name: target[attribute_name]} + # Reduces the order list to only the targets in the hierarchy between the starting state and the target itself + applicable_accumulation_order = targets_in_order[: targets_in_order.index(target)] + return _calculate_attribute_elements(attribute_name, starting_state, applicable_accumulation_order) + + +def _find_nearest_defined_attribute(targets_in_order: List[dict], attribute_name: str) -> Dict[str, Any]: + """Returns the definition of a particular attribute first encountered in the accumulation order. + + Args: + targets_in_order: the inheritance order for the target, from the target itself to its highest ancestor + attribute_name: the attribute to search for + + Returns: + A dictionary containing the definition of the requested attribute + """ + for target in targets_in_order: + if attribute_name in target: + return _calculate_attribute_for_target(attribute_name, target, targets_in_order) + return {} + + +def _determine_accumulated_attributes(targets_in_order: List[dict]) -> Dict[str, Any]: + """Finds all the accumulated attributes for a target from its list of ancestors. + + Iterates through the order of inheritance (accumulation order) to find the nearest definition + of an attribute, then retraces backwards calculating additions and deletions that modify it. + + Args: + targets_in_order: the inheritance order for the target, from the target itself to its highest ancestor + + Returns: + A dictionary containing all the accumulating attributes for a target + """ + accumulated_attributes = {} + + for attribute_name in ACCUMULATING_ATTRIBUTES: + accumulated_attributes.update(_find_nearest_defined_attribute(targets_in_order, attribute_name)) + return accumulated_attributes diff --git a/tools/python/mbed_tools/targets/_internal/targets_json_parsers/overriding_attribute_parser.py b/tools/python/mbed_tools/targets/_internal/targets_json_parsers/overriding_attribute_parser.py new file mode 100644 index 0000000000..9954a84679 --- /dev/null +++ b/tools/python/mbed_tools/targets/_internal/targets_json_parsers/overriding_attribute_parser.py @@ -0,0 +1,165 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Functions for parsing the inheritance for overriding attributes. + +Overriding attributes are defined and can be overridden further down the hierarchy. + +There are two types - standard and merging. If an attribute is defined as a +merging attribute, only individual attribute elements override, otherwise +the entire attribute is overridden by any later definitions. + +The hierarchy is determined as 'depth-first' in multiple inheritance. +eg. Look first for the attribute in the current target. If not found, +look for the attribute in the first target's parent, then in the parent of the parent and so on. +If not found, look for the property in the rest of the target's parents, relative to the +current inheritance level. + +This means a target on a higher level could potentially override one on a lower level. +""" +from collections import deque +from functools import reduce +from typing import Dict, List, Any, Deque, Set + +from mbed_tools.targets._internal.targets_json_parsers.accumulating_attribute_parser import ALL_ACCUMULATING_ATTRIBUTES + +MERGING_ATTRIBUTES = ("config", "overrides") +NON_OVERRIDING_ATTRIBUTES = ALL_ACCUMULATING_ATTRIBUTES + ("public", "inherits") + + +def get_overriding_attributes_for_target(all_targets_data: Dict[str, Any], target_name: str) -> Dict[str, Any]: + """Parses the data for all targets and returns the overriding attributes for the specified target. + + Args: + all_targets_data: a dictionary representation of the contents of targets.json + target_name: the name of the target to find the attributes of + + Returns: + A dictionary containing all the overriding attributes for the chosen target + """ + override_order = _targets_override_hierarchy(all_targets_data, target_name) + return _determine_overridden_attributes(override_order) + + +def get_labels_for_target(all_targets_data: Dict[str, Any], target_name: str) -> Set[str]: + """The labels for a target are the names of all the boards (public and private) that the board inherits from. + + The order of these labels are not reflective of inheritance order. + + Args: + all_targets_data: a dictionary representation of the contents of targets.json + target_name: the name of the target to find the labels for + + Returns: + A set of names of boards that make up the inheritance tree for the target + """ + targets_in_order = _targets_override_hierarchy(all_targets_data, target_name) + return _extract_target_labels(targets_in_order, target_name) + + +def _targets_override_hierarchy(all_targets_data: Dict[str, Any], target_name: str) -> List[dict]: + """List all ancestors of a target in order of overriding inheritance (depth-first). + + Using a depth-first traverse of the inheritance tree, return a list of targets in the + order of inheritance, starting with the target itself and finishing with its highest ancestor. + + Eg. An inheritance tree diagram for target "A" below + + D E + | | + B C + |_____| + | + A + + Would give us an inheritance order of [A, B, D, C, E] + + Args: + all_targets_data: a dictionary representation of all the data in a targets.json file + target_name: the name of the target we want to calculate the attributes for + + Returns: + A list of dicts representing each target in the hierarchy. + """ + targets_in_order: List[dict] = [] + + still_to_visit: Deque[dict] = deque() + still_to_visit.appendleft(all_targets_data[target_name]) + + while still_to_visit: + current_target = still_to_visit.popleft() + targets_in_order.append(current_target) + for parent_target in reversed(current_target.get("inherits", [])): + still_to_visit.appendleft(all_targets_data[parent_target]) + + return targets_in_order + + +def _determine_overridden_attributes(targets_in_order: List[dict]) -> Dict[str, Any]: + """Finds all the overrideable attributes for a target from its list of ancestors. + + Combines the attributes from all the targets in the hierarchy. Starts from the highest ancestor + reduces down to the target itself, overriding if they define the same attribute. + + Identifies the attributes that should be merged (only their elements overridden, + not the entire attribute definition) and updates their contents. + + Removes any accumulating attributes - they will be handled by a separate parser. + + Args: + targets_in_order: list of targets in order of inheritance, starting with the target up to its highest ancestor + + Returns: + A dictionary containing all the overridden attributes for a target + """ + target_attributes = _reduce_right_list_of_dictionaries(targets_in_order) + for merging_attribute in MERGING_ATTRIBUTES: + override_order_for_single_attribute = [target.get(merging_attribute, {}) for target in targets_in_order] + merged_attribute_elements = _reduce_right_list_of_dictionaries(list(override_order_for_single_attribute)) + if merged_attribute_elements: + target_attributes[merging_attribute] = merged_attribute_elements + overridden_attributes = _remove_unwanted_attributes(target_attributes) + return overridden_attributes + + +def _reduce_right_list_of_dictionaries(list_of_dicts: List[dict]) -> Dict[str, Any]: + """Starting from rightmost dict, merge dicts together, left dict overriding the right.""" + return reduce(lambda x, y: {**x, **y}, reversed(list_of_dicts)) + + +def _remove_unwanted_attributes(target_attributes: Dict[str, Any]) -> Dict[str, Any]: + """Removes all non-overriding attributes. + + Defined in NON_OVERRIDING_ATTRIBUTES. + Accumulating arguments are inherited in a different way that is handled by its own parser. + The 'public' attribute is not inherited. + The 'inherits' attribute is only needed to calculate the hierarchies. + + Args: + target_attributes: a dictionary of attributes for a target + + Returns: + The target attributes with the accumulating attributes removed. + """ + output_dict = target_attributes.copy() + for attribute in NON_OVERRIDING_ATTRIBUTES: + output_dict.pop(attribute, None) + return output_dict + + +def _extract_target_labels(targets_in_order: List[dict], target_name: str) -> Set[str]: + """Collect a set of all the board names from the inherits field in each target in the hierarchy. + + Args: + targets_in_order: list of targets in order of inheritance, starting with the target up to its highest ancestor + target_name: the name of the target to find the labels for + + Returns: + A set of names of boards that make up the inheritance tree for the target + """ + labels = {target_name} + for target in targets_in_order: + for parent in target.get("inherits", []): + labels.add(parent) + return labels diff --git a/tools/python/mbed_tools/targets/board.py b/tools/python/mbed_tools/targets/board.py new file mode 100644 index 0000000000..775e0b0333 --- /dev/null +++ b/tools/python/mbed_tools/targets/board.py @@ -0,0 +1,77 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Representation of an Mbed-Enabled Development Board and related utilities.""" +from dataclasses import dataclass +from typing import Tuple + + +@dataclass(frozen=True, order=True) +class Board: + """Representation of an Mbed-Enabled Development Board. + + Attributes: + board_type: Type of board in format that allows cross-referencing with target definitions. + board_name: Human readable name. + product_code: Uniquely identifies a board for the online compiler. + target_type: A confusing term that is not related to 'target' in other parts of the code. + Identifies if a board in the online database is a `module` or a `platform` (modules are more + complex and often have supplementary sensors, displays etc. A platform is a normal development board). + slug: Used with target_type to identify a board's page on the mbed website. + build_variant: Available build variants for the board. + Can be used in conjunction with board_type for referencing targets. + mbed_os_support: The versions of Mbed OS supported. + mbed_enabled: Whether Mbed OS is supported or not. + """ + + board_type: str + board_name: str + product_code: str + target_type: str + slug: str + build_variant: Tuple[str, ...] + mbed_os_support: Tuple[str, ...] + mbed_enabled: Tuple[str, ...] + + @classmethod + def from_online_board_entry(cls, board_entry: dict) -> "Board": + """Create a new instance of Board from an online database entry. + + Args: + board_entry: A single entity retrieved from the board database API. + """ + board_attrs = board_entry.get("attributes", {}) + board_features = board_attrs.get("features", {}) + + return cls( + # Online database has inconsistently cased board types. + # Since this field is used to match against `targets.json`, we need to ensure consistency is maintained. + board_type=board_attrs.get("board_type", "").upper(), + board_name=board_attrs.get("name", ""), + mbed_os_support=tuple(board_features.get("mbed_os_support", [])), + mbed_enabled=tuple(board_features.get("mbed_enabled", [])), + product_code=board_attrs.get("product_code", ""), + target_type=board_attrs.get("target_type", ""), + slug=board_attrs.get("slug", ""), + # TODO: Remove this hard-coded example after demoing. + # NOTE: Currently we hard-code the build variant for a single board type. + # This is simply so we can demo the tools to PE. This must be removed and replaced with a proper mechanism + # for determining the build variant for all platforms. We probably need to add this information to the + # boards database. + build_variant=tuple(), + ) + + @classmethod + def from_offline_board_entry(cls, board_entry: dict) -> "Board": + """Construct an Board with data from the offline database snapshot.""" + return cls( + board_type=board_entry.get("board_type", ""), + board_name=board_entry.get("board_name", ""), + product_code=board_entry.get("product_code", ""), + target_type=board_entry.get("target_type", ""), + slug=board_entry.get("slug", ""), + mbed_os_support=tuple(board_entry.get("mbed_os_support", [])), + mbed_enabled=tuple(board_entry.get("mbed_enabled", [])), + build_variant=tuple(board_entry.get("build_variant", [])), + ) diff --git a/tools/python/mbed_tools/targets/boards.py b/tools/python/mbed_tools/targets/boards.py new file mode 100644 index 0000000000..ff9af22636 --- /dev/null +++ b/tools/python/mbed_tools/targets/boards.py @@ -0,0 +1,87 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Interface to the Board Database.""" +import json + +from dataclasses import asdict +from collections.abc import Set +from typing import Iterator, Iterable, Any, Callable + +from mbed_tools.targets._internal import board_database + +from mbed_tools.targets.exceptions import UnknownBoard +from mbed_tools.targets.board import Board + + +class Boards(Set): + """Interface to the Board Database. + + Boards is initialised with an Iterable[Board]. The classmethods + can be used to construct Boards with data from either the online or offline database. + """ + + @classmethod + def from_offline_database(cls) -> "Boards": + """Initialise with the offline board database. + + Raises: + BoardDatabaseError: Could not retrieve data from the board database. + """ + return cls(Board.from_offline_board_entry(b) for b in board_database.get_offline_board_data()) + + @classmethod + def from_online_database(cls) -> "Boards": + """Initialise with the online board database. + + Raises: + BoardDatabaseError: Could not retrieve data from the board database. + """ + return cls(Board.from_online_board_entry(b) for b in board_database.get_online_board_data()) + + def __init__(self, boards_data: Iterable["Board"]) -> None: + """Initialise with a list of boards. + + Args: + boards_data: iterable of board data from a board database source. + """ + self._boards_data = tuple(boards_data) + + def __iter__(self) -> Iterator["Board"]: + """Yield an Board on each iteration.""" + for board in self._boards_data: + yield board + + def __len__(self) -> int: + """Return the number of boards.""" + return len(self._boards_data) + + def __contains__(self, board: object) -> Any: + """Check if a board is in the collection of boards. + + Args: + board: An instance of Board. + """ + if not isinstance(board, Board): + return False + + return any(x == board for x in self) + + def get_board(self, matching: Callable) -> Board: + """Returns first Board for which `matching` returns True. + + Args: + matching: A function which will be called for each board in database + + Raises: + UnknownBoard: the given product code was not found in the board database. + """ + try: + return next(board for board in self if matching(board)) + except StopIteration: + raise UnknownBoard() + + def json_dump(self) -> str: + """Return the contents of the board database as a json string.""" + return json.dumps([asdict(b) for b in self], indent=4) diff --git a/tools/python/mbed_tools/targets/env.py b/tools/python/mbed_tools/targets/env.py new file mode 100644 index 0000000000..ddf4afc9f8 --- /dev/null +++ b/tools/python/mbed_tools/targets/env.py @@ -0,0 +1,71 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Environment options for `mbed-targets`. + +All the env configuration options can be set either via environment variables or using a `.env` file +containing the variable definitions as follows: + +``` +VARIABLE=value +``` + +Environment variables take precendence, meaning the values set in the file will be overriden +by any values previously set in your environment. + +.. WARNING:: + Do not upload `.env` files containing private tokens to version control! If you use this package + as a dependency of your project, please ensure to include the `.env` in your `.gitignore`. +""" +import os + +import dotenv + +dotenv.load_dotenv(dotenv.find_dotenv(usecwd=True)) + + +class Env: + """Provides access to environment variables. + + Ensures variables are reloaded when environment changes during runtime. + """ + + @property + def MBED_API_AUTH_TOKEN(self) -> str: + """Token to use when accessing online API. + + Mbed Targets uses the online mbed board database at os.mbed.com as its data source. + A snapshot of the board database is shipped with the package, for faster lookup of known + boards. Only public boards are stored in the database snapshot. If you are fetching data + for a private board, mbed-targets will need to contact the online database. + + To fetch data about private boards from the online database, the user must have an account + on os.mbed.com and be member of a vendor team that has permissions to see the private board. + An authentication token for the team member must be provided in an environment variable named + `MBED_API_AUTH_TOKEN`. + """ + return os.getenv("MBED_API_AUTH_TOKEN", "") + + @property + def MBED_DATABASE_MODE(self) -> str: + """Database mode to use when retrieving board data. + + Mbed Targets supports an online and offline mode, which controls where to look up the board database. + + The board lookup can be from either the online or offline database, depending + on the value of an environment variable called `MBED_DATABASE_MODE`. + + The mode can be set to one of the following: + + - `AUTO`: the offline database is searched first, if the board isn't found the online database is searched. + - `ONLINE`: the online database is always used. + - `OFFLINE`: the offline database is always used. + + If `MBED_DATABASE_MODE` is not set, it defaults to `AUTO`. + """ + return os.getenv("MBED_DATABASE_MODE", "AUTO") + + +env = Env() +"""Instance of `Env` class.""" diff --git a/tools/python/mbed_tools/targets/exceptions.py b/tools/python/mbed_tools/targets/exceptions.py new file mode 100644 index 0000000000..5e0e056986 --- /dev/null +++ b/tools/python/mbed_tools/targets/exceptions.py @@ -0,0 +1,27 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Public exceptions exposed by the package.""" + +from mbed_tools.lib.exceptions import ToolsError + + +class MbedTargetsError(ToolsError): + """Base exception for mbed-targets.""" + + +class TargetError(ToolsError): + """Target definition cannot be retrieved.""" + + +class UnknownBoard(MbedTargetsError): + """Requested board was not found.""" + + +class UnsupportedMode(MbedTargetsError): + """The Database Mode is unsupported.""" + + +class BoardDatabaseError(MbedTargetsError): + """Failed to get the board data from the database.""" diff --git a/tools/python/mbed_tools/targets/get_board.py b/tools/python/mbed_tools/targets/get_board.py new file mode 100644 index 0000000000..65c8dc6ba2 --- /dev/null +++ b/tools/python/mbed_tools/targets/get_board.py @@ -0,0 +1,112 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Interface for accessing Mbed-Enabled Development Board data. + +An instance of `mbed_tools.targets.board.Board` can be retrieved by calling one of the public functions. +""" +import logging +from enum import Enum +from typing import Callable + +from mbed_tools.targets.env import env +from mbed_tools.targets.exceptions import UnknownBoard, UnsupportedMode, BoardDatabaseError +from mbed_tools.targets.board import Board +from mbed_tools.targets.boards import Boards + + +logger = logging.getLogger(__name__) + + +def get_board_by_product_code(product_code: str) -> Board: + """Returns first `mbed_tools.targets.board.Board` matching given product code. + + Args: + product_code: the product code to look up in the database. + + Raises: + UnknownBoard: a board with a matching product code was not found. + """ + return get_board(lambda board: board.product_code == product_code) + + +def get_board_by_online_id(slug: str, target_type: str) -> Board: + """Returns first `mbed_tools.targets.board.Board` matching given online id. + + Args: + slug: The slug to look up in the database. + target_type: The target type to look up in the database, normally one of `platform` or `module`. + + Raises: + UnknownBoard: a board with a matching slug and target type could not be found. + """ + matched_slug = slug.casefold() + return get_board(lambda board: board.slug.casefold() == matched_slug and board.target_type == target_type) + + +def get_board_by_jlink_slug(slug: str) -> Board: + """Returns first `mbed-tools.targets.board.Board` matching given slug. + + With J-Link, the slug is extracted from a board manufacturer URL, and may not match + the Mbed slug. The J-Link slug is compared against the slug, board_name and + board_type of entries in the board database until a match is found. + + Args: + slug: the J-Link slug to look up in the database. + + Raises: + UnknownBoard: a board matching the slug was not found. + """ + matched_slug = slug.casefold() + return get_board( + lambda board: any(matched_slug == c.casefold() for c in [board.slug, board.board_name, board.board_type]) + ) + + +def get_board(matching: Callable) -> Board: + """Returns first `mbed_tools.targets.board.Board` for which `matching` is True. + + Uses database mode configured in the environment. + + Args: + matching: A function which will be called to test matching conditions for each board in database. + + Raises: + UnknownBoard: a board matching the criteria could not be found in the board database. + """ + database_mode = _get_database_mode() + + if database_mode == _DatabaseMode.OFFLINE: + logger.info("Using the offline database (only) to identify boards.") + return Boards.from_offline_database().get_board(matching) + + if database_mode == _DatabaseMode.ONLINE: + logger.info("Using the online database (only) to identify boards.") + return Boards.from_online_database().get_board(matching) + try: + logger.info("Using the offline database to identify boards.") + return Boards.from_offline_database().get_board(matching) + except UnknownBoard: + logger.info("Unable to identify a board using the offline database, trying the online database.") + try: + return Boards.from_online_database().get_board(matching) + except BoardDatabaseError: + logger.error("Unable to access the online database to identify a board.") + raise UnknownBoard() + + +class _DatabaseMode(Enum): + """Selected database mode.""" + + OFFLINE = 0 + ONLINE = 1 + AUTO = 2 + + +def _get_database_mode() -> _DatabaseMode: + database_mode = env.MBED_DATABASE_MODE + try: + return _DatabaseMode[database_mode] + except KeyError: + raise UnsupportedMode(f"{database_mode} is not a supported database mode.") diff --git a/tools/python/mbed_tools/targets/get_target.py b/tools/python/mbed_tools/targets/get_target.py new file mode 100644 index 0000000000..3cc7bd1984 --- /dev/null +++ b/tools/python/mbed_tools/targets/get_target.py @@ -0,0 +1,44 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Interface for accessing Targets from Mbed OS's targets.json. + +An instance of `mbed_tools.targets.target.Target` +can be retrieved by calling one of the public functions. +""" +from mbed_tools.targets.exceptions import TargetError +from mbed_tools.targets._internal import target_attributes + + +def get_target_by_name(name: str, targets_json_data: dict) -> dict: + """Returns a dictionary of attributes for the target whose name matches the name given. + + The target is as defined in the targets.json file found in the Mbed OS library. + + Args: + name: the name of the Target to be returned + targets_json_data: target definitions from targets.json + + Raises: + TargetError: an error has occurred while fetching target + """ + try: + return target_attributes.get_target_attributes(targets_json_data, name) + except (FileNotFoundError, target_attributes.TargetAttributesError) as e: + raise TargetError(e) from e + + +def get_target_by_board_type(board_type: str, targets_json_data: dict) -> dict: + """Returns the target whose name matches a board's build_type. + + The target is as defined in the targets.json file found in the Mbed OS library. + + Args: + board_type: a board's board_type (see `mbed_tools.targets.board.Board`) + targets_json_data: target definitions from targets.json + + Raises: + TargetError: an error has occurred while fetching target + """ + return get_target_by_name(board_type, targets_json_data) diff --git a/tools/python/memap/__init__.py b/tools/python/memap/__init__.py new file mode 100644 index 0000000000..2bae17afc8 --- /dev/null +++ b/tools/python/memap/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright (c) 2020-2023 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# \ No newline at end of file diff --git a/tools/memap.py b/tools/python/memap/memap.py similarity index 99% rename from tools/memap.py rename to tools/python/memap/memap.py index ff54331c80..2e2ddbc991 100644 --- a/tools/memap.py +++ b/tools/python/memap/memap.py @@ -40,7 +40,7 @@ from future.utils import with_metaclass ROOT = abspath(join(dirname(__file__), "..")) path.insert(0, ROOT) -from tools.utils import ( +from .utils import ( argparse_filestring_type, argparse_lowercase_hyphen_type, argparse_uppercase_type diff --git a/tools/memap_flamegraph.html b/tools/python/memap/memap_flamegraph.html similarity index 100% rename from tools/memap_flamegraph.html rename to tools/python/memap/memap_flamegraph.html diff --git a/tools/utils.py b/tools/python/memap/utils.py similarity index 100% rename from tools/utils.py rename to tools/python/memap/utils.py diff --git a/tools/python/python_tests/__init__.py b/tools/python/python_tests/__init__.py new file mode 100644 index 0000000000..2bae17afc8 --- /dev/null +++ b/tools/python/python_tests/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright (c) 2020-2023 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# \ No newline at end of file diff --git a/tools/python/python_tests/mbed_greentea/__init__.py b/tools/python/python_tests/mbed_greentea/__init__.py new file mode 100644 index 0000000000..196ab3986e --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/__init__.py @@ -0,0 +1,22 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. +""" + +"""! @package mbed-greentea-test + +Unit tests for mbed-greentea test suite + +""" diff --git a/tools/python/python_tests/mbed_greentea/basic.py b/tools/python/python_tests/mbed_greentea/basic.py new file mode 100644 index 0000000000..3b68a58fd1 --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/basic.py @@ -0,0 +1,35 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 + +class BasicTestCase(unittest.TestCase): + """ Basic true asserts to see that testing is executed + """ + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_example(self): + self.assertEqual(True, True) + self.assertNotEqual(True, False) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_cli.py b/tools/python/python_tests/mbed_greentea/mbed_gt_cli.py new file mode 100644 index 0000000000..b8fcd93bed --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_cli.py @@ -0,0 +1,151 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 six +import sys +import unittest + +from mbed_greentea import mbed_greentea_cli +from mbed_greentea.tests_spec import TestSpec + +test_spec_def = { + "builds": { + "K64F-ARM": { + "platform": "K64F", + "toolchain": "ARM", + "base_path": "./.build/K64F/ARM", + "baud_rate": 115200, + "tests": { + "mbed-drivers-test-generic_tests":{ + "binaries":[ + { + "binary_type": "bootable", + "path": "./.build/K64F/ARM/mbed-drivers-test-generic_tests.bin" + } + ] + }, + "mbed-drivers-test-c_strings":{ + "binaries":[ + { + "binary_type": "bootable", + "path": "./.build/K64F/ARM/mbed-drivers-test-c_strings.bin" + } + ] + } + } + } + } +} + +class GreenteaCliFunctionality(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_get_greentea_version(self): + version = mbed_greentea_cli.get_greentea_version() + + self.assertIs(type(version), str) + + version_list = version.split('.') + + self.assertEqual(version_list[0].isdigit(), True) + self.assertEqual(version_list[1].isdigit(), True) + self.assertEqual(version_list[2].isdigit(), True) + + def test_print_version(self): + version = mbed_greentea_cli.get_greentea_version() + + old_stdout = sys.stdout + sys.stdout = stdout_capture = six.StringIO() + mbed_greentea_cli.print_version() + sys.stdout = old_stdout + + printed_version = stdout_capture.getvalue().splitlines()[0] + self.assertEqual(printed_version, version) + + def test_get_hello_string(self): + version = mbed_greentea_cli.get_greentea_version() + hello_string = mbed_greentea_cli.get_hello_string() + + self.assertIs(type(version), str) + self.assertIs(type(hello_string), str) + self.assertIn(version, hello_string) + + def test_get_local_host_tests_dir_invalid_path(self): + test_path = mbed_greentea_cli.get_local_host_tests_dir("invalid-path") + self.assertEqual(test_path, None) + + def test_get_local_host_tests_dir_valid_path(self): + path = "." + test_path = mbed_greentea_cli.get_local_host_tests_dir(path) + self.assertEqual(test_path, path) + + def test_get_local_host_tests_dir_default_path(self): + import os + import shutil + import tempfile + + curr_dir = os.getcwd() + test1_dir = tempfile.mkdtemp() + test2_dir = os.mkdir(os.path.join(test1_dir, "test")) + test3_dir = os.mkdir(os.path.join(test1_dir, "test", "host_tests")) + + os.chdir(test1_dir) + + test_path = mbed_greentea_cli.get_local_host_tests_dir("") + self.assertEqual(test_path, "./test/host_tests") + + os.chdir(curr_dir) + shutil.rmtree(test1_dir) + + def test_create_filtered_test_list(self): + test_spec = TestSpec() + test_spec.parse(test_spec_def) + test_build = test_spec.get_test_builds()[0] + + test_list = mbed_greentea_cli.create_filtered_test_list(test_build.get_tests(), + 'mbed-drivers-test-generic_*', + None, + test_spec=test_spec) + self.assertEqual(set(test_list.keys()), set(['mbed-drivers-test-generic_tests'])) + + test_list = mbed_greentea_cli.create_filtered_test_list(test_build.get_tests(), + '*_strings', + None, + test_spec=test_spec) + self.assertEqual(set(test_list.keys()), set(['mbed-drivers-test-c_strings'])) + + test_list = mbed_greentea_cli.create_filtered_test_list(test_build.get_tests(), + 'mbed*s', + None, + test_spec=test_spec) + expected = set(['mbed-drivers-test-c_strings', 'mbed-drivers-test-generic_tests']) + self.assertEqual(set(test_list.keys()), expected) + + test_list = mbed_greentea_cli.create_filtered_test_list(test_build.get_tests(), + '*-drivers-*', + None, + test_spec=test_spec) + expected = set(['mbed-drivers-test-c_strings', 'mbed-drivers-test-generic_tests']) + self.assertEqual(set(test_list.keys()), expected) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_cmake_handlers.py b/tools/python/python_tests/mbed_greentea/mbed_gt_cmake_handlers.py new file mode 100644 index 0000000000..7681c1905e --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_cmake_handlers.py @@ -0,0 +1,147 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 +import mock +import six +import sys +import unittest + +from mbed_greentea import cmake_handlers, tests_spec + +class GreenteaCmakeHandlers(unittest.TestCase): + """ Basic true asserts to see that testing is executed + """ + + def setUp(self): + self.ctesttestfile = """# CMake generated Testfile for +# Source directory: c:/Work2/mbed-client/test +# Build directory: c:/Work2/mbed-client/build/frdm-k64f-gcc/test +# +# This file includes the relevant testing commands required for +# testing this directory and lists subdirectories to be tested as well. +add_test(mbed-client-test-mbedclient-smokeTest "mbed-client-test-mbedclient-smokeTest") +add_test(mbed-client-test-helloworld-mbedclient "mbed-client-test-helloworld-mbedclient") +""" + + def tearDown(self): + pass + + def test_example(self): + self.assertEqual(True, True) + self.assertNotEqual(True, False) + + def test_parse_ctesttestfile_line(self): + LINK_TARGET = '/dir/to/target' + BINARY_TYPE = '.bin' + + result = {} + no_skipped_lines = 0 + for line in self.ctesttestfile.splitlines(): + line_parse = cmake_handlers.parse_ctesttestfile_line(LINK_TARGET, BINARY_TYPE, line, verbose=True) + if line_parse: + test_case, test_case_path = line_parse + result[test_case] = test_case_path + else: + no_skipped_lines += 1 + + self.assertIn('mbed-client-test-mbedclient-smokeTest', result) + self.assertIn('mbed-client-test-helloworld-mbedclient', result) + + for test_case, test_case_path in result.items(): + self.assertEqual(test_case_path.startswith(LINK_TARGET), True) + self.assertEqual(test_case_path.endswith(BINARY_TYPE), True) + + self.assertEqual(len(result), 2) # We parse two entries + self.assertEqual(no_skipped_lines, 6) # We skip six lines in this file + + def test_load_ctest_testsuite_missing_link_target(self): + null_link_target = None + null_suite = cmake_handlers.load_ctest_testsuite(null_link_target) + self.assertEqual(null_suite, {}) + + + def test_load_ctest_testsuite(self): + root_path = os.path.dirname(os.path.realpath(__file__)) + emty_path = os.path.join(root_path, "resources", "empty") + full_path = os.path.join(root_path, "resources", "not-empty") + + # Empty LINK_TARGET + empty_link_target = emty_path + empty_suite = cmake_handlers.load_ctest_testsuite(empty_link_target) + self.assertEqual(empty_suite, {}) + + # Not empty LINK_TARGET + link_target = full_path + test_suite = cmake_handlers.load_ctest_testsuite(link_target) + self.assertIsNotNone(test_suite) + self.assertIn('mbed-client-test-mbedclient-smokeTest', test_suite) + self.assertIn('mbed-client-test-helloworld-mbedclient', test_suite) + + + def test_list_binaries_for_targets(self): + root_path = os.path.dirname(os.path.realpath(__file__)) + null_path = os.path.join(root_path, "resources", "empty", "test") + full_path = os.path.join(root_path, "resources") + + def run_and_capture(path, verbose=False): + old_stdout = sys.stdout + sys.stdout = stdout_capture = six.StringIO() + cmake_handlers.list_binaries_for_targets(build_dir=path, verbose_footer=verbose) + sys.stdout = old_stdout + + return stdout_capture.getvalue() + + # Test empty target directory + output = run_and_capture(null_path) + self.assertTrue("no tests found in current location" in output) + + # Test empty target directory (Verbose output) + output = run_and_capture(null_path, verbose=True) + self.assertTrue("no tests found in current location" in output) + self.assertTrue("Example: execute 'mbedgt -t TARGET_NAME -n TEST_NAME' to run test TEST_NAME for target TARGET_NAME" in output) + + # Test non-empty target directory + output = run_and_capture(full_path) + self.assertTrue("available tests for target" in output) + + + def test_list_binaries_for_builds(self): + root_path = os.path.dirname(os.path.realpath(__file__)) + spec_path = os.path.join(root_path, "resources", "test_spec.json") + + spec = tests_spec.TestSpec(spec_path) + + for verbose in [True, False]: + # Capture logging output + old_stdout = sys.stdout + sys.stdout = stdout_capture = six.StringIO() + cmake_handlers.list_binaries_for_builds(spec, verbose_footer=verbose) + sys.stdout = old_stdout + + output = stdout_capture.getvalue() + self.assertTrue("available tests for build 'K64F-ARM'" in output) + self.assertTrue("available tests for build 'K64F-GCC'" in output) + self.assertTrue("tests-example-1" in output) + self.assertTrue("tests-example-2" in output) + self.assertTrue("tests-example-7" in output) + + if verbose == True: + self.assertTrue("Example: execute" in output) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_coverage_api.py b/tools/python/python_tests/mbed_greentea/mbed_gt_coverage_api.py new file mode 100644 index 0000000000..65ae0e81eb --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_coverage_api.py @@ -0,0 +1,63 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 +import unittest +from mbed_greentea import mbed_coverage_api + + +class GreenteaCoverageAPI(unittest.TestCase): + + def setUp(self): + pass + + def test_x(self): + pass + + def test_coverage_pack_hex_payload(self): + # This function takesstring as input + r = mbed_coverage_api.coverage_pack_hex_payload('') + self.assertEqual(bytearray(b''), r) + + r = mbed_coverage_api.coverage_pack_hex_payload('6164636772') + self.assertEqual(bytearray(b'adcgr'), r) + + r = mbed_coverage_api.coverage_pack_hex_payload('.') # '.' -> 0x00 + self.assertEqual(bytearray(b'\x00'), r) + + r = mbed_coverage_api.coverage_pack_hex_payload('...') # '.' -> 0x00 + self.assertEqual(bytearray(b'\x00\x00\x00'), r) + + r = mbed_coverage_api.coverage_pack_hex_payload('.6164636772.') # '.' -> 0x00 + self.assertEqual(bytearray(b'\x00adcgr\x00'), r) + + def test_coverage_dump_file_valid(self): + import tempfile + + payload = bytearray(b'PAYLOAD') + handle, path = tempfile.mkstemp("test_file") + mbed_coverage_api.coverage_dump_file(".", path, payload) + + with open(path, 'r') as f: + read_data = f.read() + + self.assertEqual(read_data, payload.decode("utf-8", "ignore")) + os.close(handle) + os.remove(path) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_greentea_dlm.py b/tools/python/python_tests/mbed_greentea/mbed_gt_greentea_dlm.py new file mode 100644 index 0000000000..287262ccd5 --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_greentea_dlm.py @@ -0,0 +1,142 @@ +""" +mbed SDK +Copyright (c) 2011-2017 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 +import six +import sys +import tempfile +import unittest + +from lockfile import LockFile +from mock import patch +from mbed_greentea import mbed_greentea_dlm + + +class GreenteaDlmFunctionality(unittest.TestCase): + def setUp(self): + temp_dir = tempfile.mkdtemp() + self.home_tools_patch = patch('mbed_os_tools.test.mbed_greentea_dlm.HOME_DIR', temp_dir) + self.home_local_patch = patch('mbed_greentea.mbed_greentea_dlm.HOME_DIR', temp_dir) + kettle_dir = os.path.join(temp_dir, mbed_greentea_dlm.GREENTEA_HOME_DIR, mbed_greentea_dlm.GREENTEA_KETTLE) + self.kettle_tools_patch = patch('mbed_os_tools.test.mbed_greentea_dlm.GREENTEA_KETTLE_PATH', kettle_dir) + self.kettle_local_patch = patch('mbed_greentea.mbed_greentea_dlm.GREENTEA_KETTLE_PATH', kettle_dir) + + self.home_tools_patch.start() + self.home_local_patch.start() + self.kettle_tools_patch.start() + self.kettle_local_patch.start() + + def tearDown(self): + self.home_tools_patch.stop() + self.home_local_patch.stop() + self.kettle_tools_patch.stop() + self.kettle_local_patch.stop() + + def test_greentea_home_dir_init(self): + mbed_greentea_dlm.greentea_home_dir_init() + + path = os.path.join(mbed_greentea_dlm.HOME_DIR, mbed_greentea_dlm.GREENTEA_HOME_DIR) + self.assertTrue(os.path.exists(path)) + + def test_greentea_get_app_sem(self): + sem, name, uuid = mbed_greentea_dlm.greentea_get_app_sem() + self.assertIsNotNone(sem) + self.assertIsNotNone(name) + self.assertIsNotNone(uuid) + + def test_greentea_get_target_lock(self): + lock = mbed_greentea_dlm.greentea_get_target_lock("target-id-2") + path = os.path.join(mbed_greentea_dlm.HOME_DIR, mbed_greentea_dlm.GREENTEA_HOME_DIR, "target-id-2") + self.assertIsNotNone(lock) + self.assertEqual(path, lock.path) + + def test_greentea_get_global_lock(self): + lock = mbed_greentea_dlm.greentea_get_global_lock() + path = os.path.join(mbed_greentea_dlm.HOME_DIR, mbed_greentea_dlm.GREENTEA_HOME_DIR, "glock.lock") + self.assertIsNotNone(lock) + self.assertEqual(path, lock.path) + + def test_get_json_data_from_file_invalid_file(self): + result = mbed_greentea_dlm.get_json_data_from_file("null_file") + self.assertIsNone(result) + + def test_get_json_data_from_file_invalid_json(self): + path = os.path.join(mbed_greentea_dlm.HOME_DIR, "test") + + with open(path, "w") as f: + f.write("invalid json") + + result = mbed_greentea_dlm.get_json_data_from_file(path) + self.assertEqual(result, None) + + os.remove(path) + + def test_get_json_data_from_file_valid_file(self): + path = os.path.join(mbed_greentea_dlm.HOME_DIR, "test") + + with open(path, "w") as f: + f.write("{}") + + result = mbed_greentea_dlm.get_json_data_from_file(path) + self.assertEqual(result, {}) + + os.remove(path) + + def test_greentea_update_kettle(self): + uuid = "001" + mbed_greentea_dlm.greentea_update_kettle(uuid) + + data = mbed_greentea_dlm.get_json_data_from_file(mbed_greentea_dlm.GREENTEA_KETTLE_PATH) + self.assertIsNotNone(data) + self.assertIn("start_time", data[uuid]) + self.assertIn("cwd", data[uuid]) + self.assertIn("locks", data[uuid]) + + self.assertEqual(data[uuid]["cwd"], os.getcwd()) + self.assertEqual(data[uuid]["locks"], []) + + # Check greentea_kettle_info() + output = mbed_greentea_dlm.greentea_kettle_info().splitlines() + line = output[3] + self.assertIn(os.getcwd(), line) + self.assertIn(uuid, line) + + # Test greentea_acquire_target_id + target_id = "999" + mbed_greentea_dlm.greentea_acquire_target_id(target_id, uuid) + data = mbed_greentea_dlm.get_json_data_from_file(mbed_greentea_dlm.GREENTEA_KETTLE_PATH) + self.assertIn(uuid, data) + self.assertIn("locks", data[uuid]) + self.assertIn(target_id, data[uuid]["locks"]) + + # Test greentea_release_target_id + mbed_greentea_dlm.greentea_release_target_id(target_id, uuid) + data = mbed_greentea_dlm.get_json_data_from_file(mbed_greentea_dlm.GREENTEA_KETTLE_PATH) + self.assertIn(uuid, data) + self.assertIn("locks", data[uuid]) + self.assertNotIn(target_id, data[uuid]["locks"]) + + # Test greentea_acquire_target_id_from_list + target_id = "999" + result = mbed_greentea_dlm.greentea_acquire_target_id_from_list([target_id], uuid) + data = mbed_greentea_dlm.get_json_data_from_file(mbed_greentea_dlm.GREENTEA_KETTLE_PATH) + self.assertEqual(result, target_id) + self.assertIn(uuid, data) + self.assertIn("locks", data[uuid]) + self.assertIn(target_id, data[uuid]["locks"]) + + # Check greentea_clean_kettle() + mbed_greentea_dlm.greentea_clean_kettle(uuid) + data = mbed_greentea_dlm.get_json_data_from_file(mbed_greentea_dlm.GREENTEA_KETTLE_PATH) + self.assertEqual(data, {}) diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_hooks.py b/tools/python/python_tests/mbed_greentea/mbed_gt_hooks.py new file mode 100644 index 0000000000..78fdca01ff --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_hooks.py @@ -0,0 +1,141 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 six +import sys +import unittest + +from mock import patch +from mbed_greentea.mbed_greentea_hooks import GreenteaCliTestHook +from mbed_greentea.mbed_greentea_hooks import LcovHook + +class GreenteaCliTestHookTest(unittest.TestCase): + + def setUp(self): + self.cli_hooks = GreenteaCliTestHook('test_hook', 'some command') + self.lcov_hooks = LcovHook('test_hook', 'some command') + + def tearDown(self): + pass + + def test_format_before_run(self): + command = LcovHook.format_before_run("command {expansion}", None, verbose=True) + self.assertEqual(command, "command {expansion}") + + expansions = ["test", "test2"] + command = LcovHook.format_before_run("command {expansion}", {"expansion": expansions}, verbose=True) + self.assertEqual(command, "command ['test', 'test2']") + + def test_expand_parameters_with_1_list(self): + # Simple list + self.assertEqual('new_value_1 new_value_2', + self.cli_hooks.expand_parameters('[{token_list}]', { + "token_list" : ['new_value_1', 'new_value_2'] + })) + + # List with prefix + self.assertEqual('-a new_value_1 -a new_value_2', + self.cli_hooks.expand_parameters('[-a {token_list}]', { + "token_list" : ['new_value_1', 'new_value_2'] + })) + + # List with prefix and extra text + self.assertEqual('-a /path/to/new_value_1 -a /path/to/new_value_2', + self.cli_hooks.expand_parameters('[-a /path/to/{token_list}]', { + "token_list" : ['new_value_1', 'new_value_2'] + })) + + def test_expand_parameters_with_2_lists(self): + self.assertEqual('ytA T1', + self.cli_hooks.expand_parameters('[{yt_target_list} {test_list}]', { + "test_list" : ['T1'], + "yt_target_list" : ['ytA'], + })) + + self.assertEqual('ytA T1 ytA T2 ytA T3 ytB T1 ytB T2 ytB T3 ytC T1 ytC T2 ytC T3', + self.cli_hooks.expand_parameters('[{yt_target_list} {test_list}]', { + "test_list" : ['T1', 'T2', 'T3'], + "yt_target_list" : ['ytA', 'ytB', 'ytC'], + })) + + # In this test we expect {yotta_target_name} token to stay untouched because it is not declared as a list + self.assertEqual('lcov -a /build/{yotta_target_name}/mbed-drivers-test-basic.info -a /build/{yotta_target_name}/mbed-drivers-test-hello.info', + self.cli_hooks.expand_parameters('lcov [-a /build/{yotta_target_name}/{test_list}.info]', { + "test_list" : ['mbed-drivers-test-basic', 'mbed-drivers-test-hello'], + "yotta_target_name" : 'frdm-k64f-gcc' + })) + + def test_expand_parameters_exceptions(self): + # Here [] is reduced to empty string + self.assertEqual('', + self.cli_hooks.expand_parameters('[]', { + "test_list" : ['mbed-drivers-test-basic', 'mbed-drivers-test-hello'], + "yotta_target_name" : 'frdm-k64f-gcc' + })) + + # Here [some string] is reduced to empty string + self.assertEqual('', + self.cli_hooks.expand_parameters('[some string]', { + "test_list" : ['mbed-drivers-test-basic', 'mbed-drivers-test-hello'], + "yotta_target_name" : 'frdm-k64f-gcc' + })) + + # Here [some string] is reduced to empty string + self.assertEqual('-+=abc', + self.cli_hooks.expand_parameters('-+=[some string]abc', { + "test_list" : ['mbed-drivers-test-basic', 'mbed-drivers-test-hello'], + "yotta_target_name" : 'frdm-k64f-gcc' + })) + + self.assertEqual(None, + self.cli_hooks.expand_parameters('some text without square brackets but with tokes: {test_list} and {yotta_target_name}', { + "test_list" : ['mbed-drivers-test-basic', 'mbed-drivers-test-hello'], + "yotta_target_name" : 'frdm-k64f-gcc' + })) + + def test_format_before_run_with_1_list_1_string(self): + # {test_name_list} should expand [] list twice + # {yotta_target_name} should not be used to expand, only to replace + self.assertEqual('build path = -a /build/frdm-k64f-gcc/mbed-drivers-test-basic -a /build/frdm-k64f-gcc/mbed-drivers-test-hello', + self.cli_hooks.format_before_run('build path = [-a /build/{yotta_target_name}/{test_name_list}]', { + "test_name_list" : ['mbed-drivers-test-basic', 'mbed-drivers-test-hello'], + "yotta_target_name" : 'frdm-k64f-gcc', + })) + + @patch('os.path.exists') + @patch('os.path.getsize') + def test_check_if_file_exists(self, pathGetsize_mock, pathExists_mock): + pathExists_mock.return_value = True + self.assertEqual('-a ./build/frdm-k64f-gcc.info', + self.lcov_hooks.check_if_file_exists_or_is_empty('(-a <<./build/frdm-k64f-gcc.info>>)')) + + pathExists_mock.return_value = False + self.assertEqual('', + self.lcov_hooks.check_if_file_exists_or_is_empty('(-a <<./build/frdm-k64f-gcc.info>>)')) + + @patch('os.path.exists') + @patch('os.path.getsize') + def test_check_if_file_is_empty(self, pathGetsize_mock, pathExists_mock): + pathExists_mock.return_value = True + pathGetsize_mock.return_value = 1 + self.assertEqual('-a ./build/frdm-k64f-gcc.info', + self.lcov_hooks.check_if_file_exists_or_is_empty('(-a <<./build/frdm-k64f-gcc.info>>)')) + + pathExists_mock.return_value = True + pathGetsize_mock.return_value = 0 + self.assertEqual('', + self.lcov_hooks.check_if_file_exists_or_is_empty('(-a <<./build/frdm-k64f-gcc.info>>)')) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_report_api.py b/tools/python/python_tests/mbed_greentea/mbed_gt_report_api.py new file mode 100644 index 0000000000..2c973a14bf --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_report_api.py @@ -0,0 +1,175 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 json +import os +import six +import sys +import tempfile +import unittest + +from mock import patch +from mbed_greentea import mbed_report_api + +class GreenteaReportApiFunctionality(unittest.TestCase): + def setUp(self): + self.test_case_data = { + "K64F-ARM": { + "suite": { + "testcase_result": { + "case-1": { + "duration": 1.00, + "time_start": 2.00, + "time_end": 3.00, + "result_text": "FAIL", + "passed": 10, + "failed": 10 + }, + "case-2": { + "duration": 1.00, + "time_start": 2.00, + "time_end": 3.00, + "result_text": "SKIPPED", + "passed": 10, + "failed": 10 + }, + "case-3": { + "duration": 1.00, + "time_start": 2.00, + "time_end": 3.00, + "result_text": "?", + "passed": 10, + "failed": 10 + }, + }, + "single_test_output": "OK", + "platform_name": "K64F" + } + } + } + + self.test_suite_data = { + "K64F-ARM": { + "test-1": { + "testcase_result": { + "build_path": ".." + }, + "single_test_output": "OK", + "single_test_result": "OK", + "platform_name": "K64F", + "elapsed_time": 1.2, + "copy_method": "shell" + }, + "test-2": { + "testcase_result": { + "build_path": ".." + }, + "single_test_output": "OK", + "single_test_result": "OK", + "platform_name": "K64F", + "elapsed_time": 1.2, + "copy_method": "shell" + } + }, + "N32F-ARM": { + "test-3": { + "testcase_result": { + "build_path": ".." + }, + "single_test_output": "OK", + "single_test_result": "OK", + "platform_name": "N32F", + "elapsed_time": 1.2, + "copy_method": "shell" + } + } + } + + def tearDown(self): + pass + + def test_export_to_file(self): + # Invalid filepath + payload = "PAYLOAD" + filepath = "." + + old_stdout = sys.stdout + sys.stdout = stdout_capture = six.StringIO() + result = mbed_report_api.export_to_file(filepath, payload) + sys.stdout = old_stdout + + command_output = stdout_capture.getvalue().splitlines()[0] + self.assertIn("file failed:", command_output) + self.assertEqual(result, False) + + # Valid filepath + temp_file = tempfile.mkstemp("test_file") + result = mbed_report_api.export_to_file(temp_file[1], payload) + self.assertTrue(result) + + with open(temp_file[1], 'r') as f: + read_data = f.read() + + self.assertEqual(read_data, payload) + os.close(temp_file[0]) + os.remove(temp_file[1]) + + + def test_exporter_text(self): + result, result_ = mbed_report_api.exporter_text(self.test_suite_data) + + lines = result.splitlines() + self.assertIn("target", lines[0]) + self.assertIn("platform_name", lines[0]) + self.assertIn("test suite", lines[0]) + self.assertIn("result", lines[0]) + self.assertIn("K64F", lines[2]) + self.assertIn("test-1", lines[2]) + self.assertIn("test-2", lines[3]) + self.assertIn("test-3", lines[4]) + self.assertIn("OK", lines[2]) + + def test_exporter_testcase_test(self): + result, result_ = mbed_report_api.exporter_testcase_text(self.test_case_data) + + lines = result.splitlines() + self.assertIn("target", lines[0]) + self.assertIn("platform_name", lines[0]) + self.assertIn("test suite", lines[0]) + self.assertIn("result", lines[0]) + + line = lines[2] + self.assertIn("K64F-ARM", line) + self.assertIn("suite", line) + self.assertIn("case-1", line) + + def test_exporter_testcase_junit(self): + result = mbed_report_api.exporter_testcase_junit(self.test_case_data) + self.assertIsNotNone(result) + + from xml.etree import ElementTree as ET + xml = ET.fromstring(result) + + self.assertEqual(xml.tag, "testsuites") + self.assertEqual(xml.attrib["failures"], "1") + self.assertEqual(xml.attrib["tests"], "3") + self.assertEqual(xml.attrib["errors"], "1") + self.assertEqual(xml.attrib["time"], "3.0") + + child = xml[0] + self.assertEqual(child.tag, "testsuite") + self.assertEqual(child.attrib["failures"], "1") + self.assertEqual(child.attrib["tests"], "3") + self.assertEqual(child.attrib["errors"], "1") + self.assertEqual(child.attrib["time"], "3.0") diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_target_info.py b/tools/python/python_tests/mbed_greentea/mbed_gt_target_info.py new file mode 100644 index 0000000000..5369e691d8 --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_target_info.py @@ -0,0 +1,434 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 +import shutil +import tempfile +import unittest + +from six import StringIO + +from mock import patch +from mbed_greentea import mbed_target_info + + +class GreenteaTargetInfo(unittest.TestCase): + + def setUp(self): + self.YOTTA_SEARCH_SSL_ISSUE = """/Library/Python/2.7/site-packages/requests/packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning. + InsecurePlatformWarning +frdm-k64f-gcc 0.0.24: Official mbed build target for the mbed frdm-k64f development board. +frdm-k64f-armcc 0.0.16: Official mbed build target for the mbed frdm-k64f development board, using the armcc toolchain. +/Library/Python/2.7/site-packages/requests/packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning. + InsecurePlatformWarning +""" + + def tearDown(self): + pass + + def test_parse_yotta_target_cmd_output_mixed_chars(self): + self.assertIn("m", mbed_target_info.parse_yotta_target_cmd_output("m 0.0.0")) + self.assertIn("m", mbed_target_info.parse_yotta_target_cmd_output(" m 0.0.0")) + self.assertIn("aaaaaaaaaaaaa", mbed_target_info.parse_yotta_target_cmd_output("aaaaaaaaaaaaa 0.0.0")) + self.assertIn("aaaa-bbbb-cccc", mbed_target_info.parse_yotta_target_cmd_output("aaaa-bbbb-cccc 0.0.0")) + self.assertIn("aBc-DEF_hijkkk", mbed_target_info.parse_yotta_target_cmd_output("aBc-DEF_hijkkk 0.0.0")) + + def test_parse_yotta_target_cmd_output_mixed_version(self): + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 0.0.0")) + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 1.1.1")) + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 1.1.12")) + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 1.1.123")) + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 11.22.33")) + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 0.0.1")) + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 0.10.12")) + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 0.20.123")) + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 0.2.123")) + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 10.20.123")) + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 110.200.123")) + + def test_parse_yotta_target_cmd_output(self): + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 0.0.24")) + self.assertIn("frdm-k64f-armcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-armcc 1.12.3")) + self.assertIn("mbed-gcc", mbed_target_info.parse_yotta_target_cmd_output("mbed-gcc 0.0.14")) + self.assertIn("stm32f429i-disco-gcc", mbed_target_info.parse_yotta_target_cmd_output("stm32f429i-disco-gcc 0.0.14")) + + def test_parse_yotta_target_cmd_output_mixed_whitechars(self): + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 0.0.24 ")) + self.assertIn("frdm-k64f-armcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-armcc 1.12.3 ")) + self.assertIn("mbed-gcc", mbed_target_info.parse_yotta_target_cmd_output("mbed-gcc 0.0.14 ")) + self.assertIn("stm32f429i-disco-gcc", mbed_target_info.parse_yotta_target_cmd_output("stm32f429i-disco-gcc 0.0.14 ")) + + def test_parse_yotta_target_cmd_output_mixed_nl(self): + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 0.0.24\n")) + self.assertIn("frdm-k64f-armcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-armcc 1.12.3\n")) + self.assertIn("mbed-gcc", mbed_target_info.parse_yotta_target_cmd_output("mbed-gcc 0.0.14\n")) + self.assertIn("stm32f429i-disco-gcc", mbed_target_info.parse_yotta_target_cmd_output("stm32f429i-disco-gcc 0.0.14\n")) + + def test_parse_yotta_target_cmd_output_mixed_rcnl(self): + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 0.0.24\r\n")) + self.assertIn("frdm-k64f-armcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-armcc 1.12.3\r\n")) + self.assertIn("mbed-gcc", mbed_target_info.parse_yotta_target_cmd_output("mbed-gcc 0.0.14\r\n")) + self.assertIn("stm32f429i-disco-gcc", mbed_target_info.parse_yotta_target_cmd_output("stm32f429i-disco-gcc 11.222.333\r\n")) + + def test_parse_yotta_target_cmd_output_mixed_nl_whitechars(self): + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 0.0.24 \n")) + self.assertIn("frdm-k64f-armcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-armcc 1.12.3 \n")) + self.assertIn("mbed-gcc", mbed_target_info.parse_yotta_target_cmd_output("mbed-gcc 0.0.14 \n")) + self.assertIn("stm32f429i-disco-gcc", mbed_target_info.parse_yotta_target_cmd_output("stm32f429i-disco-gcc 0.0.14 \n")) + + def test_parse_yotta_target_cmd_output_mixed_rcnl_whitechars(self): + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-gcc 0.0.24 \r\n")) + self.assertIn("frdm-k64f-armcc", mbed_target_info.parse_yotta_target_cmd_output("frdm-k64f-armcc 1.12.3 \r\n")) + self.assertIn("mbed-gcc", mbed_target_info.parse_yotta_target_cmd_output("mbed-gcc 0.0.14 \r\n")) + self.assertIn("stm32f429i-disco-gcc", mbed_target_info.parse_yotta_target_cmd_output("stm32f429i-disco-gcc 0.0.14 \r\n")) + + def test_parse_yotta_target_cmd_output_fail(self): + self.assertEqual(None, mbed_target_info.parse_yotta_target_cmd_output("")) + self.assertEqual(None, mbed_target_info.parse_yotta_target_cmd_output("stm32f429i-disco-gcc 1")) + self.assertEqual(None, mbed_target_info.parse_yotta_target_cmd_output("stm32f429i-disco-gcc 1.")) + self.assertEqual(None, mbed_target_info.parse_yotta_target_cmd_output("stm32f429i-disco-gcc 1.0")) + self.assertEqual(None, mbed_target_info.parse_yotta_target_cmd_output("stm32f429i-disco-gcc 1.0.")) + self.assertEqual(None, mbed_target_info.parse_yotta_target_cmd_output("stm32f429i-disco-gcc 1.0.x")) + + def test_parse_yotta_search_cmd_output(self): + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_search_cmd_output("frdm-k64f-gcc 0.0.24: Official mbed build target for the mbed frdm-k64f development board.")) + self.assertIn("frdm-k64f-armcc", mbed_target_info.parse_yotta_search_cmd_output("frdm-k64f-armcc 0.0.16: Official mbed build target for the mbed frdm-k64f development board, using the armcc toolchain.")) + self.assertEqual(None, mbed_target_info.parse_yotta_search_cmd_output("")) + self.assertEqual(None, mbed_target_info.parse_yotta_search_cmd_output("additional results from https://yotta-private.herokuapp.com:")) + + def test_parse_yotta_search_cmd_output_text(self): + # Old style with new switch --short : 'yotta search ... --short' + text = """frdm-k64f-gcc 0.1.4: Official mbed build target for the mbed frdm-k64f development board. +frdm-k64f-armcc 0.1.3: Official mbed build target for the mbed frdm-k64f development board, using the armcc toolchain. + +additional results from https://yotta-private.herokuapp.com: +""" + targets = [] + for line in text.splitlines(): + yotta_target_name = mbed_target_info.parse_yotta_search_cmd_output(line) + if yotta_target_name: + targets.append(yotta_target_name) + self.assertIn("frdm-k64f-gcc", targets) + self.assertIn("frdm-k64f-armcc", targets) + self.assertEqual(2, len(targets)) + + def test_parse_yotta_search_cmd_output_new_style(self): + self.assertIn("frdm-k64f-gcc", mbed_target_info.parse_yotta_search_cmd_output("frdm-k64f-gcc 0.1.4")) + self.assertIn("frdm-k64f-armcc", mbed_target_info.parse_yotta_search_cmd_output("frdm-k64f-armcc 0.1.4")) + pass + + def test_parse_yotta_search_cmd_output_new_style_text(self): + # New style of 'yotta search ...' + text = """frdm-k64f-gcc 0.1.4 + Official mbed build target for the mbed frdm-k64f development board. + mbed-target:k64f, mbed-official, k64f, frdm-k64f, gcc +frdm-k64f-armcc 0.1.4 + Official mbed build target for the mbed frdm-k64f development board, using the armcc toolchain. + mbed-target:k64f, mbed-official, k64f, frdm-k64f, armcc + +additional results from https://yotta-private.herokuapp.com:""" + targets = [] + for line in text.splitlines(): + yotta_target_name = mbed_target_info.parse_yotta_search_cmd_output(line) + if yotta_target_name: + targets.append(yotta_target_name) + self.assertIn("frdm-k64f-gcc", targets) + self.assertIn("frdm-k64f-armcc", targets) + self.assertEqual(2, len(targets)) + + def test_parse_yotta_search_cmd_output_new_style_text_2(self): + # New style of 'yotta search ...' + text = """nrf51dk-gcc 0.0.3: + Official mbed build target for the nRF51-DK 32KB platform. + mbed-official, mbed-target:nrf51_dk, gcc + +additional results from https://yotta-private.herokuapp.com: + nrf51dk-gcc 0.0.3: + Official mbed build target for the nRF51-DK 32KB platform. + mbed-official, mbed-target:nrf51_dk, gcc + nrf51dk-armcc 0.0.3: + Official mbed build target for the nRF51-DK 32KB platform. + mbed-official, mbed-target:nrf51_dk, armcc +""" + targets = [] + for line in text.splitlines(): + yotta_target_name = mbed_target_info.parse_yotta_search_cmd_output(line) + if yotta_target_name and yotta_target_name not in targets: + targets.append(yotta_target_name) + + self.assertIn("nrf51dk-gcc", targets) + self.assertIn("nrf51dk-armcc", targets) + self.assertEqual(2, len(targets)) + + def test_parse_yotta_search_cmd_output_with_ssl_errors(self): + result = [] + for line in self.YOTTA_SEARCH_SSL_ISSUE.splitlines(): + yotta_target_name = mbed_target_info.parse_yotta_search_cmd_output(line) + if yotta_target_name: + result.append(yotta_target_name) + self.assertIn("frdm-k64f-gcc", result) + self.assertIn("frdm-k64f-armcc", result) + self.assertEqual(2, len(result)) + + def test_parse_mbed_target_from_target_json_no_keywords(self): + target_json_data = { + "name": "frdm-k64f-armcc", + "version": "0.1.4", + } + + self.assertIsNone(mbed_target_info.parse_mbed_target_from_target_json('k64f', target_json_data)) + self.assertIsNone(mbed_target_info.parse_mbed_target_from_target_json('K64F', target_json_data)) + + def test_parse_mbed_target_from_target_json_no_name(self): + target_json_data = { + "version": "0.1.4", + "keywords": [ + "mbed-target:k64f", + "mbed-target::garbage", + "mbed-official", + "k64f", + "frdm-k64f", + "armcc" + ], + } + + self.assertIsNone(mbed_target_info.parse_mbed_target_from_target_json('k64f', target_json_data)) + self.assertIsNone(mbed_target_info.parse_mbed_target_from_target_json('K64F', target_json_data)) + + def test_parse_mbed_target_from_target_json(self): + target_json_data = { + "name": "frdm-k64f-armcc", + "version": "0.1.4", + "keywords": [ + "mbed-target:k64f", + "mbed-target::garbage", + "mbed-official", + "k64f", + "frdm-k64f", + "armcc" + ], + } + + # Positive tests + self.assertEqual('frdm-k64f-armcc', mbed_target_info.parse_mbed_target_from_target_json('k64f', target_json_data)) + self.assertEqual('frdm-k64f-armcc', mbed_target_info.parse_mbed_target_from_target_json('K64F', target_json_data)) + + # Except cases + self.assertNotEqual('frdm-k64f-gcc', mbed_target_info.parse_mbed_target_from_target_json('k64f', target_json_data)) + self.assertNotEqual('frdm-k64f-gcc', mbed_target_info.parse_mbed_target_from_target_json('K64F', target_json_data)) + self.assertEqual(None, mbed_target_info.parse_mbed_target_from_target_json('_k64f_', target_json_data)) + self.assertEqual(None, mbed_target_info.parse_mbed_target_from_target_json('', target_json_data)) + self.assertEqual(None, mbed_target_info.parse_mbed_target_from_target_json('Some board name', target_json_data)) + + def test_parse_mbed_target_from_target_json_multiple(self): + target_json_data = { + "name": "frdm-k64f-armcc", + "version": "0.1.4", + "keywords": [ + "mbed-target:k64f", + "mbed-target:frdm-k64f", + "mbed-official", + "k64f", + "frdm-k64f", + "armcc" + ], + } + + # Positive tests + self.assertEqual('frdm-k64f-armcc', mbed_target_info.parse_mbed_target_from_target_json('k64f', target_json_data)) + self.assertEqual('frdm-k64f-armcc', mbed_target_info.parse_mbed_target_from_target_json('frdm-k64f', target_json_data)) + self.assertEqual('frdm-k64f-armcc', mbed_target_info.parse_mbed_target_from_target_json('K64F', target_json_data)) + self.assertEqual('frdm-k64f-armcc', mbed_target_info.parse_mbed_target_from_target_json('FRDM-K64F', target_json_data)) + + # Except cases + self.assertNotEqual('frdm-k64f-gcc', mbed_target_info.parse_mbed_target_from_target_json('k64f', target_json_data)) + self.assertNotEqual('frdm-k64f-gcc', mbed_target_info.parse_mbed_target_from_target_json('frdm-k64f', target_json_data)) + self.assertNotEqual('frdm-k64f-gcc', mbed_target_info.parse_mbed_target_from_target_json('K64F', target_json_data)) + self.assertNotEqual('frdm-k64f-gcc', mbed_target_info.parse_mbed_target_from_target_json('FRDM-K64F', target_json_data)) + + @patch('mbed_os_tools.test.mbed_target_info.get_mbed_target_call_yotta_target') + def test_get_mbed_target_from_current_dir_ok(self, callYtTarget_mock): + + yotta_target_cmd = """frdm-k64f-gcc 2.0.0 +kinetis-k64-gcc 2.0.2 +mbed-gcc 1.1.0 +""" + + callYtTarget_mock.return_value = ('', '', 0) + r = mbed_target_info.get_mbed_target_from_current_dir() + self.assertEqual(None, r) + + callYtTarget_mock.return_value = ('', '', 1) + r = mbed_target_info.get_mbed_target_from_current_dir() + self.assertEqual(None, r) + + callYtTarget_mock.return_value = (yotta_target_cmd, '', 1) + r = mbed_target_info.get_mbed_target_from_current_dir() + self.assertEqual(None, r) + + callYtTarget_mock.return_value = (yotta_target_cmd, '', 0) + r = mbed_target_info.get_mbed_target_from_current_dir() + self.assertEqual('frdm-k64f-gcc', r) + + def test_get_yotta_target_from_local_config_invalid_path(self): + result = mbed_target_info.get_yotta_target_from_local_config("invalid_path.json") + + self.assertIsNone(result) + + def test_get_yotta_target_from_local_config_valid_path(self): + payload = '{"build": { "target": "test_target"}}' + handle, path = tempfile.mkstemp("test_file") + + with open(path, 'w+') as f: + f.write(payload) + + result = mbed_target_info.get_yotta_target_from_local_config(path) + + self.assertIsNotNone(result) + self.assertEqual(result, "test_target") + + def test_get_yotta_target_from_local_config_failed_open(self): + handle, path = tempfile.mkstemp() + + with open(path, 'w+') as f: + result = mbed_target_info.get_yotta_target_from_local_config(path) + + self.assertIsNone(result) + + def test_get_mbed_targets_from_yotta_local_module_invalid_path(self): + result = mbed_target_info.get_mbed_targets_from_yotta_local_module("null", "invalid_path") + self.assertEqual(result, []) + + def test_get_mbed_targets_from_yotta_local_module_invalid_target(self): + base_path = tempfile.mkdtemp() + targ_path = tempfile.mkdtemp("target-1", dir=base_path) + handle, targ_file = tempfile.mkstemp("target.json", dir=targ_path) + + result = mbed_target_info.get_mbed_targets_from_yotta_local_module("null", base_path) + + self.assertEqual(result, []) + + def test_get_mbed_targets_from_yotta_local_module_valid(self): + payload = '{"name": "test_name", "keywords": [ "mbed-target:k64f" ]}' + base_path = tempfile.mkdtemp() + tar1_path = tempfile.mkdtemp("target-1", dir=base_path) + tar1_file = os.path.join(tar1_path, "target.json") + + with open(tar1_file, 'w+') as f: + f.write(payload) + + result = mbed_target_info.get_mbed_targets_from_yotta_local_module("k64f", base_path) + self.assertIsNot(result, None) + self.assertEqual(result[0], "test_name") + + shutil.rmtree(base_path) + + def test_parse_mbed_target_from_target_json_missing_json_data(self): + result = mbed_target_info.parse_mbed_target_from_target_json("null", "null") + self.assertIsNone(result) + + def test_parse_mbed_target_from_target_json_missing_keywords(self): + data = {} + result = mbed_target_info.parse_mbed_target_from_target_json("null", data) + self.assertIsNone(result) + + def test_parse_mbed_target_from_target_json_missing_target(self): + data = {} + data["keywords"] = {} + result = mbed_target_info.parse_mbed_target_from_target_json("null", data) + self.assertIsNone(result) + + def test_parse_mbed_target_from_target_json_missing_name(self): + data = {} + data["keywords"] = ["mbed-target:null"] + result = mbed_target_info.parse_mbed_target_from_target_json("null", data) + self.assertIsNone(result) + + # def test_get_mbed_targets_from_yotta(self): + # result = mbed_target_info.get_mbed_targets_from_yotta("k64f") + + def test_parse_add_target_info_mapping(self): + result = mbed_target_info.add_target_info_mapping("null") + + def test_parse_yotta_json_for_build_name(self): + self.assertEqual("", mbed_target_info.parse_yotta_json_for_build_name( + { + "build": { + "target": "" + } + } + )) + + self.assertEqual(None, mbed_target_info.parse_yotta_json_for_build_name( + { + "build": {} + } + )) + + self.assertEqual('frdm-k64f-gcc', mbed_target_info.parse_yotta_json_for_build_name( + { + "build": { + "target": "frdm-k64f-gcc,*", + "targetSetExplicitly": True + } + } + )) + + self.assertEqual('x86-linux-native', mbed_target_info.parse_yotta_json_for_build_name( + { + "build": { + "target": "x86-linux-native,*", + "targetSetExplicitly": True + } + } + )) + + self.assertEqual('frdm-k64f-gcc', mbed_target_info.parse_yotta_json_for_build_name( + { + "build": { + "target": "frdm-k64f-gcc,*" + } + } + )) + + self.assertEqual('frdm-k64f-gcc', mbed_target_info.parse_yotta_json_for_build_name( + { + "build": { + "target": "frdm-k64f-gcc" + } + } + )) + + self.assertEqual(None, mbed_target_info.parse_yotta_json_for_build_name( + { + "build": { + } + } + )) + + self.assertEqual(None, mbed_target_info.parse_yotta_json_for_build_name( + { + "BUILD": { + "target": "frdm-k64f-gcc,*", + "targetSetExplicitly": True + } + } + )) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_test_api.py b/tools/python/python_tests/mbed_greentea/mbed_gt_test_api.py new file mode 100644 index 0000000000..df0d5332f6 --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_test_api.py @@ -0,0 +1,595 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 mbed_greentea import mbed_test_api + + + +class GreenteaTestAPI(unittest.TestCase): + + def setUp(self): + self.OUTPUT_FAILURE = """mbedgt: mbed-host-test-runner: started +[1459245784.59][CONN][RXD] >>> Test cases: 7 passed, 1 failed with reason 'Test Cases Failed' +[1459245784.61][CONN][RXD] >>> TESTS FAILED! +[1459245784.64][CONN][INF] found KV pair in stream: {{__testcase_summary;7;1}}, queued... +[1459245784.64][CONN][RXD] {{__testcase_summary;7;1}} +[1459245784.66][CONN][INF] found KV pair in stream: {{end;failure}}, queued... +[1459245784.66][HTST][INF] __notify_complete(False) +[1459245784.66][HTST][INF] test suite run finished after 2.37 sec... +[1459245784.66][CONN][RXD] {{end;failure}} +[1459245784.67][HTST][INF] CONN exited with code: 0 +[1459245784.67][HTST][INF] Some events in queue +[1459245784.67][HTST][INF] stopped consuming events +[1459245784.67][HTST][INF] host test result() call skipped, received: False +[1459245784.67][HTST][WRN] missing __exit event from DUT +[1459245784.67][HTST][INF] calling blocking teardown() +[1459245784.67][HTST][INF] teardown() finished +[1459245784.67][HTST][INF] {{result;failure}} +""" + + self.OUTPUT_SUCCESS = """mbedgt: mbed-host-test-runner: started +[1459245860.90][CONN][RXD] {{__testcase_summary;4;0}} +[1459245860.92][CONN][INF] found KV pair in stream: {{end;success}}, queued... +[1459245860.92][CONN][RXD] {{end;success}} +[1459245860.92][HTST][INF] __notify_complete(True) +[1459245860.92][HTST][INF] test suite run finished after 0.90 sec... +[1459245860.94][HTST][INF] CONN exited with code: 0 +[1459245860.94][HTST][INF] No events in queue +[1459245860.94][HTST][INF] stopped consuming events +[1459245860.94][HTST][INF] host test result() call skipped, received: True +[1459245860.94][HTST][WRN] missing __exit event from DUT +[1459245860.94][HTST][INF] calling blocking teardown() +[1459245860.94][HTST][INF] teardown() finished +[1459245860.94][HTST][INF] {{result;success}} +""" + + self.OUTPUT_TIMEOUT = """mbedgt: mbed-host-test-runner: started +[1459246047.80][HTST][INF] copy image onto target... + 1 file(s) copied. +[1459246055.05][HTST][INF] starting host test process... +[1459246055.47][CONN][INF] starting connection process... +[1459246055.47][CONN][INF] initializing serial port listener... +[1459246055.47][SERI][INF] serial(port=COM205, baudrate=9600) +[1459246055.47][SERI][INF] reset device using 'default' plugin... +[1459246055.73][SERI][INF] wait for it... +[1459246056.74][CONN][INF] sending preamble '56bdcd85-b88a-460b-915e-1b9b41713b5a'... +[1459246056.74][SERI][TXD] mbedmbedmbedmbedmbedmbedmbedmbedmbedmbed +[1459246056.74][SERI][TXD] {{__sync;56bdcd85-b88a-460b-915e-1b9b41713b5a}} +[1459246065.06][HTST][INF] test suite run finished after 10.00 sec... +[1459246065.07][HTST][INF] CONN exited with code: 0 +[1459246065.07][HTST][INF] No events in queue +[1459246065.07][HTST][INF] stopped consuming events +[1459246065.07][HTST][INF] host test result(): None +[1459246065.07][HTST][WRN] missing __exit event from DUT +[1459246065.07][HTST][ERR] missing __exit event from DUT and no result from host test, timeout... +[1459246065.07][HTST][INF] calling blocking teardown() +[1459246065.07][HTST][INF] teardown() finished +[1459246065.07][HTST][INF] {{result;timeout}} +""" + + self.OUTPUT_STARTTAG_MISSING = """ +mbedgt: mbed-host-test-runner: started +[1507470727.39][HTST][INF] host test executor ver. 1.2.0 +[1507470727.39][HTST][INF] copy image onto target... SKIPPED! +[1507470727.39][HTST][INF] starting host test process... +[1507470727.40][CONN][INF] starting connection process... +[1507470727.40][HTST][INF] setting timeout to: 120 sec +[1507470756.96][CONN][RXD] >>> Running 4 test cases... +[1507470756.96][CONN][RXD] +[1507470756.96][CONN][RXD] >>> Running case #1: 'DNS query'... +[1507470756.96][CONN][INF] found KV pair in stream: {{__testcase_name;DNS query}}, queued... +[1507470756.96][CONN][INF] found KV pair in stream: {{__testcase_name;DNS preference query}}, queued... +[1507470756.96][CONN][INF] found KV pair in stream: {{__testcase_name;DNS literal}}, queued... +[1507470756.96][CONN][INF] found KV pair in stream: {{__testcase_name;DNS preference literal}}, queued... +[1507470757.04][CONN][RXD] DNS: query "connector.mbed.com" => "169.45.82.19" +[1507470757.04][CONN][INF] found KV pair in stream: {{__testcse_start;DNS query}}, queued... +[1507470757.13][CONN][RXD] >>> 'DNS query': 1 passed, 0 failed +[1507470757.13][CONN][RXD] +[1507470757.13][CONN][INF] found KV pair in stream: {{__testcase_finish;DNS query;1;0}}, queued... +[1507470757.32][CONN][RXD] >>> Running case #2: 'DNS preference query'... +[1507470757.41][CONN][RXD] DNS: query ipv4 "connector.mbed.com" => "169.45.82.19" +[1507470757.41][CONN][RXD] >>> 'DNS preference query': 1 passed, 0 failed +[1507470757.41][CONN][RXD] +[1507470757.41][CONN][INF] found KV pair in stream: {{__testcase_start;DNS preference query}}, queued... +[1507470757.41][CONN][INF] found KV pair in stream: {{__testcase_finish;DNS preference query;1;0}}, queued... +[1507470757.57][CONN][RXD] >>> Running case #3: 'DNS literal'... +[1507470757.66][CONN][RXD] DNS: literal "10.118.13.253" => "10.118.13.253" +[1507470757.66][CONN][RXD] {{__testcase_finish;DNS l +[1507470757.66][CONN][RXD] sed, 0 fai +[1507470757.66][CONN][INF] found KV pair in stream: {{__testcase_start;DNS literal}}, queued... +[1507470757.80][CONN][RXD] case #4: 'DNS preference literal'... +[1507470757.80][CONN][RXD] {{__test +[1507470757.80][CONN][RXD] reference literal}} +[1507470757.83][CONN][RXD] DNS: literal ipv4 "10.118.13.253" => "10.118.13.253" +[1507470757.83][CONN][INF] found KV pair in stream: {{__testcase_finish;DNS preference literal;1;0}}, queued... +[1507470757.92][CONN][RXD] >>> 'DNS preference literal': 1 passed, 0 failed +[1507470757.92][CONN][RXD] +[1507470757.92][CONN][RXD] >>> Test cases: 4 passed, 0 failed +[1507470758.00][CONN][INF] found KV pair in stream: {{__testcase_summary;4;0}}, queued... +[1507470758.00][CONN][INF] found KV pair in stream: {{max_heap_usage;2800}}, queued... +[1507470758.00][CONN][INF] found KV pair in stream: {{reserved_heap;19336}}, queued... +[1507470758.00][HTST][ERR] orphan event in main phase: {{max_heap_usage;2800}}, timestamp=1507470757.999258 +[1507470758.00][HTST][ERR] orphan event in main phase: {{reserved_heap;19336}}, timestamp=1507470757.999260 +[1507470758.00][CONN][INF] found KV pair in stream: {{__thread_info;"0x010002ca8",600,4096}}, queued... +[1507470758.00][HTST][ERR] orphan event in main phase: {{__thread_info;"0x010002ca8",600,4096}}, timestamp=1507470757.999261 +[1507470758.09][CONN][INF] found KV pair in stream: {{__thread_info;"0x01000045c",72,512}}, queued... +[1507470758.09][CONN][INF] found KV pair in stream: {{__thread_info;"0x010001168",128,512}}, queued... +[1507470758.09][HTST][ERR] orphan event in main phase: {{__thread_info;"0x01000045c",72,512}}, timestamp=1507470758.085627 +[1507470758.09][HTST][ERR] orphan event in main phase: {{__thread_info;"0x010001168",128,512}}, timestamp=1507470758.085635 +[1507470758.16][CONN][INF] found KV pair in stream: {{__thread_info;"0x010001018",560,1200}}, queued... +[1507470758.16][HTST][ERR] orphan event in main phase: {{__thread_info;"0x010001018",560,1200}}, timestamp=1507470758.162780 +[1507470758.25][CONN][INF] found KV pair in stream: {{__thread_info;"0x0100004a4",112,768}}, queued... +[1507470758.25][CONN][INF] found KV pair in stream: {{__thread_info;"0x0100010f8",96,512}}, queued... +[1507470758.25][CONN][INF] found KV pair in stream: {{__thread_info;"0x010001088",152,512}}, queued... +[1507470758.25][CONN][INF] found KV pair in stream: {{end;success}}, queued... +[1507470758.25][HTST][ERR] orphan event in main phase: {{__thread_info;"0x0100004a4",112,768}}, timestamp=1507470758.248291 +[1507470758.25][HTST][ERR] orphan event in main phase: {{__thread_info;"0x0100010f8",96,512}}, timestamp=1507470758.248296 +[1507470758.25][HTST][ERR] orphan event in main phase: {{__thread_info;"0x010001088",152,512}}, timestamp=1507470758.248299 +[1507470758.25][HTST][INF] __notify_complete(True) +[1507470758.25][HTST][INF] __exit_event_queue received +[1507470760.47][HTST][INF] CONN exited with code: 0 +[1507470760.47][HTST][INF] No events in queue +[1507470760.47][HTST][INF] stopped consuming events +[1507470760.47][HTST][INF] host test result() call skipped, received: True +[1507470760.47][HTST][WRN] missing __exit event from DUT +[1507470760.47][HTST][INF] calling blocking teardown() +[1507470760.47][HTST][INF] teardown() finished +[1507470760.47][HTST][INF] {{result;success}} + """ + + self.OUTPUT_UNDEF = """mbedgt: mbed-host-test-runner: started +{{result;some_random_value}} +""" + + self.OUTOUT_CSTRING_TEST = """ +[1459246264.88][HTST][INF] copy image onto target... + 1 file(s) copied. +[1459246272.76][HTST][INF] starting host test process... +[1459246273.18][CONN][INF] starting connection process... +[1459246273.18][CONN][INF] initializing serial port listener... +[1459246273.18][SERI][INF] serial(port=COM205, baudrate=9600) +[1459246273.18][SERI][INF] reset device using 'default' plugin... +[1459246273.43][SERI][INF] wait for it... +[1459246274.43][CONN][INF] sending preamble '5daa5ff9-a9c1-4b47-88a2-9295f1de7c64'... +[1459246274.43][SERI][TXD] mbedmbedmbedmbedmbedmbedmbedmbedmbedmbed +[1459246274.43][SERI][TXD] {{__sync;5daa5ff9-a9c1-4b47-88a2-9295f1de7c64}} +[1459246274.58][CONN][INF] found SYNC in stream: {{__sync;5daa5ff9-a9c1-4b47-88a2-9295f1de7c64}}, queued... +[1459246274.58][CONN][RXD] {{__sync;5daa5ff9-a9c1-4b47-88a2-9295f1de7c64}} +[1459246274.58][HTST][INF] sync KV found, uuid=5daa5ff9-a9c1-4b47-88a2-9295f1de7c64, timestamp=1459246274.575000 +[1459246274.60][CONN][INF] found KV pair in stream: {{__version;1.1.0}}, queued... +[1459246274.60][CONN][RXD] {{__version;1.1.0}} +[1459246274.60][HTST][INF] DUT greentea-client version: 1.1.0 +[1459246274.61][CONN][INF] found KV pair in stream: {{__timeout;5}}, queued... +[1459246274.61][HTST][INF] setting timeout to: 5 sec +[1459246274.62][CONN][RXD] {{__timeout;5}} +[1459246274.64][CONN][INF] found KV pair in stream: {{__host_test_name;default_auto}}, queued... +[1459246274.64][HTST][INF] host test setup() call... +[1459246274.64][HTST][INF] CALLBACKs updated +[1459246274.64][HTST][INF] host test detected: default_auto +[1459246274.64][CONN][RXD] {{__host_test_name;default_auto}} +[1459246274.66][CONN][INF] found KV pair in stream: {{__testcase_count;8}}, queued... +[1459246274.66][CONN][RXD] {{__testcase_count;8}} +[1459246274.69][CONN][RXD] >>> Running 8 test cases... +[1459246274.74][CONN][RXD] >>> Running case #1: 'C strings: strtok'... +[1459246274.79][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: strtok}}, queued... +[1459246274.79][CONN][RXD] {{__testcase_start;C strings: strtok}} +[1459246274.84][CONN][INF] found KV pair in stream: {{__testcase_finish;C strings: strtok;1;0}}, queued... +[1459246274.84][CONN][RXD] {{__testcase_finish;C strings: strtok;1;0}} +[1459246274.88][CONN][RXD] >>> 'C strings: strtok': 1 passed, 0 failed +[1459246274.93][CONN][RXD] >>> Running case #2: 'C strings: strpbrk'... +[1459246274.97][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: strpbrk}}, queued... +[1459246274.97][CONN][RXD] {{__testcase_start;C strings: strpbrk}} +[1459246275.01][CONN][INF] found KV pair in stream: {{__testcase_finish;C strings: strpbrk;1;0}}, queued... +[1459246275.01][CONN][RXD] {{__testcase_finish;C strings: strpbrk;1;0}} +[1459246275.06][CONN][RXD] >>> 'C strings: strpbrk': 1 passed, 0 failed +[1459246275.13][CONN][RXD] >>> Running case #3: 'C strings: %i %d integer formatting'... +[1459246275.18][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: %i %d integer formatting}}, queued... +[1459246275.18][CONN][RXD] {{__testcase_start;C strings: %i %d integer formatting}} +[1459246275.24][CONN][INF] found KV pair in stream: {{__testcase_finish;C strings: %i %d integer formatting;1;0}}, queued... +[1459246275.24][CONN][RXD] {{__testcase_finish;C strings: %i %d integer formatting;1;0}} +[1459246275.32][CONN][RXD] >>> 'C strings: %i %d integer formatting': 1 passed, 0 failed +[1459246275.38][CONN][RXD] >>> Running case #4: 'C strings: %u %d integer formatting'... +[1459246275.44][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: %u %d integer formatting}}, queued... +[1459246275.44][CONN][RXD] {{__testcase_start;C strings: %u %d integer formatting}} +[1459246275.50][CONN][INF] found KV pair in stream: {{__testcase_finish;C strings: %u %d integer formatting;1;0}}, queued... +[1459246275.50][CONN][RXD] {{__testcase_finish;C strings: %u %d integer formatting;1;0}} +[1459246275.57][CONN][RXD] >>> 'C strings: %u %d integer formatting': 1 passed, 0 failed +[1459246275.64][CONN][RXD] >>> Running case #5: 'C strings: %x %E integer formatting'... +[1459246275.68][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: %x %E integer formatting}}, queued... +[1459246275.68][CONN][RXD] {{__testcase_start;C strings: %x %E integer formatting}} +[1459246275.74][CONN][INF] found KV pair in stream: {{__testcase_finish;C strings: %x %E integer formatting;1;0}}, queued... +[1459246275.74][CONN][RXD] {{__testcase_finish;C strings: %x %E integer formatting;1;0}} +[1459246275.82][CONN][RXD] >>> 'C strings: %x %E integer formatting': 1 passed, 0 failed +[1459246275.88][CONN][RXD] >>> Running case #6: 'C strings: %f %f float formatting'... +[1459246275.94][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: %f %f float formatting}}, queued... +[1459246275.94][CONN][RXD] {{__testcase_start;C strings: %f %f float formatting}} +[1459246276.10][CONN][RXD] :57::FAIL: Expected '0.002000 0.924300 15.913200 791.773680 6208.200000 25719.495200 426815.982588 6429271.046000 42468024.930000 212006462.910000' Was ' +' +[1459246276.18][CONN][RXD] >>> failure with reason 'Assertion Failed' during 'Case Handler' +[1459246276.25][CONN][INF] found KV pair in stream: {{__testcase_finish;C strings: %f %f float formatting;0;1}}, queued... +[1459246276.25][CONN][RXD] {{__testcase_finish;C strings: %f %f float formatting;0;1}} +[1459246276.34][CONN][RXD] >>> 'C strings: %f %f float formatting': 0 passed, 1 failed with reason 'Test Cases Failed' +[1459246276.41][CONN][RXD] >>> Running case #7: 'C strings: %e %E float formatting'... +[1459246276.46][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: %e %E float formatting}}, queued... +[1459246276.46][CONN][RXD] {{__testcase_start;C strings: %e %E float formatting}} +[1459246276.52][CONN][INF] found KV pair in stream: {{__testcase_finish;C strings: %e %E float formatting;1;0}}, queued... +[1459246276.53][CONN][RXD] {{__testcase_finish;C strings: %e %E float formatting;1;0}} +[1459246276.59][CONN][RXD] >>> 'C strings: %e %E float formatting': 1 passed, 0 failed +[1459246276.65][CONN][RXD] >>> Running case #8: 'C strings: %g %g float formatting'... +[1459246276.71][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: %g %g float formatting}}, queued... +[1459246276.71][CONN][RXD] {{__testcase_start;C strings: %g %g float formatting}} +[1459246276.77][CONN][INF] found KV pair in stream: {{__testcase_finish;C strings: %g %g float formatting;1;0}}, queued... +[1459246276.77][CONN][RXD] {{__testcase_finish;C strings: %g %g float formatting;1;0}} +[1459246276.83][CONN][RXD] >>> 'C strings: %g %g float formatting': 1 passed, 0 failed +[1459246276.90][CONN][RXD] >>> Test cases: 7 passed, 1 failed with reason 'Test Cases Failed' +[1459246276.92][CONN][RXD] >>> TESTS FAILED! +[1459246276.95][CONN][INF] found KV pair in stream: {{__testcase_summary;7;1}}, queued... +[1459246276.95][CONN][RXD] {{__testcase_summary;7;1}} +[1459246276.97][CONN][INF] found KV pair in stream: {{end;failure}}, queued... +[1459246276.97][CONN][RXD] {{end;failure}} +[1459246276.97][HTST][INF] __notify_complete(False) +[1459246276.97][HTST][INF] test suite run finished after 2.37 sec... +[1459246276.98][HTST][INF] CONN exited with code: 0 +[1459246276.98][HTST][INF] Some events in queue +[1459246276.98][HTST][INF] stopped consuming events +[1459246276.98][HTST][INF] host test result() call skipped, received: False +[1459246276.98][HTST][WRN] missing __exit event from DUT +[1459246276.98][HTST][INF] calling blocking teardown() +[1459246276.98][HTST][INF] teardown() finished +[1459246276.98][HTST][INF] {{result;failure}} +""" + + self.OUTOUT_CSTRING_TEST_CASE_COUNT_AND_NAME = """ +[1467197417.13][SERI][TXD] {{__sync;3018cb93-f11c-417e-bf61-240c338dfec9}} +[1467197417.27][CONN][RXD] {{__sync;3018cb93-f11c-417e-bf61-240c338dfec9}} +[1467197417.27][CONN][INF] found SYNC in stream: {{__sync;3018cb93-f11c-417e-bf61-240c338dfec9}} it is #0 sent, queued... +[1467197417.27][HTST][INF] sync KV found, uuid=3018cb93-f11c-417e-bf61-240c338dfec9, timestamp=1467197417.272000 +[1467197417.29][CONN][RXD] {{__version;1.1.0}} +[1467197417.29][CONN][INF] found KV pair in stream: {{__version;1.1.0}}, queued... +[1467197417.29][HTST][INF] DUT greentea-client version: 1.1.0 +[1467197417.31][CONN][RXD] {{__timeout;5}} +[1467197417.31][CONN][INF] found KV pair in stream: {{__timeout;5}}, queued... +[1467197417.31][HTST][INF] setting timeout to: 5 sec +[1467197417.34][CONN][RXD] {{__host_test_name;default_auto}} +[1467197417.34][CONN][INF] found KV pair in stream: {{__host_test_name;default_auto}}, queued... +[1467197417.34][HTST][INF] host test class: '' +[1467197417.34][HTST][INF] host test setup() call... +[1467197417.34][HTST][INF] CALLBACKs updated +[1467197417.34][HTST][INF] host test detected: default_auto +[1467197417.36][CONN][RXD] {{__testcase_count;2}} +[1467197417.36][CONN][INF] found KV pair in stream: {{__testcase_count;2}}, queued... +[1467197417.39][CONN][RXD] >>> Running 2 test cases... +[1467197417.43][CONN][RXD] {{__testcase_name;C strings: strtok}} +[1467197417.43][CONN][INF] found KV pair in stream: {{__testcase_name;C strings: strtok}}, queued... +[1467197417.47][CONN][RXD] {{__testcase_name;C strings: strpbrk}} +[1467197417.47][CONN][INF] found KV pair in stream: {{__testcase_name;C strings: strpbrk}}, queued... +[1467197417.52][CONN][RXD] >>> Running case #1: 'C strings: strtok'... +[1467197417.56][CONN][RXD] {{__testcase_start;C strings: strtok}} +[1467197417.56][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: strtok}}, queued... +[1467197422.31][HTST][INF] test suite run finished after 5.00 sec... +[1467197422.31][CONN][INF] received special even '__host_test_finished' value='True', finishing +[1467197422.33][HTST][INF] CONN exited with code: 0 +[1467197422.33][HTST][INF] No events in queue +[1467197422.33][HTST][INF] stopped consuming events +[1467197422.33][HTST][INF] host test result(): None +[1467197422.33][HTST][WRN] missing __exit event from DUT +[1467197422.33][HTST][ERR] missing __exit event from DUT and no result from host test, timeout... +[1467197422.33][HTST][INF] calling blocking teardown() +[1467197422.33][HTST][INF] teardown() finished +[1467197422.33][HTST][INF] {{result;timeout}} +""" + + self.OUTOUT_GENERIC_TESTS_TESCASE_NAME_AND_COUNT = """ +[1467205002.74][HTST][INF] host test executor ver. 0.2.19 +[1467205002.74][HTST][INF] copy image onto target... + 1 file(s) copied. +Plugin info: HostTestPluginCopyMethod_Shell::CopyMethod: Waiting up to 60 sec for '0240000033514e450019500585d40008e981000097969900' mount point (current is 'F:')... +[1467205011.16][HTST][INF] starting host test process... +[1467205011.74][CONN][INF] starting serial connection process... +[1467205011.74][CONN][INF] notify event queue about extra 60 sec timeout for serial port pooling +[1467205011.74][CONN][INF] initializing serial port listener... +[1467205011.74][HTST][INF] setting timeout to: 60 sec +[1467205011.74][SERI][INF] serial(port=COM219, baudrate=9600, timeout=0) +Plugin info: HostTestPluginBase::BasePlugin: Waiting up to 60 sec for '0240000033514e450019500585d40008e981000097969900' serial port (current is 'COM219')... +[1467205011.83][SERI][INF] reset device using 'default' plugin... +[1467205012.08][SERI][INF] waiting 1.00 sec after reset +[1467205013.08][SERI][INF] wait for it... +[1467205013.08][SERI][TXD] mbedmbedmbedmbedmbedmbedmbedmbedmbedmbed +[1467205013.08][CONN][INF] sending up to 2 __sync packets (specified with --sync=2) +[1467205013.08][CONN][INF] sending preamble 'f82e0251-bb3e-4434-bc93-b780b5d0e82a' +[1467205013.08][SERI][TXD] {{__sync;f82e0251-bb3e-4434-bc93-b780b5d0e82a}} +[1467205013.22][CONN][RXD] {{__sync;f82e0251-bb3e-4434-bc93-b780b5d0e82a}} +[1467205013.22][CONN][INF] found SYNC in stream: {{__sync;f82e0251-bb3e-4434-bc93-b780b5d0e82a}} it is #0 sent, queued... +[1467205013.22][HTST][INF] sync KV found, uuid=f82e0251-bb3e-4434-bc93-b780b5d0e82a, timestamp=1467205013.219000 +[1467205013.24][CONN][RXD] {{__version;1.1.0}} +[1467205013.24][CONN][INF] found KV pair in stream: {{__version;1.1.0}}, queued... +[1467205013.24][HTST][INF] DUT greentea-client version: 1.1.0 +[1467205013.25][CONN][RXD] {{__timeout;20}} +[1467205013.26][CONN][INF] found KV pair in stream: {{__timeout;20}}, queued... +[1467205013.26][HTST][INF] setting timeout to: 20 sec +[1467205013.29][CONN][RXD] {{__host_test_name;default_auto}} +[1467205013.29][CONN][INF] found KV pair in stream: {{__host_test_name;default_auto}}, queued... +[1467205013.29][HTST][INF] host test class: '' +[1467205013.29][HTST][INF] host test setup() call... +[1467205013.29][HTST][INF] CALLBACKs updated +[1467205013.29][HTST][INF] host test detected: default_auto +[1467205013.31][CONN][RXD] {{__testcase_count;4}} +[1467205013.31][CONN][INF] found KV pair in stream: {{__testcase_count;4}}, queued... +[1467205013.34][CONN][RXD] >>> Running 4 test cases... +[1467205013.37][CONN][RXD] {{__testcase_name;Basic}} +[1467205013.37][CONN][INF] found KV pair in stream: {{__testcase_name;Basic}}, queued... +[1467205013.40][CONN][RXD] {{__testcase_name;Blinky}} +[1467205013.40][CONN][INF] found KV pair in stream: {{__testcase_name;Blinky}}, queued... +[1467205013.43][CONN][RXD] {{__testcase_name;C++ stack}} +[1467205013.43][CONN][INF] found KV pair in stream: {{__testcase_name;C++ stack}}, queued... +[1467205013.46][CONN][RXD] {{__testcase_name;C++ heap}} +[1467205013.46][CONN][INF] found KV pair in stream: {{__testcase_name;C++ heap}}, queued... +[1467205013.49][CONN][RXD] >>> Running case #1: 'Basic'... +[1467205013.52][CONN][RXD] {{__testcase_start;Basic}} +[1467205013.52][CONN][INF] found KV pair in stream: {{__testcase_start;Basic}}, queued... +[1467205013.56][CONN][RXD] {{__testcase_finish;Basic;1;0}} +[1467205013.56][CONN][INF] found KV pair in stream: {{__testcase_finish;Basic;1;0}}, queued... +[1467205013.59][CONN][RXD] >>> 'Basic': 1 passed, 0 failed +[1467205013.62][CONN][RXD] >>> Running case #2: 'Blinky'... +[1467205013.65][CONN][RXD] {{__testcase_start;Blinky}} +[1467205013.65][CONN][INF] found KV pair in stream: {{__testcase_start;Blinky}}, queued... +[1467205013.69][CONN][RXD] {{__testcase_finish;Blinky;1;0}} +[1467205013.69][CONN][INF] found KV pair in stream: {{__testcase_finish;Blinky;1;0}}, queued... +[1467205013.72][CONN][RXD] >>> 'Blinky': 1 passed, 0 failed +[1467205013.75][CONN][RXD] >>> Running case #3: 'C++ stack'... +[1467205013.78][CONN][RXD] {{__testcase_start;C++ stack}} +[1467205013.78][CONN][INF] found KV pair in stream: {{__testcase_start;C++ stack}}, queued... +[1467205013.79][CONN][RXD] Static::init +[1467205013.81][CONN][RXD] Static::stack_test +[1467205013.82][CONN][RXD] Stack::init +[1467205013.85][CONN][RXD] Stack::hello +[1467205013.87][CONN][RXD] Stack::destroy +[1467205013.89][CONN][RXD] Static::check_init: OK +[1467205013.91][CONN][RXD] Static::destroy +[1467205013.94][CONN][RXD] {{__testcase_finish;C++ stack;1;0}} +[1467205013.95][CONN][INF] found KV pair in stream: {{__testcase_finish;C++ stack;1;0}}, queued... +[1467205013.98][CONN][RXD] >>> 'C++ stack': 1 passed, 0 failed +[1467205014.02][CONN][RXD] >>> Running case #4: 'C++ heap'... +[1467205014.05][CONN][RXD] {{__testcase_start;C++ heap}} +[1467205014.05][CONN][INF] found KV pair in stream: {{__testcase_start;C++ heap}}, queued... +[1467205014.06][CONN][RXD] Heap::init +[1467205014.07][CONN][RXD] Heap::hello +[1467205014.10][CONN][RXD] Heap::check_init: OK +[1467205014.11][CONN][RXD] Heap::destroy +[1467205014.15][CONN][RXD] {{__testcase_finish;C++ heap;1;0}} +[1467205014.15][CONN][INF] found KV pair in stream: {{__testcase_finish;C++ heap;1;0}}, queued... +[1467205014.18][CONN][RXD] >>> 'C++ heap': 1 passed, 0 failed +[1467205014.22][CONN][RXD] >>> Test cases: 4 passed, 0 failed +[1467205014.25][CONN][RXD] {{__testcase_summary;4;0}} +[1467205014.25][CONN][INF] found KV pair in stream: {{__testcase_summary;4;0}}, queued... +[1467205014.27][CONN][RXD] {{end;success}} +[1467205014.27][CONN][INF] found KV pair in stream: {{end;success}}, queued... +[1467205014.28][CONN][RXD] {{__exit;0}} +[1467205014.28][CONN][INF] found KV pair in stream: {{__exit;0}}, queued... +[1467205014.28][HTST][INF] __exit(0) +[1467205014.28][HTST][INF] test suite run finished after 1.02 sec... +[1467205014.28][CONN][INF] received special even '__host_test_finished' value='True', finishing +[1467205014.31][HTST][INF] CONN exited with code: 0 +[1467205014.31][HTST][INF] Some events in queue +[1467205014.31][HTST][INF] __notify_complete(True) +[1467205014.31][HTST][INF] stopped consuming events +[1467205014.31][HTST][INF] host test result() call skipped, received: True +[1467205014.31][HTST][INF] calling blocking teardown() +[1467205014.31][HTST][INF] teardown() finished +[1467205014.31][HTST][INF] {{result;success}} +""" + + self.OUTPUT_WITH_MEMORY_METRICS = """mbedgt: mbed-host-test-runner: started +[1459245860.90][CONN][RXD] {{__testcase_summary;4;0}} +[1459245860.92][CONN][INF] found KV pair in stream: {{end;success}}, queued... +[1459245860.92][CONN][RXD] {{end;success}} +[1459245860.92][CONN][INF] found KV pair in stream: {{max_heap_usage;2284}}, queued... +[1459245860.92][CONN][RXD] {{end;success}} +[1459245860.92][CONN][INF] found KV pair in stream: {{reserved_heap;124124}}, queued... +[1459245860.92][CONN][RXD] {{end;success}} +[1459245860.92][CONN][INF] found KV pair in stream: {{__thread_info;"BE-EF",42,24}}, queued... +[1459245860.92][CONN][RXD] {{end;success}} +[1459245860.92][HTST][INF] __notify_complete(True) +[1459245860.92][CONN][RXD] {{__coverage_start;c:\Work\core-util/source/PoolAllocator.cpp.gcda;6164636772393034c2733f32...a33e...b9}} +[1459245860.92][HTST][INF] test suite run finished after 0.90 sec... +[1459245860.94][HTST][INF] CONN exited with code: 0 +[1459245860.94][HTST][INF] No events in queue +[1459245860.94][HTST][INF] stopped consuming events +[1459245860.94][HTST][INF] host test result() call skipped, received: True +[1459245860.94][HTST][WRN] missing __exit event from DUT +[1459245860.94][HTST][INF] calling blocking teardown() +[1459245860.94][HTST][INF] teardown() finished +[1459245860.94][HTST][INF] {{result;success}} +""" + + def tearDown(self): + pass + + def test_get_test_result(self): + self.assertEqual(mbed_test_api.TEST_RESULT_OK, mbed_test_api.get_test_result(self.OUTPUT_SUCCESS)) + self.assertEqual(mbed_test_api.TEST_RESULT_FAIL, mbed_test_api.get_test_result(self.OUTPUT_FAILURE)) + self.assertEqual(mbed_test_api.TEST_RESULT_TIMEOUT, mbed_test_api.get_test_result(self.OUTPUT_TIMEOUT)) + self.assertEqual(mbed_test_api.TEST_RESULT_UNDEF, mbed_test_api.get_test_result(self.OUTPUT_UNDEF)) + + def test_get_test_result_ok_len(self): + r = mbed_test_api.get_testcase_utest(self.OUTOUT_CSTRING_TEST, 'C strings: %e %E float formatting') + + self.assertEqual(len(r), 6) + self.assertIn("[1459246276.41][CONN][RXD] >>> Running case #7: 'C strings: %e %E float formatting'...", r) + self.assertIn("[1459246276.46][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: %e %E float formatting}}, queued...", r) + self.assertIn("[1459246276.46][CONN][RXD] {{__testcase_start;C strings: %e %E float formatting}}", r) + self.assertIn("[1459246276.52][CONN][INF] found KV pair in stream: {{__testcase_finish;C strings: %e %E float formatting;1;0}}, queued...", r) + self.assertIn("[1459246276.53][CONN][RXD] {{__testcase_finish;C strings: %e %E float formatting;1;0}}", r) + self.assertIn("[1459246276.59][CONN][RXD] >>> 'C strings: %e %E float formatting': 1 passed, 0 failed", r) + + + def test_get_test_result_fail_len(self): + r = mbed_test_api.get_testcase_utest(self.OUTOUT_CSTRING_TEST, 'C strings: %f %f float formatting') + + self.assertEqual(len(r), 9) + self.assertIn("[1459246275.88][CONN][RXD] >>> Running case #6: 'C strings: %f %f float formatting'...", r) + self.assertIn("[1459246275.94][CONN][INF] found KV pair in stream: {{__testcase_start;C strings: %f %f float formatting}}, queued...", r) + self.assertIn("[1459246275.94][CONN][RXD] {{__testcase_start;C strings: %f %f float formatting}}", r) + self.assertIn("[1459246276.10][CONN][RXD] :57::FAIL: Expected '0.002000 0.924300 15.913200 791.773680 6208.200000 25719.495200 426815.982588 6429271.046000 42468024.930000 212006462.910000' Was '", r) + self.assertIn("'", r) + self.assertIn("[1459246276.18][CONN][RXD] >>> failure with reason 'Assertion Failed' during 'Case Handler'", r) + self.assertIn("[1459246276.25][CONN][INF] found KV pair in stream: {{__testcase_finish;C strings: %f %f float formatting;0;1}}, queued...", r) + self.assertIn("[1459246276.25][CONN][RXD] {{__testcase_finish;C strings: %f %f float formatting;0;1}}", r) + self.assertIn("[1459246276.34][CONN][RXD] >>> 'C strings: %f %f float formatting': 0 passed, 1 failed with reason 'Test Cases Failed'", r) + + def get_testcase_count_and_names(self): + tc_count, tc_names = mbed_test_api.get_testcase_count_and_names(self.OUTOUT_CSTRING_TEST_CASE_COUNT_AND_NAME) + + self.assertEqual(tc_count, 2) + self.assertIn('C strings: strtok', tc_names) + self.assertIn('C strings: strpbrk', tc_names) + + def test_get_test_result_return_val(self): + + test_case_names = [ + 'C strings: %e %E float formatting', + 'C strings: %g %g float formatting', + 'C strings: %i %d integer formatting', + 'C strings: %u %d integer formatting', + 'C strings: %x %E integer formatting', + 'C strings: strpbrk', + 'C strings: strtok' + ] + + for test_case in test_case_names: + r = mbed_test_api.get_testcase_utest(self.OUTOUT_CSTRING_TEST, test_case) + self.assertEqual(len(r), 6) + + # This failing test case has different long lenght + r = mbed_test_api.get_testcase_utest(self.OUTOUT_CSTRING_TEST, 'C strings: %f %f float formatting') + self.assertEqual(len(r), 9) + + def test_get_testcase_summary_failures(self): + r = mbed_test_api.get_testcase_summary("{{__testcase_summary;;}}") + self.assertEqual(None, r) + + r = mbed_test_api.get_testcase_summary("{{__testcase_summary;-1;-2}}") + self.assertEqual(None, r) + + r = mbed_test_api.get_testcase_summary("{{__testcase_summary;A;0}}") + self.assertEqual(None, r) + + def test_get_testcase_summary_value_failures(self): + r = mbed_test_api.get_testcase_summary("[1459246276.95][CONN][INF] found KV pair in stream: {{__testcase_summary;;}}") + self.assertEqual(None, r) + + r = mbed_test_api.get_testcase_summary("[1459246276.95][CONN][INF] found KV pair in stream: {{__testcase_summary;-1;-2}}") + self.assertEqual(None, r) + + r = mbed_test_api.get_testcase_summary("[1459246276.95][CONN][INF] found KV pair in stream: {{__testcase_summary;A;0}}") + self.assertEqual(None, r) + + def test_get_testcase_summary_ok(self): + + r = mbed_test_api.get_testcase_summary("[1459246276.95][CONN][INF] found KV pair in stream: {{__testcase_summary;0;0}}") + self.assertNotEqual(None, r) + self.assertEqual((0, 0), r) + + r = mbed_test_api.get_testcase_summary(self.OUTOUT_CSTRING_TEST) + self.assertNotEqual(None, r) + self.assertEqual((7, 1), r) # {{__testcase_summary;7;1}} + + r = mbed_test_api.get_testcase_summary(self.OUTPUT_SUCCESS) + self.assertNotEqual(None, r) + self.assertEqual((4, 0), r) # {{__testcase_summary;4;0}} + + def test_get_testcase_result(self): + r = mbed_test_api.get_testcase_result(self.OUTOUT_CSTRING_TEST) + self.assertEqual(len(r), 8) + + test_case_names = [ + 'C strings: %e %E float formatting', + 'C strings: %g %g float formatting', + 'C strings: %i %d integer formatting', + 'C strings: %u %d integer formatting', + 'C strings: %x %E integer formatting', + 'C strings: strpbrk', + 'C strings: strtok' + ] + + for test_case in test_case_names: + tc = r[test_case] + # If data structure is correct + self.assertIn('utest_log', tc) + self.assertIn('time_start', tc) + self.assertIn('time_end', tc) + self.assertIn('failed', tc) + self.assertIn('result', tc) + self.assertIn('passed', tc) + self.assertIn('duration', tc) + # values passed + self.assertEqual(tc['passed'], 1) + self.assertEqual(tc['failed'], 0) + self.assertEqual(tc['result_text'], 'OK') + + # Failing test case + tc = r['C strings: %f %f float formatting'] + self.assertEqual(tc['passed'], 0) + self.assertEqual(tc['failed'], 1) + self.assertEqual(tc['result_text'], 'FAIL') + + def test_get_testcase_result_tescase_name_and_count(self): + r = mbed_test_api.get_testcase_result(self.OUTOUT_GENERIC_TESTS_TESCASE_NAME_AND_COUNT) + self.assertEqual(len(r), 4) + + self.assertIn('Basic', r) + self.assertIn('Blinky', r) + self.assertIn('C++ heap', r) + self.assertIn('C++ stack', r) + + def test_get_testcase_result_tescase_name_and_count(self): + r = mbed_test_api.get_testcase_result(self.OUTOUT_CSTRING_TEST_CASE_COUNT_AND_NAME) + self.assertEqual(len(r), 2) + + self.assertIn('C strings: strpbrk', r) + self.assertIn('C strings: strtok', r) + + self.assertEqual(r['C strings: strpbrk']['result_text'], 'SKIPPED') + self.assertEqual(r['C strings: strtok']['result_text'], 'ERROR') + + + def test_get_test_results_empty_output(self): + result = mbed_test_api.get_test_result("") + self.assertEqual(result, "TIMEOUT") + + def test_get_memory_metrics(self): + result = mbed_test_api.get_memory_metrics(self.OUTPUT_WITH_MEMORY_METRICS) + + self.assertEqual(result[0], 2284) + self.assertEqual(result[1], 124124) + thread_info = list(result[2])[0] + self.assertIn("entry", thread_info) + self.assertEqual(thread_info["entry"], "BE") + self.assertIn("stack_size", thread_info) + self.assertEqual(thread_info["stack_size"], 24) + self.assertIn("max_stack", thread_info) + self.assertEqual(thread_info["max_stack"], 42) + self.assertIn("arg", thread_info) + self.assertEqual(thread_info["arg"], "EF") + + def test_get_testcase_result_start_tag_missing(self): + result = mbed_test_api.get_testcase_result(self.OUTPUT_STARTTAG_MISSING) + self.assertEqual(result['DNS query']['utest_log'], "__testcase_start tag not found.") + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_test_filtered_test_list.py b/tools/python/python_tests/mbed_greentea/mbed_gt_test_filtered_test_list.py new file mode 100644 index 0000000000..65938ce4c1 --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_test_filtered_test_list.py @@ -0,0 +1,294 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 mbed_greentea import mbed_greentea_cli + +class GreenteaFilteredTestList(unittest.TestCase): + + def setUp(self): + self.ctest_test_list = {'test1': '\\build\\test1.bin', + 'test2': '\\build\\test2.bin', + 'test3': '\\build\\test3.bin', + 'test4': '\\build\\test4.bin'} + self.test_by_names = None + self.skip_test = None + + self.ctest_test_list_mbed_drivers = {'mbed-drivers-test-c_strings' : './build/mbed-drivers-test-c_strings.bin', + 'mbed-drivers-test-dev_null' : './build/mbed-drivers-test-dev_null.bin', + 'mbed-drivers-test-echo' : './build/mbed-drivers-test-echo.bin', + 'mbed-drivers-test-generic_tests' : './build/mbed-drivers-test-generic_tests.bin', + 'mbed-drivers-test-rtc' : './build/mbed-drivers-test-rtc.bin', + 'mbed-drivers-test-stl_features' : './build/mbed-drivers-test-stl_features.bin', + 'mbed-drivers-test-ticker' : './build/mbed-drivers-test-ticker.bin', + 'mbed-drivers-test-ticker_2' : './build/mbed-drivers-test-ticker_2.bin', + 'mbed-drivers-test-ticker_3' : './build/mbed-drivers-test-ticker_3.bin', + 'mbed-drivers-test-timeout' : './build/mbed-drivers-test-timeout.bin', + 'mbed-drivers-test-wait_us' : './build/mbed-drivers-test-wait_us.bin'} + + self.ctest_test_list_mbed_drivers_ext = {'tests-integration-threaded_blinky' : './build/tests-integration-threaded_blinky.bin', + 'tests-mbed_drivers-c_strings' : './build/tests-mbed_drivers-c_strings.bin', + 'tests-mbed_drivers-callback' : './build/tests-mbed_drivers-callback.bin', + 'tests-mbed_drivers-dev_null' : './build/tests-mbed_drivers-dev_null.bin', + 'tests-mbed_drivers-echo' : './build/tests-mbed_drivers-echo.bin', + 'tests-mbed_drivers-generic_tests' : './build/tests-mbed_drivers-generic_tests.bin', + 'tests-mbed_drivers-rtc' : './build/tests-mbed_drivers-rtc.bin', + 'tests-mbed_drivers-stl_features' : './build/tests-mbed_drivers-stl_features.bin', + 'tests-mbed_drivers-ticker' : './build/tests-mbed_drivers-ticker.bin', + 'tests-mbed_drivers-ticker_2' : './build/tests-mbed_drivers-ticker_2.bin', + 'tests-mbed_drivers-ticker_3' : './build/tests-mbed_drivers-ticker_3.bin', + 'tests-mbed_drivers-timeout' : './build/tests-mbed_drivers-timeout.bin', + 'tests-mbed_drivers-wait_us' : './build/tests-mbed_drivers-wait_us.bin', + 'tests-mbedmicro-mbed-attributes' : './build/tests-mbedmicro-mbed-attributes.bin', + 'tests-mbedmicro-mbed-call_before_main' : './build/tests-mbedmicro-mbed-call_before_main.bin', + 'tests-mbedmicro-mbed-cpp' : './build/tests-mbedmicro-mbed-cpp.bin', + 'tests-mbedmicro-mbed-div' : './build/tests-mbedmicro-mbed-div.bin', + 'tests-mbedmicro-mbed-heap_and_stack' : './build/tests-mbedmicro-mbed-heap_and_stack.bin', + 'tests-mbedmicro-rtos-mbed-basic' : './build/tests-mbedmicro-rtos-mbed-basic.bin', + 'tests-mbedmicro-rtos-mbed-isr' : './build/tests-mbedmicro-rtos-mbed-isr.bin', + 'tests-mbedmicro-rtos-mbed-mail' : './build/tests-mbedmicro-rtos-mbed-mail.bin', + 'tests-mbedmicro-rtos-mbed-mutex' : './build/tests-mbedmicro-rtos-mbed-mutex.bin', + 'tests-mbedmicro-rtos-mbed-queue' : './build/tests-mbedmicro-rtos-mbed-queue.bin', + 'tests-mbedmicro-rtos-mbed-semaphore' : './build/tests-mbedmicro-rtos-mbed-semaphore.bin', + 'tests-mbedmicro-rtos-mbed-signals' : './build/tests-mbedmicro-rtos-mbed-signals.bin', + 'tests-mbedmicro-rtos-mbed-threads' : './build/tests-mbedmicro-rtos-mbed-threads.bin', + 'tests-mbedmicro-rtos-mbed-timer' : './build/tests-mbedmicro-rtos-mbed-timer.bin', + 'tests-storage_abstraction-basicapi' : './build/tests-storage_abstraction-basicapi.bin'} + + def tearDown(self): + pass + + def test_filter_test_list(self): + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list, + self.test_by_names, + self.skip_test) + + filtered_test_list = {'test1': '\\build\\test1.bin', + 'test2': '\\build\\test2.bin', + 'test3': '\\build\\test3.bin', + 'test4': '\\build\\test4.bin'} + self.assertEqual(filtered_test_list, filtered_ctest_test_list) + + def test_skip_test(self): + self.skip_test = 'test1,test2' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list, + self.test_by_names, + self.skip_test) + + filtered_test_list = {'test3': '\\build\\test3.bin', + 'test4': '\\build\\test4.bin'} + self.assertEqual(filtered_test_list, filtered_ctest_test_list) + + def test_skip_test_invaild(self): + self.skip_test='test1,testXY' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list, + self.test_by_names, + self.skip_test) + + filtered_test_list = {'test2': '\\build\\test2.bin', + 'test3': '\\build\\test3.bin', + 'test4': '\\build\\test4.bin'} + self.assertEqual(filtered_test_list, filtered_ctest_test_list) + + def test_test_by_names(self): + self.test_by_names='test3' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list, + self.test_by_names, + self.skip_test) + + filtered_test_list = {'test3': '\\build\\test3.bin'} + self.assertEqual(filtered_test_list, filtered_ctest_test_list) + + def test_test_by_names_invalid(self): + self.test_by_names='test3,testXY' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list, + self.test_by_names, + self.skip_test) + + filtered_test_list = {'test3': '\\build\\test3.bin'} + self.assertEqual(filtered_test_list, filtered_ctest_test_list) + + def test_list_is_None_skip_test(self): + self.ctest_test_list = None + self.skip_test='test3' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list, + self.test_by_names, + self.skip_test) + + filtered_test_list = {} + self.assertEqual(filtered_test_list, filtered_ctest_test_list) + + def test_list_is_None_test_by_names(self): + self.ctest_test_list = None + self.test_by_names='test3' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list, + self.test_by_names, + self.skip_test) + + filtered_test_list = {} + self.assertEqual(filtered_test_list, filtered_ctest_test_list) + + def test_list_is_Empty_skip_test(self): + self.ctest_test_list = {} + self.skip_test='test4' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list, + self.test_by_names, + self.skip_test) + + filtered_test_list = {} + self.assertEqual(filtered_test_list, filtered_ctest_test_list) + + def test_list_is_Empty_test_by_names(self): + self.ctest_test_list = {} + self.test_by_names='test4' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list, + self.test_by_names, + self.skip_test) + + filtered_test_list = {} + self.assertEqual(filtered_test_list, filtered_ctest_test_list) + + def test_prefix_filter_one_star(self): + self.test_by_names='mbed-drivers-test-t*' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list_mbed_drivers, + self.test_by_names, + self.skip_test) + + expected = ['mbed-drivers-test-ticker', + 'mbed-drivers-test-ticker_2', + 'mbed-drivers-test-ticker_3', + 'mbed-drivers-test-timeout'] + self.assertEqual(len(expected), len(filtered_ctest_test_list)) + self.assertEqual(set(filtered_ctest_test_list.keys()), set(expected)) + + def test_prefix_filter_one_star_and_no_star(self): + self.test_by_names='mbed-drivers-test-t*,mbed-drivers-test-rtc' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list_mbed_drivers, + self.test_by_names, + self.skip_test) + + expected = ['mbed-drivers-test-ticker', + 'mbed-drivers-test-ticker_2', + 'mbed-drivers-test-ticker_3', + 'mbed-drivers-test-timeout', + 'mbed-drivers-test-rtc'] + self.assertEqual(len(expected), len(filtered_ctest_test_list)) + self.assertEqual(set(filtered_ctest_test_list.keys()), set(expected)) + + def test_prefix_filter_no_star(self): + self.test_by_names='mbed-drivers-test-ticker_2,mbed-drivers-test-rtc,mbed-drivers-test-ticker' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list_mbed_drivers, + self.test_by_names, + self.skip_test) + + expected = ['mbed-drivers-test-ticker', + 'mbed-drivers-test-ticker_2', + 'mbed-drivers-test-rtc'] + self.assertEqual(len(expected), len(filtered_ctest_test_list)) + self.assertEqual(set(filtered_ctest_test_list.keys()), set(expected)) + + def test_prefix_filter_merge_n_and_i(self): + self.test_by_names='mbed-drivers-test-ticker_2,mbed-drivers-test-ticker_3,mbed-drivers-test-rtc,mbed-drivers-test-ticker' + self.skip_test = 'mbed-drivers-test-ticker_3' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list_mbed_drivers, + self.test_by_names, + self.skip_test) + + expected = ['mbed-drivers-test-ticker', + 'mbed-drivers-test-ticker_2', + 'mbed-drivers-test-rtc'] + self.assertEqual(len(expected), len(filtered_ctest_test_list)) + self.assertEqual(set(filtered_ctest_test_list.keys()), set(expected)) + + def test_prefix_filter_merge_n_and_i_repeated(self): + self.test_by_names='mbed-drivers-test-ticker_2,mbed-drivers-test-ticker_3,mbed-drivers-test-rtc,mbed-drivers-test-ticker' + self.skip_test = 'mbed-drivers-test-ticker_3,mbed-drivers-test-ticker_3' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list_mbed_drivers, + self.test_by_names, + self.skip_test) + + expected = ['mbed-drivers-test-ticker', + 'mbed-drivers-test-ticker_2', + 'mbed-drivers-test-rtc'] + self.assertEqual(len(expected), len(filtered_ctest_test_list)) + self.assertEqual(set(filtered_ctest_test_list.keys()), set(expected)) + + def test_prefix_filter_merge_n_and_i_missing(self): + self.test_by_names='mbed-drivers-test-ticker_2,mbed-drivers-test-ticker_3,mbed-drivers-test-rtc,mbed-drivers-test-ticker' + self.skip_test = 'mbed-drivers-test-ticker_XXX' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list_mbed_drivers, + self.test_by_names, + self.skip_test) + + expected = ['mbed-drivers-test-ticker', + 'mbed-drivers-test-ticker_2', + 'mbed-drivers-test-ticker_3', + 'mbed-drivers-test-rtc'] + self.assertEqual(len(expected), len(filtered_ctest_test_list)) + self.assertEqual(set(filtered_ctest_test_list.keys()), set(expected)) + + def test_prefix_filter_merge_n_multi_star(self): + self.test_by_names='tests-mbedmicro-mbed*,tests-mbedmicro-rtos*' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list_mbed_drivers_ext, + self.test_by_names, + self.skip_test) + + expected = ['tests-mbedmicro-mbed-attributes', + 'tests-mbedmicro-mbed-call_before_main', + 'tests-mbedmicro-mbed-cpp', + 'tests-mbedmicro-mbed-div', + 'tests-mbedmicro-mbed-heap_and_stack', + 'tests-mbedmicro-rtos-mbed-basic', + 'tests-mbedmicro-rtos-mbed-isr', + 'tests-mbedmicro-rtos-mbed-mail', + 'tests-mbedmicro-rtos-mbed-mutex', + 'tests-mbedmicro-rtos-mbed-queue', + 'tests-mbedmicro-rtos-mbed-semaphore', + 'tests-mbedmicro-rtos-mbed-signals', + 'tests-mbedmicro-rtos-mbed-threads', + 'tests-mbedmicro-rtos-mbed-timer'] + + self.assertEqual(len(expected), len(filtered_ctest_test_list)) + self.assertEqual(set(filtered_ctest_test_list.keys()), set(expected)) + + def test_prefix_filter_merge_n_multi_star_and_i(self): + self.test_by_names='tests-mbedmicro-mbed*,tests-mbedmicro-rtos*' + self.skip_test='tests-mbedmicro-rtos-mbed-isr,tests-mbedmicro-rtos-mbed-semaphore,tests-mbedmicro-mbed-call_before_main' + filtered_ctest_test_list = mbed_greentea_cli.create_filtered_test_list(self.ctest_test_list_mbed_drivers_ext, + self.test_by_names, + self.skip_test) + + expected = ['tests-mbedmicro-mbed-attributes', + #'tests-mbedmicro-mbed-call_before_main', + 'tests-mbedmicro-mbed-cpp', + 'tests-mbedmicro-mbed-div', + 'tests-mbedmicro-mbed-heap_and_stack', + 'tests-mbedmicro-rtos-mbed-basic', + #'tests-mbedmicro-rtos-mbed-isr', + 'tests-mbedmicro-rtos-mbed-mail', + 'tests-mbedmicro-rtos-mbed-mutex', + 'tests-mbedmicro-rtos-mbed-queue', + #'tests-mbedmicro-rtos-mbed-semaphore', + 'tests-mbedmicro-rtos-mbed-signals', + 'tests-mbedmicro-rtos-mbed-threads', + 'tests-mbedmicro-rtos-mbed-timer'] + + self.assertEqual(len(expected), len(filtered_ctest_test_list)) + self.assertEqual(set(filtered_ctest_test_list.keys()), set(expected)) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_tests_spec.py b/tools/python/python_tests/mbed_greentea/mbed_gt_tests_spec.py new file mode 100644 index 0000000000..79ddf4c998 --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_tests_spec.py @@ -0,0 +1,232 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 +import unittest +from mbed_greentea.tests_spec import Test, TestBinary, TestBuild, TestSpec + +simple_test_spec = { + "builds": { + "K64F-ARM": { + "platform": "K64F", + "toolchain": "ARM", + "base_path": "./.build/K64F/ARM", + "baud_rate": 115200, + "tests": { + "mbed-drivers-test-generic_tests":{ + "binaries":[ + { + "binary_type": "bootable", + "path": "./.build/K64F/ARM/mbed-drivers-test-generic_tests.bin" + } + ] + }, + "mbed-drivers-test-c_strings":{ + "binaries":[ + { + "binary_type": "bootable", + "path": "./.build/K64F/ARM/mbed-drivers-test-c_strings.bin" + } + ] + } + } + }, + "K64F-GCC": { + "platform": "K64F", + "toolchain": "GCC_ARM", + "base_path": "./.build/K64F/GCC_ARM", + "baud_rate": 9600, + "tests": { + "mbed-drivers-test-generic_tests":{ + "binaries":[ + { + "binary_type": "bootable", + "path": "./.build/K64F/GCC_ARM/mbed-drivers-test-generic_tests.bin" + } + ] + } + + } + } + + } +} + + +class TestsSpecFunctionality(unittest.TestCase): + + def setUp(self): + self.ts_2_builds = simple_test_spec + + def tearDown(self): + pass + + def test_example(self): + self.assertEqual(True, True) + self.assertNotEqual(True, False) + + def test_initialise_test_spec_with_filename(self): + root_path = os.path.dirname(os.path.realpath(__file__)) + spec_path = os.path.join(root_path, "resources", "test_spec.json") + + self.test_spec = TestSpec(spec_path) + test_builds = self.test_spec.get_test_builds() + + build = list(filter(lambda x: x.get_name() == "K64F-ARM", test_builds))[0] + self.assertEqual(build.get_name(), "K64F-ARM") + self.assertEqual(build.get_platform(), "K64F") + self.assertEqual(build.get_baudrate(), 9600) + self.assertEqual(build.get_path(), "./BUILD/K64F/ARM") + + self.assertEqual(len(build.get_tests()), 2) + self.assertTrue("tests-example-1" in build.get_tests()) + self.assertTrue("tests-example-2" in build.get_tests()) + + test = build.get_tests()["tests-example-1"] + self.assertEqual(test.get_name(), "tests-example-1") + self.assertEqual(test.get_binary().get_path(), "./BUILD/K64F/ARM/tests-mbedmicro-rtos-mbed-mail.bin") + + self.assertIs(type(test_builds), list) + self.assertEqual(len(test_builds), 2) + + def test_initialise_test_spec_with_invalid_filename(self): + root_path = os.path.dirname(os.path.realpath(__file__)) + spec_path = os.path.join(root_path, "resources", "null.json") + + self.test_spec = TestSpec(spec_path) + test_builds = self.test_spec.get_test_builds() + + def test_manually_add_test_binary(self): + test_name = "example-test" + test_path = "test-path" + test = Test(test_name) + self.assertEqual(test.get_name(), test_name) + self.assertEqual(test.get_binary(), None) + + test.add_binary(test_path, "bootable") + self.assertEqual(test.get_binary().get_path(), test_path) + + def test_manually_add_test_to_build(self): + name = "example-test" + test = Test(name) + test_build = TestBuild("build", "K64F", "ARM", 9600, "./") + + self.assertEqual(len(test_build.get_tests()), 0) + test_build.add_test(name, test) + self.assertEqual(len(test_build.get_tests()), 1) + self.assertTrue(name in test_build.get_tests()) + + def test_manually_add_test_build_to_test_spec(self): + test_name = "example-test" + test = Test(test_name) + test_spec = TestSpec(None) + build_name = "example-build" + test_build = TestBuild(build_name, "K64F", "ARM", 9600, "./") + test_build.add_test(test_name, test) + + self.assertEqual(len(test_spec.get_test_builds()), 0) + test_spec.add_test_builds(build_name, test_build) + self.assertEqual(len(test_spec.get_test_builds()), 1) + self.assertTrue(build_name in test_spec.get_test_builds()[0].get_name()) + + def test_get_test_builds(self): + self.test_spec = TestSpec() + self.test_spec.parse(self.ts_2_builds) + test_builds = self.test_spec.get_test_builds() + + self.assertIs(type(test_builds), list) + self.assertEqual(len(test_builds), 2) + + def test_get_test_builds_names(self): + self.test_spec = TestSpec() + self.test_spec.parse(self.ts_2_builds) + test_builds = self.test_spec.get_test_builds() + test_builds_names = [x.get_name() for x in self.test_spec.get_test_builds()] + + self.assertEqual(len(test_builds_names), 2) + self.assertIs(type(test_builds_names), list) + + self.assertIn('K64F-ARM', test_builds_names) + self.assertIn('K64F-GCC', test_builds_names) + + def test_get_test_build(self): + self.test_spec = TestSpec() + self.test_spec.parse(self.ts_2_builds) + test_builds = self.test_spec.get_test_builds() + test_builds_names = [x.get_name() for x in self.test_spec.get_test_builds()] + + self.assertEqual(len(test_builds_names), 2) + self.assertIs(type(test_builds_names), list) + + self.assertNotEqual(None, self.test_spec.get_test_build('K64F-ARM')) + self.assertNotEqual(None, self.test_spec.get_test_build('K64F-GCC')) + + def test_get_build_properties(self): + self.test_spec = TestSpec() + self.test_spec.parse(self.ts_2_builds) + test_builds = self.test_spec.get_test_builds() + test_builds_names = [x.get_name() for x in self.test_spec.get_test_builds()] + + self.assertEqual(len(test_builds_names), 2) + self.assertIs(type(test_builds_names), list) + + k64f_arm = self.test_spec.get_test_build('K64F-ARM') + k64f_gcc = self.test_spec.get_test_build('K64F-GCC') + + self.assertNotEqual(None, k64f_arm) + self.assertNotEqual(None, k64f_gcc) + + self.assertEqual('K64F', k64f_arm.get_platform()) + self.assertEqual('ARM', k64f_arm.get_toolchain()) + self.assertEqual(115200, k64f_arm.get_baudrate()) + + self.assertEqual('K64F', k64f_gcc.get_platform()) + self.assertEqual('GCC_ARM', k64f_gcc.get_toolchain()) + self.assertEqual(9600, k64f_gcc.get_baudrate()) + + def test_get_test_builds_properties(self): + self.test_spec = TestSpec() + self.test_spec.parse(self.ts_2_builds) + test_builds = self.test_spec.get_test_builds() + test_builds_names = [x.get_name() for x in self.test_spec.get_test_builds()] + + self.assertIn('K64F-ARM', test_builds_names) + self.assertIn('K64F-GCC', test_builds_names) + + def test_get_test_builds_names_filter_by_names(self): + self.test_spec = TestSpec() + self.test_spec.parse(self.ts_2_builds) + + filter_by_names = ['K64F-ARM'] + test_builds = self.test_spec.get_test_builds(filter_by_names=filter_by_names) + test_builds_names = [x.get_name() for x in test_builds] + self.assertEqual(len(test_builds_names), 1) + self.assertIn('K64F-ARM', test_builds_names) + + filter_by_names = ['K64F-GCC'] + test_builds = self.test_spec.get_test_builds(filter_by_names=filter_by_names) + test_builds_names = [x.get_name() for x in test_builds] + self.assertEqual(len(test_builds_names), 1) + self.assertIn('K64F-GCC', test_builds_names) + + filter_by_names = ['SOME-PLATFORM-NAME'] + test_builds = self.test_spec.get_test_builds(filter_by_names=filter_by_names) + test_builds_names = [x.get_name() for x in test_builds] + self.assertEqual(len(test_builds_names), 0) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_yotta_api.py b/tools/python/python_tests/mbed_greentea/mbed_gt_yotta_api.py new file mode 100644 index 0000000000..fd39c2e350 --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_yotta_api.py @@ -0,0 +1,87 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 +import shutil +import tempfile +import unittest +from mbed_greentea import mbed_yotta_api + +def generate_paths_and_write(data): + # Generate some dummy temp directories + curr_dir = os.getcwd() + temp0_dir = tempfile.mkdtemp() + temp1_dir = os.mkdir(os.path.join(temp0_dir, "yotta_targets")) + temp2_dir = os.mkdir(os.path.join(temp0_dir, "yotta_targets", "target_name")) + + with open(os.path.join(os.path.join(temp0_dir, "yotta_targets", "target_name"), "target.json"), "w") as f: + f.write(data) + + return temp0_dir + +class YottaApiFunctionality(unittest.TestCase): + def setUp(self): + self.curr_dir = os.getcwd() + + def tearDown(self): + pass + + def test_build_with_yotta_invalid_target_name(self): + res, ret = mbed_yotta_api.build_with_yotta("invalid_name", True, build_to_debug=True) + self.assertEqual(res, False) + + res, ret = mbed_yotta_api.build_with_yotta("invalid_name", True, build_to_release=True) + self.assertEqual(res, False) + + def test_get_platform_name_from_yotta_target_invalid_target_file(self): + temp0_dir = generate_paths_and_write("test") + + os.chdir(temp0_dir) + result = mbed_yotta_api.get_platform_name_from_yotta_target("target_name") + self.assertEqual(result, None) + os.chdir(self.curr_dir) + + shutil.rmtree(temp0_dir) + + def test_get_platform_name_from_yotta_target_missing_keywords(self): + temp0_dir = generate_paths_and_write("{}") + + os.chdir(temp0_dir) + result = mbed_yotta_api.get_platform_name_from_yotta_target("target_name") + self.assertEqual(result, None) + os.chdir(self.curr_dir) + + shutil.rmtree(temp0_dir) + + def test_get_platform_name_from_yotta_target_missing_targets(self): + temp0_dir = generate_paths_and_write('{"keywords": []}') + + os.chdir(temp0_dir) + result = mbed_yotta_api.get_platform_name_from_yotta_target("target_name") + self.assertEqual(result, None) + os.chdir(self.curr_dir) + + shutil.rmtree(temp0_dir) + + def test_get_platform_name_from_yotta_target_valid_targets(self): + temp0_dir = generate_paths_and_write('{"keywords": ["mbed-target:K64F"]}') + + os.chdir(temp0_dir) + result = mbed_yotta_api.get_platform_name_from_yotta_target("target_name") + self.assertEqual(result, "K64F") + os.chdir(self.curr_dir) + + shutil.rmtree(temp0_dir) diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_yotta_config.py b/tools/python/python_tests/mbed_greentea/mbed_gt_yotta_config.py new file mode 100644 index 0000000000..5d8627fdf1 --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_yotta_config.py @@ -0,0 +1,175 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 mbed_greentea.mbed_yotta_module_parse import YottaConfig + + +class YOttaConfigurationParse(unittest.TestCase): + + def setUp(self): + self.YOTTA_CONFIG_LONG = { + "minar": { + "initial_event_pool_size": 50, + "additional_event_pools_size": 100 + }, + "mbed-os": { + "net": { + "stacks": { + "lwip": True + } + }, + "stdio": { + "default-baud": 9600 + } + }, + "cmsis": { + "nvic": { + "ram_vector_address": "0x1FFF0000", + "flash_vector_address": "0x0", + "user_irq_offset": 16, + "user_irq_number": 86 + } + }, + "hardware": { + "pins": { + "LED_RED": "PTB22", + "LED_GREEN": "PTE26", + "LED_BLUE": "PTB21", + "LED1": "LED_RED", + "LED2": "LED_GREEN", + "LED3": "LED_BLUE", + "LED4": "LED_RED", + "SW2": "PTC6", + "SW3": "PTA4", + "USBTX": "PTB17", + "USBRX": "PTB16", + "D0": "PTC16", + "D1": "PTC17", + "D2": "PTB9", + "D3": "PTA1", + "D4": "PTB23", + "D5": "PTA2", + "D6": "PTC2", + "D7": "PTC3", + "D8": "PTA0", + "D9": "PTC4", + "D10": "PTD0", + "D11": "PTD2", + "D12": "PTD3", + "D13": "PTD1", + "D14": "PTE25", + "D15": "PTE24", + "I2C_SCL": "D15", + "I2C_SDA": "D14", + "A0": "PTB2", + "A1": "PTB3", + "A2": "PTB10", + "A3": "PTB11", + "A4": "PTC10", + "A5": "PTC11", + "DAC0_OUT": "0xFEFE" + }, + "test-pins": { + "spi": { + "mosi": "PTD2", + "miso": "PTD3", + "sclk": "PTD1", + "ssel": "PTD0" + }, + "i2c": { + "sda": "PTE25", + "scl": "PTE24" + }, + "serial": { + "tx": "PTC17", + "rx": "PTD2" + } + } + }, + "uvisor": { + "present": 1 + }, + "arch": { + "arm": {} + }, + "mbed": {} + } + + self.YOTTA_CONFIG_SHORT = { + "minar": { + "initial_event_pool_size": 50, + "additional_event_pools_size": 100 + }, + "mbed-os": { + "net": { + "stacks": { + "lwip": True + } + }, + "stdio": { + "default-baud": 38400 + } + }, + "cmsis": { + "nvic": { + "ram_vector_address": "0x1FFF0000", + "flash_vector_address": "0x0", + "user_irq_offset": 16, + "user_irq_number": 86 + } + }, + } + + def tearDown(self): + pass + + def test_get_baudrate_9600(self): + yotta_config = YottaConfig() + yotta_config.set_yotta_config(self.YOTTA_CONFIG_LONG) + self.assertEqual(yotta_config.get_baudrate(), 9600) + + def test_get_baudrate_38400(self): + yotta_config = YottaConfig() + yotta_config.set_yotta_config(self.YOTTA_CONFIG_SHORT) + self.assertEqual(yotta_config.get_baudrate(), 38400) + + def test_get_baudrate_default_115200(self): + yotta_config = YottaConfig() + self.assertEqual(115200, yotta_config.DEFAULT_BAUDRATE) + + def test_get_baudrate_default_115200_no_yotta_config(self): + yotta_config = YottaConfig() + self.assertEqual(yotta_config.get_baudrate(), yotta_config.DEFAULT_BAUDRATE) + + def test_get_baudrate_None(self): + yotta_config = YottaConfig() + yotta_config.set_yotta_config(None) + self.assertEqual(yotta_config.get_baudrate(), yotta_config.DEFAULT_BAUDRATE) + self.assertEqual(115200, yotta_config.DEFAULT_BAUDRATE) + + def test_get_test_pins(self): + yotta_config = YottaConfig() + yotta_config.set_yotta_config(self.YOTTA_CONFIG_LONG) + self.assertEqual(yotta_config.get_baudrate(), 9600) + self.assertIn('spi', yotta_config.get_test_pins()) + self.assertIn('i2c', yotta_config.get_test_pins()) + self.assertIn('serial', yotta_config.get_test_pins()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_greentea/mbed_gt_yotta_module.py b/tools/python/python_tests/mbed_greentea/mbed_gt_yotta_module.py new file mode 100644 index 0000000000..899ddba2b1 --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/mbed_gt_yotta_module.py @@ -0,0 +1,228 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 mbed_greentea.mbed_yotta_module_parse import YottaModule + + +class YOttaConfigurationParse(unittest.TestCase): + + def setUp(self): + # greentea-client defined in 'dependencies' and 'testDependencies' + self.YOTTA_MODULE_LONG = { + "name": "utest", + "version": "1.9.1", + "description": "Simple test harness with unity and greentea integration.", + "keywords": [ + "greentea", + "testing", + "unittest", + "unity", + "unit", + "test", + "asynchronous", + "async", + "mbed-official" + ], + "author": "Niklas Hauser ", + "license": "Apache-2.0", + "dependencies": { + "minar": "^1.0.0", + "core-util": "^1.0.1", + "compiler-polyfill": "^1.2.0", + "mbed-drivers": "~0.12.0", + "greentea-client": "^0.1.2" + }, + "testDependencies": { + "unity": "^2.0.1", + "greentea-client": "^0.1.2" + } + } + + # greentea-client defined in 'dependencies' + self.YOTTA_MODULE_LONG_IN_DEP = { + "name": "utest", + "version": "1.9.1", + "description": "Simple test harness with unity and greentea integration.", + "keywords": [ + "greentea", + "testing", + "unittest", + "unity", + "unit", + "test", + "asynchronous", + "async", + "mbed-official" + ], + "author": "Niklas Hauser ", + "license": "Apache-2.0", + "dependencies": { + "minar": "^1.0.0", + "core-util": "^1.0.1", + "compiler-polyfill": "^1.2.0", + "mbed-drivers": "~0.12.0", + "greentea-client": "^0.1.2" + }, + "testDependencies": { + "unity": "^2.0.1", + } + } + + # greentea-client defined in 'testDependencies' + self.YOTTA_MODULE_LONG_IN_TESTDEP = { + "name": "utest", + "version": "1.9.1", + "description": "Simple test harness with unity and greentea integration.", + "keywords": [ + "greentea", + "testing", + "unittest", + "unity", + "unit", + "test", + "asynchronous", + "async", + "mbed-official" + ], + "author": "Niklas Hauser ", + "license": "Apache-2.0", + "dependencies": { + "minar": "^1.0.0", + "core-util": "^1.0.1", + "compiler-polyfill": "^1.2.0", + "mbed-drivers": "~0.12.0" + }, + "testDependencies": { + "unity": "^2.0.1", + "greentea-client": "^0.1.2" + } + } + + # No dependency to greentea-client + self.YOTTA_MODULE_LONG_NO_DEP = { + "name": "utest", + "version": "1.9.1", + "description": "Simple test harness with unity and greentea integration.", + "keywords": [ + "greentea", + "testing", + "unittest", + "unity", + "unit", + "test", + "asynchronous", + "async", + "mbed-official" + ], + "author": "Niklas Hauser ", + "license": "Apache-2.0", + "dependencies": { + "minar": "^1.0.0", + "core-util": "^1.0.1", + "compiler-polyfill": "^1.2.0", + "mbed-drivers": "~0.12.0" + }, + "testDependencies": { + "unity": "^2.0.1" + } + } + + # Yotta module itself is 'greentea-client' + self.GREENTEA_CLIENT_MODULE = { + "name": "greentea-client", + "version": "0.1.6", + "description": "greentea client for mbed devices.", + "keywords": [ + "greentea", + "greentea-client", + "mbedgt" + ], + "author": "Przemyslaw.Wirkus ", + "homepage": "https://github.com/ARMmbed/greentea-client", + "repository": { + "url": "git@github.com:ARMmbed/greentea-client.git", + "type": "git" + }, + "license": "Apache-2.0", + "dependencies": { + }, + "testDependencies": { + "utest": "^1.10.0", + "unity": "^2.0.0" + } + } + + + + def tearDown(self): + pass + + def test_get_name(self): + yotta_module = YottaModule() + # Modules using Greentea >= v0.2.0 + for module_json in [self.YOTTA_MODULE_LONG, + self.YOTTA_MODULE_LONG_IN_DEP, + self.YOTTA_MODULE_LONG_IN_TESTDEP, + self.YOTTA_MODULE_LONG_NO_DEP]: + yotta_module.set_yotta_module(module_json) + self.assertEqual('utest', yotta_module.get_name()) + + # 'greentea-client' module itself + yotta_module.set_yotta_module(self.GREENTEA_CLIENT_MODULE) + self.assertEqual('greentea-client', yotta_module.get_name()) + + def test_get_dict_items(self): + yotta_module = YottaModule() + + yotta_module.set_yotta_module(self.YOTTA_MODULE_LONG) + self.assertEqual('Simple test harness with unity and greentea integration.', yotta_module.get_data().get('description')) + self.assertEqual('Apache-2.0', yotta_module.get_data().get('license')) + + def test_check_greentea_client_in_dep(self): + yotta_module = YottaModule() + + yotta_module.set_yotta_module(self.YOTTA_MODULE_LONG) + self.assertTrue(yotta_module.check_greentea_client()) + + yotta_module.set_yotta_module(self.YOTTA_MODULE_LONG_IN_DEP) + self.assertTrue(yotta_module.check_greentea_client()) + + yotta_module.set_yotta_module(self.GREENTEA_CLIENT_MODULE) + self.assertTrue(yotta_module.check_greentea_client()) + + yotta_module.set_yotta_module(self.YOTTA_MODULE_LONG_NO_DEP) + self.assertFalse(yotta_module.check_greentea_client()) + + def test_check_greentea_client_in_test_dep(self): + yotta_module = YottaModule() + + yotta_module.set_yotta_module(self.YOTTA_MODULE_LONG) + self.assertTrue(yotta_module.check_greentea_client()) + + yotta_module.set_yotta_module(self.YOTTA_MODULE_LONG_IN_TESTDEP) + self.assertTrue(yotta_module.check_greentea_client()) + + yotta_module.set_yotta_module(self.GREENTEA_CLIENT_MODULE) + self.assertTrue(yotta_module.check_greentea_client()) + + yotta_module.set_yotta_module(self.YOTTA_MODULE_LONG_NO_DEP) + self.assertFalse(yotta_module.check_greentea_client()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_greentea/report_api.py b/tools/python/python_tests/mbed_greentea/report_api.py new file mode 100644 index 0000000000..88c4ee646b --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/report_api.py @@ -0,0 +1,55 @@ +""" +mbed SDK +Copyright (c) 2017 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 mbed_greentea.mbed_report_api import exporter_html, \ + exporter_memory_metrics_csv, exporter_testcase_junit, \ + exporter_testcase_text, exporter_text, exporter_json + + +class ReportEmitting(unittest.TestCase): + + + report_fns = [exporter_html, exporter_memory_metrics_csv, + exporter_testcase_junit, exporter_testcase_text, + exporter_text, exporter_json] + def test_report_zero_tests(self): + test_data = {} + for report_fn in self.report_fns: + report_fn(test_data) + + def test_report_zero_testcases(self): + test_data = { + 'k64f-gcc_arm': { + 'garbage_test_suite' :{ + u'single_test_result': u'NOT_RAN', + u'elapsed_time': 0.0, + u'build_path': u'N/A', + u'build_path_abs': u'N/A', + u'copy_method': u'N/A', + u'image_path': u'N/A', + u'single_test_output': u'\x80abc' , + u'platform_name': u'k64f', + u'test_bin_name': u'N/A', + u'testcase_result': {}, + } + } + } + for report_fn in self.report_fns: + report_fn(test_data) diff --git a/tools/python/python_tests/mbed_greentea/resources/empty/test/CTestTestfile.cmake b/tools/python/python_tests/mbed_greentea/resources/empty/test/CTestTestfile.cmake new file mode 100644 index 0000000000..096b2a3cc8 --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/resources/empty/test/CTestTestfile.cmake @@ -0,0 +1,6 @@ +# CMake generated Testfile for +# Source directory: c:/Work2/mbed-client/test +# Build directory: c:/Work2/mbed-client/build/frdm-k64f-gcc/test +# +# This file includes the relevant testing commands required for +# testing this directory and lists subdirectories to be tested as well. diff --git a/tools/python/python_tests/mbed_greentea/resources/not-empty/test/CTestTestfile.cmake b/tools/python/python_tests/mbed_greentea/resources/not-empty/test/CTestTestfile.cmake new file mode 100644 index 0000000000..04f0fb431b --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/resources/not-empty/test/CTestTestfile.cmake @@ -0,0 +1,9 @@ +# CMake generated Testfile for +# Source directory: c:/Work2/mbed-client/test +# Build directory: c:/Work2/mbed-client/build/frdm-k64f-gcc/test +# +# This file includes the relevant testing commands required for +# testing this directory and lists subdirectories to be tested as well. +add_test(mbed-client-test-mbedclient-smokeTest "mbed-client-test-mbedclient-smokeTest") +add_test(mbed-client-test-helloworld-mbedclient "mbed-client-test-helloworld-mbedclient") +add_test() diff --git a/tools/python/python_tests/mbed_greentea/resources/test_spec.json b/tools/python/python_tests/mbed_greentea/resources/test_spec.json new file mode 100644 index 0000000000..71fc92beb5 --- /dev/null +++ b/tools/python/python_tests/mbed_greentea/resources/test_spec.json @@ -0,0 +1,44 @@ +{ + "builds": { + "K64F-ARM": { + "platform": "K64F", + "toolchain": "ARM", + "base_path": "./BUILD/K64F/ARM", + "baud_rate": 9600, + "tests": { + "tests-example-1": { + "binaries": [ + { + "binary_type": "bootable", + "path": "./BUILD/K64F/ARM/tests-mbedmicro-rtos-mbed-mail.bin" + } + ] + }, + "tests-example-2": { + "binaries": [ + { + "binary_type": "bootable", + "path": "./BUILD/K64F/ARM/tests-mbed_drivers-c_strings.bin" + } + ] + } + } + }, + "K64F-GCC": { + "platform": "K64F", + "toolchain": "GCC_ARM", + "base_path": "./BUILD/K64F/GCC_ARM", + "baud_rate": 9600, + "tests": { + "tests-example-7": { + "binaries": [ + { + "binary_type": "bootable", + "path": "./BUILD/K64F/GCC_ARM/tests-mbedmicro-rtos-mbed-mail.bin" + } + ] + } + } + } + } +} diff --git a/tools/python/python_tests/mbed_host_tests/__init__.py b/tools/python/python_tests/mbed_host_tests/__init__.py new file mode 100644 index 0000000000..7345469b28 --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/__init__.py @@ -0,0 +1,22 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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. +""" + +"""! @package mbed-host-tests-test + +Unit tests for mbed-host-tests package + +""" diff --git a/tools/python/python_tests/mbed_host_tests/basic.py b/tools/python/python_tests/mbed_host_tests/basic.py new file mode 100644 index 0000000000..547a31bea3 --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/basic.py @@ -0,0 +1,33 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 + +class BasicTestCase(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_example(self): + self.assertEqual(True, True) + self.assertNotEqual(True, False) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_host_tests/basic_ht.py b/tools/python/python_tests/mbed_host_tests/basic_ht.py new file mode 100644 index 0000000000..af94e3be67 --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/basic_ht.py @@ -0,0 +1,36 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 mbed_host_tests import get_plugin_caps + +class BasicHostTestsTestCase(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_get_plugin_caps(self): + d = get_plugin_caps() + self.assertIs(type(d), dict) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_host_tests/conn_primitive_remote.py b/tools/python/python_tests/mbed_host_tests/conn_primitive_remote.py new file mode 100644 index 0000000000..87c468527f --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/conn_primitive_remote.py @@ -0,0 +1,128 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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 +import mock + +from mbed_host_tests.host_tests_conn_proxy.conn_primitive_remote import RemoteConnectorPrimitive + + +class RemoteResourceMock(object): + def __init__(self, requirements): + self._is_allocated = True + self._is_connected = True + self.requirements = requirements + self.open_connection = mock.MagicMock() + self.close_connection = mock.MagicMock() + self.write = mock.MagicMock() + self.read = mock.MagicMock() + self.read.return_value = "abc" + self.disconnect = mock.MagicMock() + self.flash = mock.MagicMock() + self.reset = mock.MagicMock() + self.release = mock.MagicMock() + + @property + def is_connected(self): + return self._is_connected + + @property + def is_allocated(self): + return self._is_allocated + + +class RemoteModuleMock(object): + class SerialParameters(object): + def __init__(self, baudrate): + self.baudrate = baudrate + + def __init__(self, host, port): + self.host = host + self.port = port + self.is_allocated_mock = mock.MagicMock() + self.allocate = mock.MagicMock() + self.allocate.side_effect = lambda req: RemoteResourceMock(req) + self.get_resources = mock.MagicMock() + self.get_resources.return_value = [1] + + @staticmethod + def create(host, port): + return RemoteModuleMock(host, port) + + +class ConnPrimitiveRemoteTestCase(unittest.TestCase): + + def setUp(self): + self.config = { + "grm_module": "RemoteModuleMock", + "tags": "a,b", + "image_path": "test.bin", + "platform_name": "my_platform", + } + self.importer = mock.MagicMock() + self.importer.side_effect = lambda x: RemoteModuleMock + self.remote = RemoteConnectorPrimitive("remote", self.config, self.importer) + + def test_constructor(self): + self.importer.assert_called_once_with("RemoteModuleMock") + + self.remote.client.get_resources.assert_called_once() + self.assertEqual(self.remote.remote_module, RemoteModuleMock) + self.assertIsInstance(self.remote.client, RemoteModuleMock) + self.assertIsInstance(self.remote.selected_resource, RemoteResourceMock) + + # allocate is called + self.remote.client.allocate.assert_called_once_with({ + 'platform_name': self.config.get('platform_name'), + 'power_on': True, + 'connected': True, + 'tags': {"a": True, "b": True}}) + + # flash is called + self.remote.selected_resource.flash.assert_called_once_with("test.bin", forceflash=True) + + # open_connection is called + self.remote.selected_resource.open_connection.assert_called_once() + connect = self.remote.selected_resource.open_connection.call_args[1] + self.assertEqual(connect["parameters"].baudrate, 9600) + + # reset once + self.remote.selected_resource.reset.assert_called_once_with() + + def test_write(self): + self.remote.write("abc") + self.remote.selected_resource.write.assert_called_once_with("abc") + + def test_read(self): + data = self.remote.read(6) + self.remote.selected_resource.read.assert_called_once_with(6) + self.assertEqual(data, "abc") + + def test_reset(self): + self.remote.reset() + self.assertEqual(self.remote.selected_resource.reset.call_count, 2) + + def test_finish(self): + resource = self.remote.selected_resource + self.remote.finish() + self.assertEqual(self.remote.selected_resource, None) + resource.close_connection.assert_called_once() + resource.release.assert_called_once() + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_host_tests/event_callback_decorator.py b/tools/python/python_tests/mbed_host_tests/event_callback_decorator.py new file mode 100644 index 0000000000..35801fe71b --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/event_callback_decorator.py @@ -0,0 +1,41 @@ +# Copyright 2015 ARM Limited, All rights reserved +# +# 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 mbed_host_tests.host_tests.base_host_test import BaseHostTest, event_callback + + +class TestEvenCallbackDecorator(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_event_callback_decorator(self): + class Ht(BaseHostTest): + + @event_callback('Hi') + def hi(self, key, value, timestamp): + print('hi') + + @event_callback('Hello') + def hello(self, key, value, timestamp): + print('hello') + h = Ht() + h.setup() + callbacks = h.get_callbacks() + self.assertIn('Hi', callbacks) + self.assertIn('Hello', callbacks) diff --git a/tools/python/python_tests/mbed_host_tests/host_registry.py b/tools/python/python_tests/mbed_host_tests/host_registry.py new file mode 100644 index 0000000000..794fab8fae --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/host_registry.py @@ -0,0 +1,76 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 mbed_host_tests.host_tests_registry import HostRegistry +from mbed_host_tests import BaseHostTest + + +class HostRegistryTestCase(unittest.TestCase): + + class HostTestClassMock(BaseHostTest): + def setup(self): + pass + + def result(self): + pass + + def teardown(self): + pass + + def setUp(self): + self.HOSTREGISTRY = HostRegistry() + + def tearDown(self): + pass + + def test_register_host_test(self): + self.HOSTREGISTRY.register_host_test('host_test_mock_auto', self.HostTestClassMock()) + self.assertEqual(True, self.HOSTREGISTRY.is_host_test('host_test_mock_auto')) + + def test_unregister_host_test(self): + self.HOSTREGISTRY.register_host_test('host_test_mock_2_auto', self.HostTestClassMock()) + self.assertEqual(True, self.HOSTREGISTRY.is_host_test('host_test_mock_2_auto')) + self.assertNotEqual(None, self.HOSTREGISTRY.get_host_test('host_test_mock_2_auto')) + self.HOSTREGISTRY.unregister_host_test('host_test_mock_2_auto') + self.assertEqual(False, self.HOSTREGISTRY.is_host_test('host_test_mock_2_auto')) + + def test_get_host_test(self): + self.HOSTREGISTRY.register_host_test('host_test_mock_3_auto', self.HostTestClassMock()) + self.assertEqual(True, self.HOSTREGISTRY.is_host_test('host_test_mock_3_auto')) + self.assertNotEqual(None, self.HOSTREGISTRY.get_host_test('host_test_mock_3_auto')) + + def test_is_host_test(self): + self.assertEqual(False, self.HOSTREGISTRY.is_host_test('')) + self.assertEqual(False, self.HOSTREGISTRY.is_host_test(None)) + self.assertEqual(False, self.HOSTREGISTRY.is_host_test('xyz')) + + def test_host_test_str_not_empty(self): + for ht_name in self.HOSTREGISTRY.HOST_TESTS: + ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] + self.assertNotEqual(None, ht) + + def test_host_test_has_name_attribute(self): + for ht_name in self.HOSTREGISTRY.HOST_TESTS: + ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] + self.assertTrue(hasattr(ht, 'setup')) + self.assertTrue(hasattr(ht, 'result')) + self.assertTrue(hasattr(ht, 'teardown')) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_host_tests/host_test_base.py b/tools/python/python_tests/mbed_host_tests/host_test_base.py new file mode 100644 index 0000000000..c2342583ea --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/host_test_base.py @@ -0,0 +1,43 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 mbed_host_tests.host_tests_registry import HostRegistry + +class BaseHostTestTestCase(unittest.TestCase): + + def setUp(self): + self.HOSTREGISTRY = HostRegistry() + + def tearDown(self): + pass + + def test_host_test_has_setup_teardown_attribute(self): + for ht_name in self.HOSTREGISTRY.HOST_TESTS: + ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] + self.assertTrue(hasattr(ht, 'setup')) + self.assertTrue(hasattr(ht, 'teardown')) + + def test_host_test_has_no_rampUpDown_attribute(self): + for ht_name in self.HOSTREGISTRY.HOST_TESTS: + ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] + self.assertFalse(hasattr(ht, 'rampUp')) + self.assertFalse(hasattr(ht, 'rampDown')) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_host_tests/host_test_os_detect.py b/tools/python/python_tests/mbed_host_tests/host_test_os_detect.py new file mode 100644 index 0000000000..27753fd904 --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/host_test_os_detect.py @@ -0,0 +1,57 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 + +import os +import re +import sys +import platform +from mbed_host_tests.host_tests_plugins.host_test_plugins import HostTestPluginBase + + +class HostOSDetectionTestCase(unittest.TestCase): + + def setUp(self): + self.plugin_base = HostTestPluginBase() + self.os_names = ['Windows7', 'Ubuntu', 'LinuxGeneric', 'Darwin'] + self.re_float = re.compile("^\d+\.\d+$") + + def tearDown(self): + pass + + def test_os_info(self): + self.assertNotEqual(None, self.plugin_base.mbed_os_info()) + + def test_os_support(self): + self.assertNotEqual(None, self.plugin_base.mbed_os_support()) + + def test_supported_os_name(self): + self.assertIn(self.plugin_base.mbed_os_support(), self.os_names) + + def test_detect_os_support_ext(self): + os_info = (os.name, + platform.system(), + platform.release(), + platform.version(), + sys.platform) + + self.assertEqual(os_info, self.plugin_base.mbed_os_info()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_host_tests/host_test_plugins.py b/tools/python/python_tests/mbed_host_tests/host_test_plugins.py new file mode 100644 index 0000000000..91384ba296 --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/host_test_plugins.py @@ -0,0 +1,43 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 mbed_host_tests.host_tests_plugins.module_reset_mbed import HostTestPluginResetMethod_Mbed + +class HostOSDetectionTestCase(unittest.TestCase): + + def setUp(self): + self.plugin_reset_mbed = HostTestPluginResetMethod_Mbed() + + def tearDown(self): + pass + + def test_examle(self): + pass + + def test_pyserial_version_detect(self): + self.assertEqual(1.0, self.plugin_reset_mbed.get_pyserial_version("1.0")) + self.assertEqual(1.0, self.plugin_reset_mbed.get_pyserial_version("1.0.0")) + self.assertEqual(2.7, self.plugin_reset_mbed.get_pyserial_version("2.7")) + self.assertEqual(2.7, self.plugin_reset_mbed.get_pyserial_version("2.7.1")) + self.assertEqual(3.0, self.plugin_reset_mbed.get_pyserial_version("3.0")) + self.assertEqual(3.0, self.plugin_reset_mbed.get_pyserial_version("3.0.1")) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_host_tests/host_test_scheme.py b/tools/python/python_tests/mbed_host_tests/host_test_scheme.py new file mode 100644 index 0000000000..63f29eb6f6 --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/host_test_scheme.py @@ -0,0 +1,61 @@ +""" +mbed SDK +Copyright (c) 2011-2015 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 six +import unittest +from mbed_host_tests.host_tests_registry import HostRegistry + + +class HostRegistryTestCase(unittest.TestCase): + + def setUp(self): + self.HOSTREGISTRY = HostRegistry() + + def tearDown(self): + pass + + def test_host_test_class_has_test_attr(self): + """ Check if host test has 'result' class member + """ + for i, ht_name in enumerate(self.HOSTREGISTRY.HOST_TESTS): + ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] + if ht is not None: + self.assertEqual(True, hasattr(ht, 'result')) + + def test_host_test_class_test_attr_callable(self): + """ Check if host test has callable 'result' class member + """ + for i, ht_name in enumerate(self.HOSTREGISTRY.HOST_TESTS): + ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] + if ht: + self.assertEqual(True, hasattr(ht, 'result') and callable(getattr(ht, 'result'))) + + def test_host_test_class_test_attr_callable_args_num(self): + """ Check if host test has callable setup(), result() and teardown() class member has 2 arguments + """ + for i, ht_name in enumerate(self.HOSTREGISTRY.HOST_TESTS): + ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] + if ht and hasattr(ht, 'setup') and callable(getattr(ht, 'setup')): + self.assertEqual(1, six.get_function_code(ht.setup).co_argcount) + if ht and hasattr(ht, 'result') and callable(getattr(ht, 'result')): + self.assertEqual(1, six.get_function_code(ht.result).co_argcount) + if ht and hasattr(ht, 'teardown') and callable(getattr(ht, 'teardown')): + self.assertEqual(1, six.get_function_code(ht.teardown).co_argcount) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_host_tests/mps2_copy.py b/tools/python/python_tests/mbed_host_tests/mps2_copy.py new file mode 100644 index 0000000000..6202ecbee5 --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/mps2_copy.py @@ -0,0 +1,49 @@ +""" +mbed SDK +Copyright (c) 2018 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 +import os + +from mbed_host_tests.host_tests_plugins.module_copy_mps2 import HostTestPluginCopyMethod_MPS2 + +class MPS2CopyTestCase(unittest.TestCase): + + def setUp(self): + self.mps2_copy_plugin = HostTestPluginCopyMethod_MPS2() + self.filename = "toto.bin" + # Create the empty file named self.filename + open(self.filename, "w+").close() + + def tearDown(self): + os.remove(self.filename) + + def test_copy_bin(self): + # Check that file has been copied as "mbed.bin" + self.mps2_copy_plugin.mps2_copy(self.filename, ".") + self.assertTrue(os.path.isfile("mbed.bin")) + os.remove("mbed.bin") + + def test_copy_elf(self): + # Check that file has been copied as "mbed.elf" + os.rename(self.filename, "toto.elf") + self.filename = "toto.elf" + self.mps2_copy_plugin.mps2_copy(self.filename, ".") + self.assertTrue(os.path.isfile("mbed.elf")) + os.remove("mbed.elf") + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_host_tests/mps2_reset.py b/tools/python/python_tests/mbed_host_tests/mps2_reset.py new file mode 100644 index 0000000000..07a030dcde --- /dev/null +++ b/tools/python/python_tests/mbed_host_tests/mps2_reset.py @@ -0,0 +1,44 @@ +""" +mbed SDK +Copyright (c) 2018 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 +import mock +import os +import time + +from mbed_host_tests.host_tests_plugins.module_reset_mps2 import HostTestPluginResetMethod_MPS2 + +class MPS2ResetTestCase(unittest.TestCase): + + def setUp(self): + self.mps2_reset_plugin = HostTestPluginResetMethod_MPS2() + + def tearDown(self): + pass + + @mock.patch("os.name", "posix") + @mock.patch("time.sleep") + @mock.patch("mbed_host_tests.host_tests_plugins.module_reset_mps2.HostTestPluginResetMethod_MPS2.run_command") + def test_check_sync(self, run_command_function, sleep_function): + # Check that a sync call has correctly been executed + self.mps2_reset_plugin.execute("reboot.txt", disk=".") + args, _ = run_command_function.call_args + self.assertTrue("sync" in args[0]) + os.remove("reboot.txt") + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_lstools/__init__.py b/tools/python/python_tests/mbed_lstools/__init__.py new file mode 100644 index 0000000000..57f0caca49 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/__init__.py @@ -0,0 +1,22 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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. +""" + +"""! @package mbed-ls-test + +Unit tests for mbed-ls package + +""" \ No newline at end of file diff --git a/tools/python/python_tests/mbed_lstools/base.py b/tools/python/python_tests/mbed_lstools/base.py new file mode 100644 index 0000000000..3833d2cb35 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/base.py @@ -0,0 +1,135 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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 +import os +import errno +import logging +import re +import pkg_resources +import json +from mock import patch, MagicMock +from copy import deepcopy +from six import StringIO + +import mbed_lstools.main as cli +import mbed_os_tools + +try: + basestring +except NameError: + # Python 3 + basestring = str + +class CLIComands(unittest.TestCase): + """ Test the CLI + """ + + def setUp(self): + self._stdout = patch('sys.stdout', new_callable=StringIO) + self.stdout = self._stdout.start() + self.mbeds = MagicMock() + self.args = MagicMock() + self.mbeds.list_mbeds.return_value = [ + {'platform_name': 'foo', 'platform_name_unique': 'foo[0]', + 'mount_point': 'a mount point', 'serial_port': 'a serial port', + 'target_id': 'DEADBEEF', 'daplink_version': 'v12345' + } + ] + + def tearDown(self): + self._stdout.stop() + + def test_print_version(self): + cli.print_version(self.mbeds, self.args) + self.assertIn(mbed_os_tools.VERSION, + self.stdout.getvalue()) + + def test_print_table(self): + cli.print_table(self.mbeds, self.args) + for d in self.mbeds.list_mbeds.return_value: + for v in d.values(): + self.assertIn(v, self.stdout.getvalue()) + + def test_print_simple(self): + cli.print_simple(self.mbeds, self.args) + for d in self.mbeds.list_mbeds.return_value: + for v in d.values(): + self.assertIn(v, self.stdout.getvalue()) + + def test_mbeds_as_json(self): + cli.mbeds_as_json(self.mbeds, self.args) + self.assertEqual(self.mbeds.list_mbeds.return_value, + json.loads(self.stdout.getvalue())) + + def test_json_by_target_id(self): + cli.json_by_target_id(self.mbeds, self.args) + out_dict = json.loads(self.stdout.getvalue()) + for d in out_dict.values(): + self.assertIn(d, self.mbeds.list_mbeds.return_value) + + def test_json_platforms(self): + cli.json_platforms(self.mbeds, self.args) + platform_names = [d['platform_name'] for d + in self.mbeds.list_mbeds.return_value] + for name in json.loads(self.stdout.getvalue()): + self.assertIn(name, platform_names) + + def test_json_platforms_ext(self): + cli.json_platforms_ext(self.mbeds, self.args) + platform_names = [d['platform_name'] for d + in self.mbeds.list_mbeds.return_value] + for name in json.loads(self.stdout.getvalue()).keys(): + self.assertIn(name, platform_names) + + def test_list_platform(self): + self.mbeds.list_manufacture_ids.return_value =""" + foo + bar + baz + """ + cli.list_platforms(self.mbeds, self.args) + self.assertIn(self.mbeds.list_manufacture_ids.return_value, + self.stdout.getvalue()) + +class CLIParser(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_parse_cli_defaults(self): + args = cli.parse_cli([]) + assert callable(args.command) + + def test_parse_cli_conflict(self): + try: + args = cli.parse_cli(["-j", "-J"]) + assert False, "Parsing conflicting options should have failed" + except: + pass + + def test_parse_cli_single_param(self): + for p in ['j', 'J', 'p', 'P', '-version', 'd', 'u']: + args = cli.parse_cli(['-' + p]) + assert callable(args.command) + +class CLISetup(unittest.TestCase): + def test_start_logging(self): + cli.start_logging() diff --git a/tools/python/python_tests/mbed_lstools/details_txt.py b/tools/python/python_tests/mbed_lstools/details_txt.py new file mode 100644 index 0000000000..22ec0af971 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/details_txt.py @@ -0,0 +1,111 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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 +import os +import errno +import logging + +from mbed_lstools.main import create + + + +class ParseMbedHTMTestCase(unittest.TestCase): + """ Unit tests checking HTML parsing code for 'mbed.htm' files + """ + + details_txt_0226 = """Version: 0226 +Build: Aug 24 2015 17:06:30 +Git Commit SHA: 27a236b9fe39c674a703c5c89655fbd26b8e27e1 +Git Local mods: Yes +""" + + details_txt_0240 = """# DAPLink Firmware - see https://mbed.com/daplink +Unique ID: 0240000029164e45002f0012706e0006f301000097969900 +HIF ID: 97969900 +Auto Reset: 0 +Automation allowed: 0 +Daplink Mode: Interface +Interface Version: 0240 +Git SHA: c765cbb590f57598756683254ca38b211693ae5e +Local Mods: 0 +USB Interfaces: MSD, CDC, HID +Interface CRC: 0x26764ebf +""" + + def setUp(self): + self.mbeds = create() + + def tearDown(self): + pass + + def test_simplified_daplink_txt_content(self): + # Fetch lines from DETAILS.TXT + lines = self.details_txt_0226.splitlines() + self.assertEqual(4, len(lines)) + + # Check parsing content + result = self.mbeds._parse_details(lines) + self.assertEqual(4, len(result)) + self.assertIn('Version', result) + self.assertIn('Build', result) + self.assertIn('Git Commit SHA', result) + self.assertIn('Git Local mods', result) + + # Check for daplink_version + self.assertEqual(result['Version'], "0226") + + def test_extended_daplink_txt_content(self): + # Fetch lines from DETAILS.TXT + lines = self.details_txt_0240.splitlines() + self.assertEqual(11, len(lines)) + + # Check parsing content + result = self.mbeds._parse_details(lines) + self.assertEqual(11, len(result)) # 12th would be comment + self.assertIn('Unique ID', result) + self.assertIn('HIF ID', result) + self.assertIn('Auto Reset', result) + self.assertIn('Automation allowed', result) + self.assertIn('Daplink Mode', result) + self.assertIn('Interface Version', result) + self.assertIn('Git SHA', result) + self.assertIn('Local Mods', result) + self.assertIn('USB Interfaces', result) + self.assertIn('Interface CRC', result) + + # Check if we parsed comment line: + # "# DAPLink Firmware - see https://mbed.com/daplink" + for key in result: + # Check if we parsed comment + self.assertFalse(key.startswith('#')) + # Check if we parsed + self.assertFalse('https://mbed.com/daplink' in result[key]) + + # Check for daplink_version + # DAPlink <240 compatibility + self.assertEqual(result['Interface Version'], "0240") + self.assertEqual(result['Version'], "0240") + + def test_(self): + pass + + def test_(self): + pass + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_lstools/detect_os.py b/tools/python/python_tests/mbed_lstools/detect_os.py new file mode 100644 index 0000000000..74fa7d6160 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/detect_os.py @@ -0,0 +1,64 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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 +import os +import sys +import errno +import logging + +import platform +from mbed_lstools.main import create +from mbed_lstools.main import mbed_os_support +from mbed_lstools.main import mbed_lstools_os_info + + +class DetectOSTestCase(unittest.TestCase): + """ Test cases for host OS related functionality. Helpful during porting + """ + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_porting_mbed_lstools_os_info(self): + self.assertNotEqual(None, mbed_lstools_os_info()) + + def test_porting_mbed_os_support(self): + self.assertNotEqual(None, mbed_os_support()) + + def test_porting_create(self): + self.assertNotEqual(None, create()) + + def test_supported_os_name(self): + os_names = ['Windows7', 'Ubuntu', 'LinuxGeneric', 'Darwin'] + self.assertIn(mbed_os_support(), os_names) + + def test_detect_os_support_ext(self): + os_info = (os.name, + platform.system(), + platform.release(), + platform.version(), + sys.platform) + + self.assertEqual(os_info, mbed_lstools_os_info()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_lstools/mbed_htm.py b/tools/python/python_tests/mbed_lstools/mbed_htm.py new file mode 100644 index 0000000000..b06f618dab --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/mbed_htm.py @@ -0,0 +1,104 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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 +import os +import errno +import logging + +from mbed_lstools.main import create + + + + +class ParseMbedHTMTestCase(unittest.TestCase): + """ Unit tests checking HTML parsing code for 'mbed.htm' files + """ + + # DAPlink <0240 + test_mbed_htm_k64f_url_str = '' + test_mbed_htm_l152re_url_str = '' + test_mbed_htm_lpc1768_url_str = '' + test_mbed_htm_nucleo_l031k6_str = '' + test_mbed_htm_nrf51_url_str = '' + + # DAPLink 0240 + test_daplink_240_mbed_html_str = 'window.location.replace("https://mbed.org/device/?code=0240000029164e45002f0012706e0006f301000097969900?version=0240?target_id=0007ffffffffffff4e45315450090023");' + + def setUp(self): + self.mbeds = create() + + def tearDown(self): + pass + + def test_mbed_htm_k64f_url(self): + target_id = self.mbeds._target_id_from_htm(self.test_mbed_htm_k64f_url_str) + self.assertEqual('02400203D94B0E7724B7F3CF', target_id) + + def test_mbed_htm_l152re_url(self): + target_id = self.mbeds._target_id_from_htm(self.test_mbed_htm_l152re_url_str) + self.assertEqual('07100200656A9A955A0F0CB8', target_id) + + def test_mbed_htm_lpc1768_url(self): + target_id = self.mbeds._target_id_from_htm(self.test_mbed_htm_lpc1768_url_str) + self.assertEqual('101000000000000000000002F7F1869557200730298d254d3ff3509e3fe4722d', target_id) + + def test_daplink_nucleo_l031k6_url(self): + target_id = self.mbeds._target_id_from_htm(self.test_mbed_htm_nucleo_l031k6_str) + self.assertEqual('07900221461663077952F5AA', target_id) + + def test_daplink_240_mbed_html(self): + target_id = self.mbeds._target_id_from_htm(self.test_daplink_240_mbed_html_str) + self.assertEqual('0240000029164e45002f0012706e0006f301000097969900', target_id) + + def test_mbed_htm_nrf51_url(self): + target_id = self.mbeds._target_id_from_htm(self.test_mbed_htm_nrf51_url_str) + self.assertEqual('1100021952333120353935373130313232323032AFD5DFD8', target_id) + + def get_mbed_htm_comment_section_ver_build(self): + # Incorrect data + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNone(ver_bld) + + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNone(ver_bld) + + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNone(ver_bld) + + # Correct data + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNotNone(ver_bld) + self.assertEqual(('0200', 'Mar 26 2014 13:22:20'), ver_bld) + + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNotNone(ver_bld) + self.assertEqual(('0200', 'Aug 27 2014 13:29:28'), ver_bld) + + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNotNone(ver_bld) + self.assertEqual(('0219', 'Feb 2 2016 15:20:54'), ver_bld) + + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNotNone(ver_bld) + self.assertEqual(('0.14.3', '471'), ver_bld) + + def test_(self): + pass + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_lstools/mbedls_toolsbase.py b/tools/python/python_tests/mbed_lstools/mbedls_toolsbase.py new file mode 100644 index 0000000000..0a90017831 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/mbedls_toolsbase.py @@ -0,0 +1,582 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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 +import os +import errno +import logging +import re +import json +from io import StringIO +from mock import patch, mock_open, DEFAULT +from copy import deepcopy + +from mbed_lstools.lstools_base import MbedLsToolsBase, FSInteraction + +class DummyLsTools(MbedLsToolsBase): + return_value = [] + def find_candidates(self): + return self.return_value + +try: + basestring +except NameError: + # Python 3 + basestring = str + +class BasicTestCase(unittest.TestCase): + """ Basic test cases checking trivial asserts + """ + + def setUp(self): + self.base = DummyLsTools(force_mock=True) + + def tearDown(self): + pass + + def test_list_mbeds_valid_platform(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}, + {'mount_point': None, + 'target_id_usb_id': '00000000000', + 'serial_port': 'not_valid'}] + with patch("mbed_lstools.lstools_base.MbedLsToolsBase._read_htm_ids") as _read_htm,\ + patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch("mbed_os_tools.detect.platform_database.PlatformDatabase.get") as _get,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir: + _mpr.return_value = True + _read_htm.return_value = (u'0241BEEFDEAD', {}) + _get.return_value = { + 'platform_name': 'foo_target' + } + _listdir.return_value = ['mbed.htm'] + to_check = self.base.list_mbeds() + _read_htm.assert_called_once_with('dummy_mount_point') + _get.assert_any_call('0241', device_type='daplink', verbose_data=True) + self.assertEqual(len(to_check), 1) + self.assertEqual(to_check[0]['target_id'], "0241BEEFDEAD") + self.assertEqual(to_check[0]['platform_name'], 'foo_target') + + def test_list_mbeds_invalid_tid(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}, + {'mount_point': 'dummy_mount_point', + 'target_id_usb_id': "", + 'serial_port': 'not_valid'}] + with patch("mbed_lstools.lstools_base.MbedLsToolsBase._read_htm_ids") as _read_htm,\ + patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch("mbed_os_tools.detect.platform_database.PlatformDatabase.get") as _get,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir: + _mpr.return_value = True + _read_htm.side_effect = [(u'0241BEEFDEAD', {}), (None, {})] + _get.return_value = { + 'platform_name': 'foo_target' + } + _listdir.return_value = ['mbed.htm'] + to_check = self.base.list_mbeds() + _get.assert_any_call('0241', device_type='daplink', verbose_data=True) + self.assertEqual(len(to_check), 2) + self.assertEqual(to_check[0]['target_id'], "0241BEEFDEAD") + self.assertEqual(to_check[0]['platform_name'], 'foo_target') + self.assertEqual(to_check[1]['target_id'], "") + self.assertEqual(to_check[1]['platform_name'], None) + + def test_list_mbeds_invalid_platform(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'not_in_target_db', + 'serial_port': "dummy_serial_port"}] + for qos in [FSInteraction.BeforeFilter, FSInteraction.AfterFilter]: + with patch("mbed_lstools.lstools_base.MbedLsToolsBase._read_htm_ids") as _read_htm,\ + patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch("mbed_os_tools.detect.platform_database.PlatformDatabase.get") as _get,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir: + _mpr.return_value = True + _read_htm.return_value = (u'not_in_target_db', {}) + _get.return_value = None + _listdir.return_value = ['MBED.HTM'] + to_check = self.base.list_mbeds() + _read_htm.assert_called_once_with('dummy_mount_point') + _get.assert_any_call('not_', device_type='daplink', verbose_data=True) + self.assertEqual(len(to_check), 1) + self.assertEqual(to_check[0]['target_id'], "not_in_target_db") + self.assertEqual(to_check[0]['platform_name'], None) + + def test_list_mbeds_unmount_mid_read(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}] + with patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir: + _mpr.return_value = True + _listdir.side_effect = OSError + to_check = self.base.list_mbeds() + self.assertEqual(len(to_check), 0) + + def test_list_mbeds_read_mbed_htm_failure(self): + def _test(mock): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}] + with patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir,\ + patch('mbed_os_tools.detect.lstools_base.open', mock, create=True): + _mpr.return_value = True + _listdir.return_value = ['MBED.HTM', 'DETAILS.TXT'] + to_check = self.base.list_mbeds() + mock.assert_called_once_with(os.path.join('dummy_mount_point', 'mbed.htm'), 'r') + self.assertEqual(len(to_check), 0) + + m = mock_open() + m.side_effect = OSError + _test(m) + + m.reset_mock() + m.side_effect = IOError + _test(m) + + def test_list_mbeds_read_no_mbed_htm(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}] + + details_txt_contents = '''\ +# DAPLink Firmware - see https://mbed.com/daplink +Unique ID: 0240000032044e4500257009997b00386781000097969900 +HIC ID: 97969900 +Auto Reset: 0 +Automation allowed: 1 +Overflow detection: 1 +Daplink Mode: Interface +Interface Version: 0246 +Bootloader Version: 0244 +Git SHA: 0beabef8aa4b382809d79e98321ecf6a28936812 +Local Mods: 0 +USB Interfaces: MSD, CDC, HID +Bootloader CRC: 0xb92403e6 +Interface CRC: 0x434eddd1 +Remount count: 0 +''' + def _handle_open(*args, **kwargs): + if args[0].lower() == os.path.join('dummy_mount_point', 'mbed.htm'): + raise OSError("(mocked open) No such file or directory: 'mbed.htm'") + else: + return DEFAULT + + m = mock_open(read_data=details_txt_contents) + with patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir,\ + patch('mbed_os_tools.detect.lstools_base.open', m, create=True) as mocked_open: + mocked_open.side_effect = _handle_open + _mpr.return_value = True + _listdir.return_value = ['PRODINFO.HTM', 'DETAILS.TXT'] + to_check = self.base.list_mbeds() + self.assertEqual(len(to_check), 1) + m.assert_called_once_with(os.path.join('dummy_mount_point', 'DETAILS.TXT'), 'r') + self.assertEqual(to_check[0]['target_id'], '0240000032044e4500257009997b00386781000097969900') + + def test_list_mbeds_read_details_txt_failure(self): + def _test(mock): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}] + with patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir,\ + patch("mbed_lstools.lstools_base.MbedLsToolsBase._update_device_from_htm") as _htm,\ + patch('mbed_os_tools.detect.lstools_base.open', mock, create=True): + _mpr.return_value = True + _htm.side_effect = None + _listdir.return_value = ['MBED.HTM', 'DETAILS.TXT'] + to_check = self.base.list_mbeds(read_details_txt=True) + mock.assert_called_once_with(os.path.join('dummy_mount_point', 'DETAILS.TXT'), 'r') + self.assertEqual(len(to_check), 0) + + m = mock_open() + m.side_effect = OSError + _test(m) + + m.reset_mock() + m.side_effect = IOError + _test(m) + + def test_list_mbeds_unmount_mid_read_list_unmounted(self): + self.base.list_unmounted = True + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}] + with patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir: + _mpr.return_value = True + _listdir.side_effect = OSError + to_check = self.base.list_mbeds() + self.assertEqual(len(to_check), 1) + self.assertEqual(to_check[0]['mount_point'], None) + self.assertEqual(to_check[0]['device_type'], 'unknown') + self.assertEqual(to_check[0]['platform_name'], 'K64F') + + def test_list_manufacture_ids(self): + table_str = self.base.list_manufacture_ids() + self.assertTrue(isinstance(table_str, basestring)) + + def test_mock_manufacture_ids_default_multiple(self): + # oper='+' + for mid, platform_name in [('0341', 'TEST_PLATFORM_NAME_1'), + ('0342', 'TEST_PLATFORM_NAME_2'), + ('0343', 'TEST_PLATFORM_NAME_3')]: + self.base.mock_manufacture_id(mid, platform_name) + self.assertEqual(platform_name, self.base.plat_db.get(mid)) + + def test_mock_manufacture_ids_minus(self): + # oper='+' + for mid, platform_name in [('0341', 'TEST_PLATFORM_NAME_1'), + ('0342', 'TEST_PLATFORM_NAME_2'), + ('0343', 'TEST_PLATFORM_NAME_3')]: + self.base.mock_manufacture_id(mid, platform_name) + self.assertEqual(platform_name, self.base.plat_db.get(mid)) + + # oper='-' + mock_ids = self.base.mock_manufacture_id('0342', '', oper='-') + self.assertEqual('TEST_PLATFORM_NAME_1', self.base.plat_db.get("0341")) + self.assertEqual(None, self.base.plat_db.get("0342")) + self.assertEqual('TEST_PLATFORM_NAME_3', self.base.plat_db.get("0343")) + + def test_mock_manufacture_ids_star(self): + # oper='+' + for mid, platform_name in [('0341', 'TEST_PLATFORM_NAME_1'), + ('0342', 'TEST_PLATFORM_NAME_2'), + ('0343', 'TEST_PLATFORM_NAME_3')]: + self.base.mock_manufacture_id(mid, platform_name) + + self.assertEqual(platform_name, self.base.plat_db.get(mid)) + + # oper='-' + self.base.mock_manufacture_id('*', '', oper='-') + self.assertEqual(None, self.base.plat_db.get("0341")) + self.assertEqual(None, self.base.plat_db.get("0342")) + self.assertEqual(None, self.base.plat_db.get("0343")) + + def test_update_device_from_fs_mid_unmount(self): + dummy_mount = 'dummy_mount' + device = { + 'mount_point': dummy_mount + } + + with patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir: + _listdir.side_effect = OSError + self.base._update_device_from_fs(device, False) + self.assertEqual(device['mount_point'], None) + + def test_detect_device_test(self): + device_type = self.base._detect_device_type({ + 'vendor_id': '0483' + }) + self.assertEqual(device_type, 'stlink') + + device_type = self.base._detect_device_type({ + 'vendor_id': '0d28' + }) + self.assertEqual(device_type, 'daplink') + + device_type = self.base._detect_device_type({ + 'vendor_id': '1366' + }) + self.assertEqual(device_type, 'jlink') + + def test_device_type_unmounted(self): + self.base.list_unmounted = True + self.base.return_value = [{'mount_point': None, + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port", + 'vendor_id': '0d28', + 'product_id': '0204'}] + with patch("mbed_os_tools.detect.platform_database.PlatformDatabase.get") as _get,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir: + _get.return_value = { + 'platform_name': 'foo_target' + } + to_check = self.base.list_mbeds() + #_get.assert_any_call('0240', device_type='daplink', verbose_data=True) + self.assertEqual(len(to_check), 1) + self.assertEqual(to_check[0]['target_id'], "0240DEADBEEF") + self.assertEqual(to_check[0]['platform_name'], 'foo_target') + self.assertEqual(to_check[0]['device_type'], 'daplink') + + def test_update_device_details_jlink(self): + jlink_html_contents = ('' + 'NXP Product Page') + _open = mock_open(read_data=jlink_html_contents) + dummy_mount_point = 'dummy' + base_device = { + 'mount_point': dummy_mount_point + } + + with patch('mbed_os_tools.detect.lstools_base.open', _open, create=True): + device = deepcopy(base_device) + device['directory_entries'] = ['Board.html', 'User Guide.html'] + self.base._update_device_details_jlink(device, False) + self.assertEqual(device['url'], 'http://www.nxp.com/FRDM-KL27Z') + self.assertEqual(device['platform_name'], 'KL27Z') + _open.assert_called_once_with(os.path.join(dummy_mount_point, 'Board.html'), 'r') + + _open.reset_mock() + + device = deepcopy(base_device) + device['directory_entries'] = ['User Guide.html'] + self.base._update_device_details_jlink(device, False) + self.assertEqual(device['url'], 'http://www.nxp.com/FRDM-KL27Z') + self.assertEqual(device['platform_name'], 'KL27Z') + _open.assert_called_once_with(os.path.join(dummy_mount_point, 'User Guide.html'), 'r') + + _open.reset_mock() + + device = deepcopy(base_device) + device['directory_entries'] = ['unhelpful_file.html'] + self.base._update_device_details_jlink(device, False) + _open.assert_not_called() + + def test_fs_never(self): + device = { + 'target_id_usb_id': '024075309420ABCE', + 'mount_point': 'invalid_mount_point', + 'serial_port': 'invalid_serial_port' + } + self.base.return_value = [device] + with patch("mbed_lstools.lstools_base.MbedLsToolsBase._update_device_from_fs") as _up_fs,\ + patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as mount_point_ready: + mount_point_ready.return_value = True + + filter = None + ret = self.base.list_mbeds(FSInteraction.Never, filter, read_details_txt=False) + ret_with_details = self.base.list_mbeds(FSInteraction.Never, filter, read_details_txt=True) + self.assertIsNotNone(ret[0]) + self.assertIsNotNone(ret_with_details[0]) + self.assertEqual(ret[0]['target_id'], ret[0]['target_id_usb_id']) + self.assertEqual(ret[0]['platform_name'], "K64F") + self.assertEqual(ret[0], ret_with_details[0]) + _up_fs.assert_not_called() + + filter_in = lambda m: m['platform_name'] == 'K64F' + ret = self.base.list_mbeds(FSInteraction.Never, filter_in, read_details_txt=False) + ret_with_details = self.base.list_mbeds(FSInteraction.Never, filter_in, read_details_txt=True) + self.assertIsNotNone(ret[0]) + self.assertIsNotNone(ret_with_details[0]) + self.assertEqual(ret[0]['target_id'], ret[0]['target_id_usb_id']) + self.assertEqual(ret[0]['platform_name'], "K64F") + self.assertEqual(ret[0], ret_with_details[0]) + _up_fs.assert_not_called() + + filter_out = lambda m: m['platform_name'] != 'K64F' + ret = self.base.list_mbeds(FSInteraction.Never, filter_out, read_details_txt=False) + ret_with_details = self.base.list_mbeds(FSInteraction.Never, filter_out, read_details_txt=True) + _up_fs.assert_not_called() + self.assertEqual(ret, []) + self.assertEqual(ret, ret_with_details) + _up_fs.assert_not_called() + + def test_fs_after(self): + device = { + 'target_id_usb_id': '024075309420ABCE', + 'mount_point': 'invalid_mount_point', + 'serial_port': 'invalid_serial_port' + } + with patch("mbed_lstools.lstools_base.MbedLsToolsBase._read_htm_ids") as _read_htm,\ + patch("mbed_lstools.lstools_base.MbedLsToolsBase._details_txt") as _up_details,\ + patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as mount_point_ready,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir: + new_device_id = "00017531642046" + _read_htm.return_value = (new_device_id, {}) + _listdir.return_value = ['mbed.htm', 'details.txt'] + _up_details.return_value = { + 'automation_allowed': '0' + } + mount_point_ready.return_value = True + + filter = None + self.base.return_value = [deepcopy(device)] + ret = self.base.list_mbeds(FSInteraction.AfterFilter, filter, False, False) + _up_details.assert_not_called() + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds(FSInteraction.AfterFilter, filter, False, True) + + self.assertIsNotNone(ret[0]) + self.assertIsNotNone(ret_with_details[0]) + self.assertEqual(ret[0]['target_id'], new_device_id) + self.assertEqual(ret_with_details[0]['daplink_automation_allowed'], '0') + self.assertDictContainsSubset(ret[0], ret_with_details[0]) + _read_htm.assert_called_with(device['mount_point']) + _up_details.assert_called_with(device['mount_point']) + + _read_htm.reset_mock() + _up_details.reset_mock() + + filter_in = lambda m: m['target_id'] == device['target_id_usb_id'] + filter_details = lambda m: m.get('daplink_automation_allowed', None) == '0' + + self.base.return_value = [deepcopy(device)] + ret = self.base.list_mbeds( + FSInteraction.AfterFilter, filter_in, False, False) + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds( + FSInteraction.AfterFilter, filter_details, False, True) + + self.assertIsNotNone(ret[0]) + self.assertEqual(ret_with_details, []) + self.assertEqual(ret[0]['target_id'], new_device_id) + _read_htm.assert_called_with(device['mount_point']) + _up_details.assert_not_called() + + _read_htm.reset_mock() + _up_details.reset_mock() + + filter_out = lambda m: m['target_id'] == new_device_id + + self.base.return_value = [deepcopy(device)] + ret = self.base.list_mbeds( + FSInteraction.AfterFilter, filter_out, False, False) + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds( + FSInteraction.AfterFilter, filter_out, False, True) + + self.assertEqual(ret, []) + self.assertEqual(ret_with_details, []) + _read_htm.assert_not_called() + _up_details.assert_not_called() + + def test_get_supported_platforms(self): + supported_platforms = self.base.get_supported_platforms() + self.assertTrue(isinstance(supported_platforms, dict)) + self.assertEqual(supported_platforms['0240'], 'K64F') + + def test_fs_before(self): + device = { + 'target_id_usb_id': '024075309420ABCE', + 'mount_point': 'invalid_mount_point', + 'serial_port': 'invalid_serial_port' + } + with patch("mbed_lstools.lstools_base.MbedLsToolsBase._read_htm_ids") as _read_htm,\ + patch("mbed_lstools.lstools_base.MbedLsToolsBase._details_txt") as _up_details,\ + patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as mount_point_ready,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir: + new_device_id = u'00017575430420' + _read_htm.return_value = (new_device_id, {}) + _listdir.return_value = ['mbed.htm', 'details.txt'] + _up_details.return_value = { + 'automation_allowed': '0' + } + mount_point_ready.return_value = True + + filter = None + self.base.return_value = [deepcopy(device)] + ret = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter, False, False) + _up_details.assert_not_called() + + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter, False, True) + self.assertIsNotNone(ret[0]) + self.assertIsNotNone(ret_with_details[0]) + self.assertEqual(ret[0]['target_id'], new_device_id) + self.assertEqual(ret_with_details[0]['daplink_automation_allowed'], '0') + self.assertDictContainsSubset(ret[0], ret_with_details[0]) + _read_htm.assert_called_with(device['mount_point']) + _up_details.assert_called_with(device['mount_point']) + + _read_htm.reset_mock() + _up_details.reset_mock() + + filter_in = lambda m: m['target_id'] == '00017575430420' + filter_in_details = lambda m: m['daplink_automation_allowed'] == '0' + self.base.return_value = [deepcopy(device)] + ret = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter_in, False, False) + _up_details.assert_not_called() + + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter_in_details, False, True) + + self.assertIsNotNone(ret[0]) + self.assertIsNotNone(ret_with_details[0]) + self.assertEqual(ret[0]['target_id'], new_device_id) + self.assertEqual(ret_with_details[0]['daplink_automation_allowed'], '0') + self.assertDictContainsSubset(ret[0], ret_with_details[0]) + _read_htm.assert_called_with(device['mount_point']) + _up_details.assert_called_with(device['mount_point']) + + _read_htm.reset_mock() + _up_details.reset_mock() + + filter_out = lambda m: m['target_id'] == '024075309420ABCE' + filter_out_details = lambda m: m['daplink_automation_allowed'] == '1' + ret = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter_out, False, False) + _up_details.assert_not_called() + + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter_out_details, False, True) + + self.assertEqual(ret, []) + self.assertEqual(ret_with_details, []) + _read_htm.assert_called_with(device['mount_point']) + +class RetargetTestCase(unittest.TestCase): + """ Test cases that makes use of retargetting + """ + + def setUp(self): + retarget_data = { + '0240DEADBEEF': { + 'serial_port' : 'valid' + } + } + + _open = mock_open(read_data=json.dumps(retarget_data)) + + with patch('os.path.isfile') as _isfile,\ + patch('mbed_os_tools.detect.lstools_base.isfile') as _isfile,\ + patch('mbed_os_tools.detect.lstools_base.open', _open, create=True): + _isfile.return_value = True + self.base = DummyLsTools() + _open.assert_called() + + def tearDown(self): + pass + + def test_list_mbeds_valid_platform(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': None}] + with patch('mbed_lstools.lstools_base.MbedLsToolsBase._read_htm_ids') as _read_htm,\ + patch('mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready') as _mpr,\ + patch('mbed_os_tools.detect.platform_database.PlatformDatabase.get') as _get,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir: + _mpr.return_value = True + _read_htm.return_value = (u'0240DEADBEEF', {}) + _get.return_value = { + 'platform_name': 'foo_target' + } + _listdir.return_value = ['mbed.htm'] + to_check = self.base.list_mbeds() + self.assertEqual(len(to_check), 1) + self.assertEqual(to_check[0]['serial_port'], 'valid') + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_lstools/os_darwin.py b/tools/python/python_tests/mbed_lstools/os_darwin.py new file mode 100644 index 0000000000..d112243b95 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/os_darwin.py @@ -0,0 +1,171 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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 +import sys +import plistlib +from mock import MagicMock, patch +from six import BytesIO + +from mbed_lstools.darwin import MbedLsToolsDarwin + +class DarwinTestCase(unittest.TestCase): + """Tests for the Darwin port + """ + + def setUp(self): + with patch("platform.mac_ver") as _pv: + _pv.return_value = ["10.2.2"] + self.darwin = MbedLsToolsDarwin() + + def tearDown(self): + pass + + def test_a_k64f(self): + disks = { + 'AllDisks': ['disk0', 'disk0s1', 'disk0s2', 'disk0s3', 'disk1', 'disk2'], + 'AllDisksAndPartitions': [{ 'Content': 'GUID_partition_scheme', + 'DeviceIdentifier': 'disk0', + 'Partitions': [ + { 'Content': 'EFI', + 'DeviceIdentifier': 'disk0s1', + 'DiskUUID': 'nope', + 'Size': 209715200, + 'VolumeName': 'EFI', + 'VolumeUUID': 'nu-uh'}, + { 'Content': 'Apple_CoreStorage', + 'DeviceIdentifier': 'disk0s2', + 'DiskUUID': 'nodda', + 'Size': 250006216704}, + { 'Content': 'Apple_Boot', + 'DeviceIdentifier': 'disk0s3', + 'DiskUUID': 'no soup for you!', + 'Size': 650002432, + 'VolumeName': 'Recovery HD', + 'VolumeUUID': 'Id rather not'}], + 'Size': 251000193024}, + { 'Content': 'Apple_HFS', + 'DeviceIdentifier': 'disk1', + 'MountPoint': '/', + 'Size': 249653772288, + 'VolumeName': 'Mac HD'}, + { 'Content': '', + 'DeviceIdentifier': 'disk2', + 'MountPoint': '/Volumes/DAPLINK', + 'Size': 67174400, + 'VolumeName': 'DAPLINK'}], + 'VolumesFromDisks': ['Mac HD', 'DAPLINK'], + 'WholeDisks': ['disk0', 'disk1', 'disk2'] + } + usb_tree = [{ + 'IORegistryEntryName': 'DAPLink CMSIS-DAP', + 'USB Serial Number': '0240000034544e45003a00048e3800525a91000097969900', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'AppleUSBHostLegacyClient'}, + {'IORegistryEntryName': 'AppleUSBHostCompositeDevice'}, + {'IORegistryEntryName': 'USB_MSC', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOUSBMassStorageInterfaceNub', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOUSBMassStorageDriverNub', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOUSBMassStorageDriver', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOSCSILogicalUnitNub', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOSCSIPeripheralDeviceType00', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOBlockStorageServices', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOBlockStorageDriver', + 'IORegistryEntryChildren': [ + {'BSD Name': 'disk2', + 'IORegistryEntryName': 'MBED VFS Media', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOMediaBSDClient'}], + }], + }], + }], + }], + }], + }], + }], + }], + }, + {'IORegistryEntryName': 'CMSIS-DAP', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOUSBHostHIDDevice', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOHIDInterface'}, + {'IORegistryEntryName': 'IOHIDLibUserClient'}, + {'IORegistryEntryName': 'IOHIDLibUserClient'}], + }], + }, + {'IORegistryEntryName': 'mbed Serial Port', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + { 'IORegistryEntryName': 'AppleUSBACMControl'}], + }, + {'IORegistryEntryName': 'mbed Serial Port', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'AppleUSBACMData', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOModemSerialStreamSync', + 'IORegistryEntryChildren': [ + {'IODialinDevice': '/dev/tty.usbmodem1422', + 'IORegistryEntryName': 'IOSerialBSDClient'}], + }], + }], + }], + } + ] + + with patch("subprocess.Popen") as _popen: + def do_popen(command, *args, **kwargs): + to_ret = MagicMock() + to_ret.wait.return_value = 0 + to_ret.stdout = BytesIO() + plistlib.dump( + {'diskutil': disks, + 'ioreg': usb_tree}[command[0]], + to_ret.stdout) + to_ret.stdout.seek(0) + to_ret.communicate.return_value = (to_ret.stdout.getvalue(), "") + return to_ret + _popen.side_effect = do_popen + candidates = self.darwin.find_candidates() + self.assertIn({'mount_point': '/Volumes/DAPLINK', + 'serial_port': '/dev/tty.usbmodem1422', + 'target_id_usb_id': '0240000034544e45003a00048e3800525a91000097969900', + 'vendor_id': '0d28', + 'product_id': '0204'}, + candidates) diff --git a/tools/python/python_tests/mbed_lstools/os_linux_generic.py b/tools/python/python_tests/mbed_lstools/os_linux_generic.py new file mode 100644 index 0000000000..53c7fa4499 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/os_linux_generic.py @@ -0,0 +1,545 @@ +''' +mbed SDK +Copyright (c) 2011-2018 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 +import sys +import os +from mock import patch, mock_open +from mbed_lstools.linux import MbedLsToolsLinuxGeneric + + +class LinuxPortTestCase(unittest.TestCase): + ''' Basic test cases checking trivial asserts + ''' + + def setUp(self): + self.linux_generic = MbedLsToolsLinuxGeneric() + + def tearDown(self): + pass + + vfat_devices = [ + b'/dev/sdb on /media/usb0 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdd on /media/usb2 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sde on /media/usb3 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdc on /media/usb1 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)' + ] + + def test_get_mount_point_basic(self): + with patch('mbed_lstools.linux.MbedLsToolsLinuxGeneric._run_cli_process') as _cliproc: + _cliproc.return_value = (b'\n'.join(self.vfat_devices), None, 0) + mount_dict = dict(self.linux_generic._fat_mounts()) + _cliproc.assert_called_once_with('mount') + self.assertEqual('/media/usb0', mount_dict['/dev/sdb']) + self.assertEqual('/media/usb2', mount_dict['/dev/sdd']) + self.assertEqual('/media/usb3', mount_dict['/dev/sde']) + self.assertEqual('/media/usb1', mount_dict['/dev/sdc']) + + + vfat_devices_ext = [ + b'/dev/sdb on /media/MBED_xxx type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdd on /media/MBED___x type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sde on /media/MBED-xxx type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdc on /media/MBED_x-x type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + + b'/dev/sda on /mnt/NUCLEO type vfat (rw,relatime,uid=999,fmask=0133,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,utf8,flush,errors=remount-ro,uhelper=ldm)', + b'/dev/sdf on /mnt/NUCLEO_ type vfat (rw,relatime,uid=999,fmask=0133,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,utf8,flush,errors=remount-ro,uhelper=ldm)', + b'/dev/sdg on /mnt/DAPLINK type vfat (rw,relatime,sync,uid=999,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro,uhelper=ldm)', + b'/dev/sdh on /mnt/DAPLINK_ type vfat (rw,relatime,sync,uid=999,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro,uhelper=ldm)', + b'/dev/sdi on /mnt/DAPLINK__ type vfat (rw,relatime,sync,uid=999,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro,uhelper=ldm)', + ] + + def test_get_mount_point_ext(self): + with patch('mbed_lstools.linux.MbedLsToolsLinuxGeneric._run_cli_process') as _cliproc: + _cliproc.return_value = (b'\n'.join(self.vfat_devices_ext), None, 0) + mount_dict = dict(self.linux_generic._fat_mounts()) + _cliproc.assert_called_once_with('mount') + self.assertEqual('/media/MBED_xxx', mount_dict['/dev/sdb']) + self.assertEqual('/media/MBED___x', mount_dict['/dev/sdd']) + self.assertEqual('/media/MBED-xxx', mount_dict['/dev/sde']) + self.assertEqual('/media/MBED_x-x', mount_dict['/dev/sdc']) + + self.assertEqual('/mnt/NUCLEO', mount_dict['/dev/sda']) + self.assertEqual('/mnt/NUCLEO_', mount_dict['/dev/sdf']) + self.assertEqual('/mnt/DAPLINK', mount_dict['/dev/sdg']) + self.assertEqual('/mnt/DAPLINK_', mount_dict['/dev/sdh']) + self.assertEqual('/mnt/DAPLINK__', mount_dict['/dev/sdi']) + + def find_candidates_with_patch(self, mount_list, link_dict, listdir_dict, open_dict): + if not getattr(sys.modules['os'], 'readlink', None): + sys.modules['os'].readlink = None + + def do_open(path, mode='r'): + path = path.replace('\\', '/') + file_object = mock_open(read_data=open_dict[path]).return_value + file_object.__iter__.return_value = open_dict[path].splitlines(True) + return file_object + + with patch('mbed_lstools.linux.MbedLsToolsLinuxGeneric._run_cli_process') as _cliproc,\ + patch('os.readlink') as _readlink,\ + patch('os.listdir') as _listdir,\ + patch('os.path.abspath') as _abspath,\ + patch('mbed_os_tools.detect.linux.open', do_open) as _,\ + patch('os.path.isdir') as _isdir: + _isdir.return_value = True + _cliproc.return_value = (b'\n'.join(mount_list), None, 0) + def do_readlink(link): + # Fix for testing on Windows + link = link.replace('\\', '/') + return link_dict[link] + _readlink.side_effect = do_readlink + def do_listdir(dir): + # Fix for testing on Windows + dir = dir.replace('\\', '/') + return listdir_dict[dir] + _listdir.side_effect = do_listdir + def do_abspath(dir): + _, path = os.path.splitdrive( + os.path.normpath(os.path.join(os.getcwd(), dir))) + path = path.replace('\\', '/') + return path + _abspath.side_effect = do_abspath + ret_val = self.linux_generic.find_candidates() + _cliproc.assert_called_once_with('mount') + return ret_val + + + listdir_dict_rpi = { + '/dev/disk/by-id': [ + 'usb-MBED_VFS_0240000028634e4500135006691700105f21000097969900-0:0', + 'usb-MBED_VFS_0240000028884e450018700f6bf000338021000097969900-0:0', + 'usb-MBED_VFS_0240000028884e45001f700f6bf000118021000097969900-0:0', + 'usb-MBED_VFS_0240000028884e450036700f6bf000118021000097969900-0:0', + 'usb-MBED_VFS_0240000029164e45001b0012706e000df301000097969900-0:0', + 'usb-MBED_VFS_0240000029164e45002f0012706e0006f301000097969900-0:0', + 'usb-MBED_VFS_9900000031864e45000a100e0000003c0000000097969901-0:0' + ], + '/dev/serial/by-id': [ + 'usb-ARM_DAPLink_CMSIS-DAP_0240000028634e4500135006691700105f21000097969900-if01', + 'usb-ARM_DAPLink_CMSIS-DAP_0240000028884e450018700f6bf000338021000097969900-if01', + 'usb-ARM_DAPLink_CMSIS-DAP_0240000028884e450036700f6bf000118021000097969900-if01', + 'usb-ARM_DAPLink_CMSIS-DAP_0240000029164e45001b0012706e000df301000097969900-if01', + 'usb-ARM_BBC_micro:bit_CMSIS-DAP_9900000031864e45000a100e0000003c0000000097969901-if01' + ], + '/sys/class/block': [ + 'sdb', + 'sdc', + 'sdd', + 'sde', + 'sdf', + 'sdg', + 'sdh', + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-1/1-1.2/1-1.2.6': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-7': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-8': [ + 'idVendor', + 'idProduct' + ] + } + + open_dict_rpi = { + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-1/1-1.2/1-1.2.6/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-1/1-1.2/1-1.2.6/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-7/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-7/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-8/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-8/idProduct': '0204\n' + } + + link_dict_rpi = { + '/dev/disk/by-id/usb-MBED_VFS_0240000028634e4500135006691700105f21000097969900-0:0': '../../sdb', + '/dev/disk/by-id/usb-MBED_VFS_0240000028884e450018700f6bf000338021000097969900-0:0': '../../sdc', + '/dev/disk/by-id/usb-MBED_VFS_0240000028884e45001f700f6bf000118021000097969900-0:0': '../../sdd', + '/dev/disk/by-id/usb-MBED_VFS_0240000028884e450036700f6bf000118021000097969900-0:0': '../../sde', + '/dev/disk/by-id/usb-MBED_VFS_0240000029164e45001b0012706e000df301000097969900-0:0': '../../sdf', + '/dev/disk/by-id/usb-MBED_VFS_0240000029164e45002f0012706e0006f301000097969900-0:0': '../../sdg', + '/dev/disk/by-id/usb-MBED_VFS_9900000031864e45000a100e0000003c0000000097969901-0:0': '../../sdh', + '/dev/serial/by-id/usb-ARM_DAPLink_CMSIS-DAP_0240000028634e4500135006691700105f21000097969900-if01': '../../ttyACM0', + '/dev/serial/by-id/usb-ARM_DAPLink_CMSIS-DAP_0240000028884e450018700f6bf000338021000097969900-if01': '../../ttyACM1', + '/dev/serial/by-id/usb-ARM_DAPLink_CMSIS-DAP_0240000028884e450036700f6bf000118021000097969900-if01': '../../ttyACM3', + '/dev/serial/by-id/usb-ARM_DAPLink_CMSIS-DAP_0240000029164e45001b0012706e000df301000097969900-if01': '../../ttyACM2', + '/dev/serial/by-id/usb-ARM_BBC_micro:bit_CMSIS-DAP_9900000031864e45000a100e0000003c0000000097969901-if01': '../../ttyACM4', + '/sys/class/block/sdb': '../../devices/pci0000:00/0000:00:06.0/usb1/1-1/1-1.2/1-1.2.6/1-1.2.6:1.0/host8568/target8568:0:0/8568:0:0:0/block/sdb', + '/sys/class/block/sdc': '../../devices/pci0000:00/0000:00:06.0/usb1/1-3/1-3:1.0/host4/target4:0:0/4:0:0:0/block/sdc', + '/sys/class/block/sdd': '../../devices/pci0000:00/0000:00:06.0/usb1/1-4/1-4:1.0/host5/target5:0:0/5:0:0:0/block/sdd', + '/sys/class/block/sde': '../../devices/pci0000:00/0000:00:06.0/usb1/1-5/1-5:1.0/host6/target6:0:0/6:0:0:0/block/sde', + '/sys/class/block/sdf': '../../devices/pci0000:00/0000:00:06.0/usb1/1-6/1-6:1.0/host7/target7:0:0/7:0:0:0/block/sdf', + '/sys/class/block/sdg': '../../devices/pci0000:00/0000:00:06.0/usb1/1-7/1-7:1.0/host8/target8:0:0/8:0:0:0/block/sdg', + '/sys/class/block/sdh': '../../devices/pci0000:00/0000:00:06.0/usb1/1-8/1-7:1.0/host9/target9:0:0/9:0:0:0/block/sdh' + } + + mount_list_rpi = [ + b'/dev/sdb on /media/usb0 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdc on /media/usb1 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdd on /media/usb2 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sde on /media/usb3 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdf on /media/usb4 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdg on /media/usb5 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdh on /media/usb6 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)' + ] + def test_get_detected_rpi(self): + mbed_det = self.find_candidates_with_patch( + self.mount_list_rpi, self.link_dict_rpi, self.listdir_dict_rpi, self.open_dict_rpi) + + self.assertIn({ + 'mount_point': '/media/usb0', + 'serial_port': '/dev/ttyACM0', + 'target_id_usb_id': '0240000028634e4500135006691700105f21000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + self.assertIn({ + 'mount_point': '/media/usb1', + 'serial_port': '/dev/ttyACM1', + 'target_id_usb_id': '0240000028884e450018700f6bf000338021000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + self.assertIn({ + 'mount_point': '/media/usb4', + 'serial_port': '/dev/ttyACM2', + 'target_id_usb_id': '0240000029164e45001b0012706e000df301000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + self.assertIn({ + 'mount_point': '/media/usb3', + 'serial_port': '/dev/ttyACM3', + 'target_id_usb_id': '0240000028884e450036700f6bf000118021000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + self.assertIn({ + 'mount_point': '/media/usb6', + 'serial_port': '/dev/ttyACM4', + 'target_id_usb_id': '9900000031864e45000a100e0000003c0000000097969901', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + + + listdir_dict_1 = { + '/dev/disk/by-id': [ + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part1', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part2', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part5', + '/dev/disk/by-id/ata-TSSTcorpDVD-ROM_TS-H352C', + '/dev/disk/by-id/usb-MBED_FDi_sk_A000000001-0:0', + '/dev/disk/by-id/usb-MBED_microcontroller_0240020152986E5EAF6693E6-0:0', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part1', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part2', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part5', + ], + '/dev/serial/by-id': [ + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_0240020152986E5EAF6693E6-if01', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_A000000001-if01', + ], + '/sys/class/block': [ + 'sdb', + 'sdc' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3': [ + 'idVendor', + 'idProduct' + ] + } + + link_dict_1 = { + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM': '../../sda', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part1': '../../sda1', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part2': '../../sda2', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part5': '../../sda5', + '/dev/disk/by-id/ata-TSSTcorpDVD-ROM_TS-H352C': '../../sr0', + '/dev/disk/by-id/usb-MBED_FDi_sk_A000000001-0:0': '../../sdc', + '/dev/disk/by-id/usb-MBED_microcontroller_0240020152986E5EAF6693E6-0:0': '../../sdb', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77': '../../sda', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part1': '../../sda1', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part2': '../../sda2', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part5': '../../sda5', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_0240020152986E5EAF6693E6-if01': '../../ttyACM1', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_A000000001-if01': '../../ttyACM0', + '/sys/class/block/sdb': '../../devices/pci0000:00/0000:00:06.0/usb1/1-2/1-2:1.0/host3/target3:0:0/3:0:0:0/block/sdb', + '/sys/class/block/sdc': '../../devices/pci0000:00/0000:00:06.0/usb1/1-3/1-3:1.0/host4/target4:0:0/4:0:0:0/block/sdc' + } + + open_dict_1 = { + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idProduct': '0204\n' + } + + mount_list_1 = [ + b'/dev/sdb on /media/usb0 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdc on /media/usb1 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)' + ] + def test_get_detected_1_k64f(self): + mbed_det = self.find_candidates_with_patch( + self.mount_list_1, self.link_dict_1, self.listdir_dict_1, self.open_dict_1) + self.assertIn({ + 'mount_point': '/media/usb0', + 'serial_port': '/dev/ttyACM1', + 'target_id_usb_id': '0240020152986E5EAF6693E6', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + + self.assertIn({ + 'mount_point': '/media/usb1', + 'serial_port': '/dev/ttyACM0', + 'target_id_usb_id': 'A000000001', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + + + listdir_dict_2 = { + '/dev/disk/by-id': [ + 'ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM', + 'ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part1', + 'ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part2', + 'ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part5', + 'ata-TSSTcorpDVD-ROM_TS-H352C', + 'usb-MBED_FDi_sk_A000000001-0:0', + 'usb-MBED_microcontroller_02400201489A1E6CB564E3D4-0:0', + 'usb-MBED_microcontroller_0240020152986E5EAF6693E6-0:0', + 'usb-MBED_microcontroller_0240020152A06E54AF5E93EC-0:0', + 'usb-MBED_microcontroller_0672FF485649785087171742-0:0', + 'wwn-0x5000cca30ccffb77', + 'wwn-0x5000cca30ccffb77-part1', + 'wwn-0x5000cca30ccffb77-part2', + 'wwn-0x5000cca30ccffb77-part5' + ], + '/dev/serial/by-id': [ + 'usb-MBED_MBED_CMSIS-DAP_02400201489A1E6CB564E3D4-if01', + 'usb-MBED_MBED_CMSIS-DAP_0240020152986E5EAF6693E6-if01', + 'usb-MBED_MBED_CMSIS-DAP_0240020152A06E54AF5E93EC-if01', + 'usb-MBED_MBED_CMSIS-DAP_A000000001-if01', + 'usb-STMicroelectronics_STM32_STLink_0672FF485649785087171742-if02' + ], + '/sys/class/block': [ + 'sdb', + 'sdc', + 'sdd', + 'sde', + 'sdf' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6': [ + 'idVendor', + 'idProduct' + ] + } + + open_dict_2 = { + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6/idProduct': '0204\n' + } + + link_dict_2 = { + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM': '../../sda', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part1': '../../sda1', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part2': '../../sda2', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part5': '../../sda5', + '/dev/disk/by-id/ata-TSSTcorpDVD-ROM_TS-H352C': '../../sr0', + '/dev/disk/by-id/usb-MBED_FDi_sk_A000000001-0:0': '../../sdc', + '/dev/disk/by-id/usb-MBED_microcontroller_02400201489A1E6CB564E3D4-0:0': '../../sde', + '/dev/disk/by-id/usb-MBED_microcontroller_0240020152986E5EAF6693E6-0:0': '../../sdb', + '/dev/disk/by-id/usb-MBED_microcontroller_0240020152A06E54AF5E93EC-0:0': '../../sdf', + '/dev/disk/by-id/usb-MBED_microcontroller_0672FF485649785087171742-0:0': '../../sdd', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77': '../../sda', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part1': '../../sda1', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part2': '../../sda2', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part5': '../../sda5', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_02400201489A1E6CB564E3D4-if01': '../../ttyACM3', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_0240020152986E5EAF6693E6-if01': '../../ttyACM1', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_0240020152A06E54AF5E93EC-if01': '../../ttyACM4', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_A000000001-if01': '../../ttyACM0', + '/dev/serial/by-id/usb-STMicroelectronics_STM32_STLink_0672FF485649785087171742-if02': '../../ttyACM2', + '/sys/class/block/sdb': '../../devices/pci0000:00/0000:00:06.0/usb1/1-2/1-2:1.0/host3/target3:0:0/3:0:0:0/block/sdb', + '/sys/class/block/sdc': '../../devices/pci0000:00/0000:00:06.0/usb1/1-3/1-3:1.0/host4/target4:0:0/4:0:0:0/block/sdc', + '/sys/class/block/sdd': '../../devices/pci0000:00/0000:00:06.0/usb1/1-4/1-4:1.0/host5/target5:0:0/5:0:0:0/block/sdd', + '/sys/class/block/sde': '../../devices/pci0000:00/0000:00:06.0/usb1/1-5/1-5:1.0/host6/target6:0:0/6:0:0:0/block/sde', + '/sys/class/block/sdf': '../../devices/pci0000:00/0000:00:06.0/usb1/1-6/1-6:1.0/host7/target7:0:0/7:0:0:0/block/sdf' + } + + mount_list_2 = [ + b'/dev/sdb on /media/usb0 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdc on /media/usb1 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdd on /media/usb2 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sde on /media/usb3 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdf on /media/usb4 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)' + ] + def test_get_detected_2_k64f(self): + mbed_det = self.find_candidates_with_patch( + self.mount_list_2, self.link_dict_2, self.listdir_dict_2, self.open_dict_2) + + self.assertIn({ + 'mount_point': '/media/usb1', + 'serial_port': '/dev/ttyACM0', + 'target_id_usb_id': 'A000000001', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + self.assertIn({ + 'mount_point': '/media/usb2', + 'serial_port': '/dev/ttyACM2', + 'target_id_usb_id': '0672FF485649785087171742', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + + self.assertIn({ + 'mount_point': '/media/usb4', + 'serial_port': '/dev/ttyACM4', + 'target_id_usb_id': '0240020152A06E54AF5E93EC', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + + self.assertIn({ + 'mount_point': '/media/usb3', + 'serial_port': '/dev/ttyACM3', + 'target_id_usb_id': '02400201489A1E6CB564E3D4', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + + self.assertIn({ + 'mount_point': '/media/usb0', + 'serial_port': '/dev/ttyACM1', + 'target_id_usb_id': '0240020152986E5EAF6693E6', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + + + listdir_dict_4 = { + '/dev/disk/by-id': [ + 'ata-VMware_Virtual_SATA_CDRW_Drive_00000000000000000001', + 'ata-VMware_Virtual_SATA_CDRW_Drive_01000000000000000001', + 'usb-MBED_VFS_0240000033514e45001f500585d40014e981000097969900-0:0' + ], + '/dev/serial/by-id': [ + 'pci-ARM_DAPLink_CMSIS-DAP_0240000033514e45001f500585d40014e981000097969900-if01' + ], + '/sys/class/block': [ + 'sdb' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2': [ + 'idVendor', + 'idProduct' + ], + } + + open_dict_4 = { + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idProduct': '0204\n' + } + + link_dict_4 = { + '/dev/disk/by-id/ata-VMware_Virtual_SATA_CDRW_Drive_00000000000000000001': '../../sr0', + '/dev/disk/by-id/ata-VMware_Virtual_SATA_CDRW_Drive_01000000000000000001': '../../sr1', + '/dev/disk/by-id/usb-MBED_VFS_0240000033514e45001f500585d40014e981000097969900-0:0': '../../sdb', + '/dev/serial/by-id/pci-ARM_DAPLink_CMSIS-DAP_0240000033514e45001f500585d40014e981000097969900-if01': '../../ttyACM0', + '/sys/class/block/sdb': '../../devices/pci0000:00/0000:00:06.0/usb1/1-2/1-2:1.0/host3/target3:0:0/3:0:0:0/block/sdb' + } + + mount_list_4 = [ + b'/dev/sdb on /media/przemek/DAPLINK type vfat (rw,nosuid,nodev,relatime,uid=1000,gid=1000,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,showexec,utf8,flush,errors=remount-ro,uhelper=udisks2)' + ] + def test_get_detected_3_k64f(self): + mbed_det = self.find_candidates_with_patch( + self.mount_list_4, self.link_dict_4, self.listdir_dict_4, self.open_dict_4) + + self.assertIn({ + 'mount_point': '/media/przemek/DAPLINK', + 'serial_port': '/dev/ttyACM0', + 'target_id_usb_id': '0240000033514e45001f500585d40014e981000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_lstools/os_win7.py b/tools/python/python_tests/mbed_lstools/os_win7.py new file mode 100644 index 0000000000..f2224c8f6b --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/os_win7.py @@ -0,0 +1,230 @@ +# coding: utf-8 +""" +mbed SDK +Copyright (c) 2011-2018 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 +import sys +import os +from mock import MagicMock, patch, call + +# Mock the winreg and _winreg module for non-windows python +_winreg = MagicMock() +sys.modules['_winreg'] = _winreg +sys.modules['winreg'] = _winreg + +from mbed_lstools.windows import MbedLsToolsWin7, CompatibleIDsNotFoundException + +class Win7TestCase(unittest.TestCase): + """ Basic test cases checking trivial asserts + """ + + def setUp(self): + self.lstool = MbedLsToolsWin7() + import logging + logging.basicConfig() + root_logger = logging.getLogger("mbedls") + root_logger.setLevel(logging.DEBUG) + del logging + _winreg.HKEY_LOCAL_MACHINE = None + _winreg.OpenKey.reset_mock() + _winreg.OpenKey.side_effect = None + _winreg.EnumValue.reset_mock() + _winreg.EnumValue.side_effect = None + _winreg.EnumKey.reset_mock() + _winreg.EnumKey.side_effect = None + _winreg.QueryValue.reset_mock() + _winreg.QueryValue.side_effect = None + _winreg.QueryInfoKey.reset_mock() + _winreg.QueryInfoKey.side_effect = None + _winreg.CreateKey.reset_mock() + _winreg.CreateKeyEx.reset_mock() + _winreg.DeleteKey.reset_mock() + _winreg.DeleteKeyEx.reset_mock() + _winreg.DeleteValue.reset_mock() + _winreg.SetValue.reset_mock() + _winreg.SetValueEx.reset_mock() + _winreg.SaveKey.reset_mock() + + def test_os_supported(self): + pass + + def test_empty_reg(self): + value_dict = { + (None, 'SYSTEM\\MountedDevices'): [ + ('\\DosDevices\\F:', + u'_??_USBSTOR#Disk&Ven_MBED&Prod_VFS&Rev_0.1#9&215b8c47&0&0240000032044e4500257009997b00386781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}'.encode('utf-16le')), + ], + (None, 'SYSTEM\\CurrentControlSet\\Services\\volume\\Enum'): [], + (None, 'SYSTEM\\CurrentControlSet\\Services\\USBSTOR\\Enum'): [] + } + self.setUpRegistry(value_dict, {}) + candidates = self.lstool.find_candidates() + self.assertEqual(_winreg.OpenKey.mock_calls, [ + call(_winreg.HKEY_LOCAL_MACHINE, 'SYSTEM\\MountedDevices'), + call(_winreg.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum'), + call(_winreg.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Services\\USBSTOR\\Enum') + ]) + self.assertEqual(candidates, []) + + def assertNoRegMut(self): + """Assert that the registry was not mutated in this test""" + _winreg.CreateKey.assert_not_called() + _winreg.CreateKeyEx.assert_not_called() + _winreg.DeleteKey.assert_not_called() + _winreg.DeleteKeyEx.assert_not_called() + _winreg.DeleteValue.assert_not_called() + _winreg.SetValue.assert_not_called() + _winreg.SetValueEx.assert_not_called() + _winreg.SaveKey.assert_not_called() + + def setUpRegistry(self, value_dict, key_dict): + all_keys = set(value_dict.keys()) | set(key_dict.keys()) + def open_key_effect(key, subkey): + if ((key, subkey) in all_keys or key in all_keys): + return key, subkey + else: + raise OSError((key, subkey)) + _winreg.OpenKey.side_effect = open_key_effect + def enum_value(key, index): + try: + a, b = value_dict[key][index] + return a, b, None + except KeyError: + raise OSError + _winreg.EnumValue.side_effect = enum_value + def enum_key(key, index): + try: + return key_dict[key][index] + except KeyError: + raise OSError + _winreg.EnumKey.side_effect = enum_key + def query_value(key, subkey): + try: + return value_dict[(key, subkey)] + except KeyError: + raise OSError + _winreg.QueryValueEx.side_effect = query_value + def query_info_key(key): + return (len(key_dict.get(key, [])), + len(value_dict.get(key, []))) + _winreg.QueryInfoKey.side_effect = query_info_key + + def test_one_composite_dev(self): + value_dict = { + (None, 'SYSTEM\\MountedDevices'): [ + ('\\DosDevices\\C:', u'NOT A VALID MBED DRIVE'.encode('utf-16le')), + ('\\DosDevices\\F:', + u'_??_USBSTOR#Disk&Ven_MBED&Prod_VFS&Rev_0.1#9&215b8c47&0&0240000032044e4500257009997b00386781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}'.encode('utf-16le')) + ], + (None, 'SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum'): [ + ('0', 'USBSTOR\\Disk&Ven_MBED&Prod_VFS&Rev_0.1\\9&215b8c47&0&0240000032044e4500257009997b00386781000097969900&0') + ], + (None, 'SYSTEM\\CurrentControlSet\\Services\\USBSTOR\\Enum'): [ + ('0', 'USB\\VID_0D28&PID_0204&MI_00\\8&26b12a60&0&0000') + ], + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'): [], + (((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'), + '0240000032044e4500257009997b00386781000097969900'), + 'ParentIdPrefix'): ('8&26b12a60&0', None), + (((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'), + '0240000032044e4500257009997b00386781000097969900'), + 'CompatibleIDs'): ([u'USB\\DevClass_00&SubClass_00&Prot_00', u'USB\\DevClass_00&SubClass_00', u'USB\\DevClass_00', u'USB\\COMPOSITE'], 7), + (((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_00'), '8&26b12a60&0&0000'), 'CompatibleIDs'): ([u'USB\\Class_08&SubClass_06&Prot_50', u'USB\\Class_08&SubClass_06', u'USB\\Class_08'], 7), + (((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_01'), + '8&26b12a60&0&0001'), + 'CompatibleIDs'): ([u'USB\\CLASS_02&SUBCLASS_02&PROT_01', u'USB\\CLASS_02&SUBCLASS_02', u'USB\\CLASS_02'], 7), + ((((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_01'), + '8&26b12a60&0&0001'), + 'Device Parameters'), + 'PortName'): ('COM7', None) + } + key_dict = { + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'): + ['0240000032044e4500257009997b00386781000097969900'], + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_00'): [], + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_01'): [], + (((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_01'), + '8&26b12a60&0&0001'), + 'Device Parameters'): [] + } + self.setUpRegistry(value_dict, key_dict) + + with patch('mbed_lstools.windows.MbedLsToolsWin7._run_cli_process') as _cliproc: + _cliproc.return_value = ("", "", 0) + expected_info = { + 'mount_point': 'F:', + 'serial_port': 'COM7', + 'target_id_usb_id': u'0240000032044e4500257009997b00386781000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + } + + devices = self.lstool.find_candidates() + self.assertIn(expected_info, devices) + self.assertNoRegMut() + + def test_one_non_composite_dev(self): + value_dict = { + (None, 'SYSTEM\\MountedDevices'): [ + ('\\DosDevices\\C:', u'NOT A VALID MBED DRIVE'.encode('utf-16le')), + ('\\DosDevices\\F:', + u'_??_USBSTOR#Disk&Ven_MBED&Prod_VFS&Rev_0.1#0000000032044e4500257009997b00386781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}'.encode('utf-16le')) + ], + (None, 'SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum'): [ + ('0', 'USBSTOR\Disk&Ven_MBED&Prod_VFS&Rev_0.1\\0000000032044e4500257009997b00386781000097969900&0') + ], + (None, 'SYSTEM\\CurrentControlSet\\Services\\USBSTOR\\Enum'): [ + ('0', 'USB\\VID_0D28&PID_0204\\0000000032044e4500257009997b00386781000097969900') + ], + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'): [], + ((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204\\0000000032044e4500257009997b00386781000097969900'), + 'CompatibleIDs'): ([u'USB\\Class_08&SubClass_06&Prot_50', u'USB\\Class_08&SubClass_06', u'USB\\Class_08'], 7) + } + key_dict = { + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'): + ['0000000032044e4500257009997b00386781000097969900'], + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204\\0000000032044e4500257009997b00386781000097969900'): [] + } + self.setUpRegistry(value_dict, key_dict) + + with patch('mbed_lstools.windows.MbedLsToolsWin7._run_cli_process') as _cliproc: + _cliproc.return_value = ("", "", 0) + expected_info = { + 'mount_point': 'F:', + 'serial_port': None, + 'target_id_usb_id': u'0000000032044e4500257009997b00386781000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + } + + devices = self.lstool.find_candidates() + self.assertIn(expected_info, devices) + self.assertNoRegMut() + + def test_mount_point_ready(self): + with patch('mbed_lstools.windows.MbedLsToolsWin7._run_cli_process') as _cliproc: + _cliproc.return_value = ("dummy", "", 0) + self.assertTrue(self.lstool.mount_point_ready("dummy")) + + _cliproc.reset_mock() + + _cliproc.return_value = ("", "dummy", 1) + self.assertFalse(self.lstool.mount_point_ready("dummy")) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_lstools/platform_database.py b/tools/python/python_tests/mbed_lstools/platform_database.py new file mode 100644 index 0000000000..775a48bfc8 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/platform_database.py @@ -0,0 +1,316 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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 +import os +import errno +import logging +import tempfile +import json +from mock import patch, MagicMock, DEFAULT +from io import StringIO + +from mbed_lstools.platform_database import PlatformDatabase, DEFAULT_PLATFORM_DB,\ + LOCAL_PLATFORM_DATABASE + +try: + unicode +except NameError: + unicode = str + +class EmptyPlatformDatabaseTests(unittest.TestCase): + """ Basic test cases with an empty database + """ + + def setUp(self): + self.base_db_path = os.path.join(tempfile.mkdtemp(), 'base') + self.base_db = open(self.base_db_path, 'w+b') + self.base_db.write(b'{}') + self.base_db.seek(0) + self.pdb = PlatformDatabase([self.base_db_path]) + + def tearDown(self): + self.base_db.close() + + def test_broken_database_io(self): + """Verify that the platform database still works without a + working backing file + """ + with patch("mbed_os_tools.detect.platform_database.open") as _open: + _open.side_effect = IOError("Bogus") + self.pdb = PlatformDatabase([self.base_db_path]) + self.pdb.add("1234", "MYTARGET") + self.assertEqual(self.pdb.get("1234"), "MYTARGET") + + def test_broken_database_bad_json(self): + """Verify that the platform database still works without a + working backing file + """ + self.base_db.write(b'{}') + self.base_db.seek(0) + self.pdb = PlatformDatabase([self.base_db_path]) + self.pdb.add("1234", "MYTARGET") + self.assertEqual(self.pdb.get("1234"), "MYTARGET") + + def test_broken_database(self): + """Verify that the platform database correctly reset's its database + """ + with patch("mbed_os_tools.detect.platform_database.open") as _open,\ + patch("mbed_os_tools.detect.platform_database._older_than_me") as _older: + _older.return_value = False + stringio = MagicMock() + _open.side_effect = (IOError("Bogus"), stringio) + self.pdb = PlatformDatabase([LOCAL_PLATFORM_DATABASE]) + stringio.__enter__.return_value.write.assert_called_with( + unicode(json.dumps(DEFAULT_PLATFORM_DB))) + self.pdb.add("1234", "MYTARGET") + self.assertEqual(self.pdb.get("1234"), "MYTARGET") + + def test_extra_broken_database(self): + """Verify that the platform database falls back to the built in database + even when it can't write to disk + """ + with patch("mbed_os_tools.detect.platform_database.open") as _open: + _open.side_effect = IOError("Bogus") + self.pdb = PlatformDatabase([LOCAL_PLATFORM_DATABASE]) + self.pdb.add("1234", "MYTARGET") + self.assertEqual(self.pdb.get("1234"), "MYTARGET") + + def test_old_database(self): + """Verify that the platform database correctly updates's its database + """ + with patch("mbed_os_tools.detect.platform_database.open") as _open,\ + patch("mbed_os_tools.detect.platform_database.getmtime") as _getmtime: + file_mock = MagicMock() + file_mock.read.return_value = '' + _open.return_value.__enter__.return_value = file_mock + _getmtime.side_effect = (0, 1000000) + self.pdb = PlatformDatabase([LOCAL_PLATFORM_DATABASE]) + file_mock.write.assert_called_with( + unicode(json.dumps(DEFAULT_PLATFORM_DB))) + + def test_bogus_database(self): + """Basic empty database test + """ + self.assertEqual(list(self.pdb.items()), []) + self.assertEqual(list(self.pdb.all_ids()), []) + self.assertEqual(self.pdb.get('Also_Junk', None), None) + + def test_add(self): + """Test that what was added can later be queried + """ + self.assertEqual(self.pdb.get('4753', None), None) + self.pdb.add('4753', 'Test_Platform', permanent=False) + self.assertEqual(self.pdb.get('4753', None), 'Test_Platform') + + def test_remove(self): + """Test that once something is removed it no longer shows up when queried + """ + self.assertEqual(self.pdb.get('4753', None), None) + self.pdb.add('4753', 'Test_Platform', permanent=False) + self.assertEqual(self.pdb.get('4753', None), 'Test_Platform') + self.assertEqual(self.pdb.remove('4753', permanent=False), 'Test_Platform') + self.assertEqual(self.pdb.get('4753', None), None) + + def test_bogus_add(self): + """Test that add requires properly formatted platform ids + """ + self.assertEqual(self.pdb.get('NOTVALID', None), None) + with self.assertRaises(ValueError): + self.pdb.add('NOTVALID', 'Test_Platform', permanent=False) + + def test_bogus_remove(self): + """Test that removing a not present platform does nothing + """ + self.assertEqual(self.pdb.get('NOTVALID', None), None) + self.assertEqual(self.pdb.remove('NOTVALID', permanent=False), None) + + def test_simplify_verbose_data(self): + """Test that fetching a verbose entry without verbose data correctly + returns just the 'platform_name' + """ + platform_data = { + 'platform_name': 'VALID', + 'other_data': 'data' + } + self.pdb.add('1337', platform_data, permanent=False) + self.assertEqual(self.pdb.get('1337', verbose_data=True), platform_data) + self.assertEqual(self.pdb.get('1337'), platform_data['platform_name']) + +class OverriddenPlatformDatabaseTests(unittest.TestCase): + """ Test that for one database overriding another + """ + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.base_db_path = os.path.join(self.temp_dir, 'base') + self.base_db = open(self.base_db_path, 'w+b') + self.base_db.write(json.dumps(dict([('0123', 'Base_Platform')])). + encode('utf-8')) + self.base_db.seek(0) + self.overriding_db_path = os.path.join(self.temp_dir, 'overriding') + self.overriding_db = open(self.overriding_db_path, 'w+b') + self.overriding_db.write(b'{}') + self.overriding_db.seek(0) + self.pdb = PlatformDatabase([self.overriding_db_path, self.base_db_path], + primary_database=self.overriding_db_path) + self.base_db.seek(0) + self.overriding_db.seek(0) + + def tearDown(self): + self.base_db.close() + self.overriding_db.close() + + def assertBaseUnchanged(self): + """Assert that the base database has not changed + """ + self.base_db.seek(0) + self.assertEqual(self.base_db.read(), + json.dumps(dict([('0123', 'Base_Platform')])) + .encode('utf-8')) + + def assertOverrideUnchanged(self): + """Assert that the override database has not changed + """ + self.overriding_db.seek(0) + self.assertEqual(self.overriding_db.read(), b'{}') + + def test_basline(self): + """Sanity check that the base database does what we expect + """ + self.assertEqual(list(self.pdb.items()), [('0123', 'Base_Platform')]) + self.assertEqual(list(self.pdb.all_ids()), ['0123']) + + def test_add_non_override(self): + """Check that adding keys goes to the Override database + """ + self.pdb.add('1234', 'Another_Platform') + self.assertEqual(list(self.pdb.items()), [('1234', 'Another_Platform'), ('0123', 'Base_Platform')]) + self.assertEqual(set(self.pdb.all_ids()), set(['0123', '1234'])) + self.assertBaseUnchanged() + + def test_load_override(self): + """Check that adding a platform goes to the Override database and + you can no longer query for the base database definition and + that the override database was not written to disk + """ + self.overriding_db.write(json.dumps(dict([('0123', 'Overriding_Platform')])). + encode('utf-8')) + self.overriding_db.seek(0) + self.pdb = PlatformDatabase([self.overriding_db_path, self.base_db_path], + primary_database=self.overriding_db_path) + self.assertIn(('0123', 'Overriding_Platform'), list(self.pdb.items())) + self.assertEqual(set(self.pdb.all_ids()), set(['0123'])) + self.assertEqual(self.pdb.get('0123'), 'Overriding_Platform') + self.assertBaseUnchanged() + + def test_add_override_permanent(self): + """Check that adding a platform goes to the Override database and + you can no longer query for the base database definition and + that the override database was written to disk + """ + self.pdb.add('0123', 'Overriding_Platform', permanent=True) + self.assertIn(('0123', 'Overriding_Platform'), list(self.pdb.items())) + self.assertEqual(set(self.pdb.all_ids()), set(['0123'])) + self.assertEqual(self.pdb.get('0123'), 'Overriding_Platform') + self.overriding_db.seek(0) + self.assertEqual(self.overriding_db.read(), + json.dumps(dict([('daplink', dict([('0123', 'Overriding_Platform')]))])) + .encode('utf-8')) + self.assertBaseUnchanged() + + def test_remove_override(self): + """Check that removing a platform from the Override database allows you to query + the original base database definition and that + that the override database was not written to disk + """ + self.pdb.add('0123', 'Overriding_Platform') + self.assertIn(('0123', 'Overriding_Platform'), list(self.pdb.items())) + self.assertEqual(set(self.pdb.all_ids()), set(['0123'])) + self.assertEqual(self.pdb.get('0123'), 'Overriding_Platform') + self.assertEqual(self.pdb.remove('0123'), 'Overriding_Platform') + self.assertEqual(self.pdb.get('0123'), 'Base_Platform') + self.assertOverrideUnchanged() + self.assertBaseUnchanged() + + def test_remove_from_base(self): + """Check that removing a platform from the base database no longer allows you to query + the original base database definition and that that the base database + was not written to disk + """ + self.assertEqual(self.pdb.remove('0123'), 'Base_Platform') + self.assertEqual(self.pdb.get('0123'), None) + self.assertOverrideUnchanged() + self.assertBaseUnchanged() + + def test_remove_from_base_permanent(self): + """Check that removing a platform from the base database no longer allows you to query + the original base database definition and that that the base database + was not modified on disk + """ + self.assertEqual(self.pdb.remove('0123', permanent=True), 'Base_Platform') + self.assertEqual(self.pdb.get('0123'), None) + self.assertBaseUnchanged() + +class InternalLockingChecks(unittest.TestCase): + + def setUp(self): + self.mocked_lock = patch('mbed_os_tools.detect.platform_database.InterProcessLock', spec=True).start() + self.acquire = self.mocked_lock.return_value.acquire + self.release = self.mocked_lock.return_value.release + + self.base_db_path = os.path.join(tempfile.mkdtemp(), 'base') + self.base_db = open(self.base_db_path, 'w+b') + self.base_db.write(b'{}') + self.base_db.seek(0) + self.pdb = PlatformDatabase([self.base_db_path]) + self.addCleanup(patch.stopall) + + def tearDown(self): + self.base_db.close() + + def test_no_update(self): + """Test that no locks are used when no modifications are specified + """ + self.pdb.add('7155', 'Junk') + self.acquire.assert_not_called() + self.release.assert_not_called() + + def test_update(self): + """Test that locks are used when modifications are specified + """ + self.pdb.add('7155', 'Junk', permanent=True) + assert self.acquire.called, 'Lock acquire should have been called' + assert self.release.called + + def test_update_fail_acquire(self): + """Test that the backing file is not updated when lock acquisition fails + """ + self.acquire.return_value = False + self.pdb.add('7155', 'Junk', permanent=True) + assert self.acquire.called, 'Lock acquire should have been called' + self.base_db.seek(0) + self.assertEqual(self.base_db.read(), b'{}') + + def test_update_ambiguous(self): + """Test that the backing file is not updated when lock acquisition fails + """ + self.pdb._prim_db = None + self.pdb.add('7155', 'Junk', permanent=True) + self.acquire.assert_not_called() + self.release.assert_not_called() + self.assertEqual(self.base_db.read(), b'{}') diff --git a/tools/python/python_tests/mbed_lstools/platform_detection.py b/tools/python/python_tests/mbed_lstools/platform_detection.py new file mode 100644 index 0000000000..1039965fa6 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/platform_detection.py @@ -0,0 +1,213 @@ +""" +mbed SDK +Copyright (c) 2011-2018 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 +import os +import copy +from mock import patch, mock_open, DEFAULT + +from mbed_lstools.lstools_base import MbedLsToolsBase + +TEST_DATA_PATH = 'test_data' + +class DummyLsTools(MbedLsToolsBase): + return_value = [] + def find_candidates(self): + return self.return_value + +try: + basestring +except NameError: + # Python 3 + basestring = str + + +def get_case_insensitive_path(path, file_name): + for entry in os.listdir(path): + if entry.lower() == file_name.lower(): + return os.path.join(path, entry) + + raise Exception('No matching file for %s found in $s' % (file_name, path)) + + +class PlatformDetectionTestCase(unittest.TestCase): + """ Basic test cases checking trivial asserts + """ + + def setUp(self): + self.base = DummyLsTools() + + def tearDown(self): + pass + + def run_test(self, test_data_case, candidate_data, expected_data): + # Add necessary candidate data + candidate_data['mount_point'] = 'dummy_mount_point' + + # Find the test data in the test_data folder + test_script_path = os.path.dirname(os.path.abspath(__file__)) + test_data_path = os.path.join(test_script_path, TEST_DATA_PATH) + test_data_cases = os.listdir(test_data_path) + self.assertTrue(test_data_case in test_data_cases, 'Expected %s to be present in %s folder' % (test_data_case, test_data_path)) + test_data_case_path = os.path.join(test_data_path, test_data_case) + + # NOTE a limitation of this mocked test is that it only allows mocking of one directory level. + # This is enough at the moment because all firmwares seem to use a flat file structure. + # If this changes in the future, this mocking framework can be extended to support this. + + test_data_case_file_names = os.listdir(test_data_case_path) + mocked_open_file_paths = [os.path.join(candidate_data['mount_point'], file_name ) for file_name in test_data_case_file_names] + + # Setup all the mocks + self.base.return_value = [candidate_data] + + def do_open(path, mode='r'): + file_name = os.path.basename(path) + try: + with open(get_case_insensitive_path(test_data_case_path, file_name), 'r') as test_data_file: + test_data_file_data = test_data_file.read() + except OSError: + raise OSError("(mocked open) No such file or directory: '%s'" % (path)) + + file_object = mock_open(read_data=test_data_file_data).return_value + file_object.__iter__.return_value = test_data_file_data.splitlines(True) + return file_object + + with patch("mbed_lstools.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch('mbed_os_tools.detect.lstools_base.open', do_open) as _,\ + patch("mbed_os_tools.detect.lstools_base.listdir") as _listdir: + _mpr.return_value = True + _listdir.return_value = test_data_case_file_names + results = self.base.list_mbeds(read_details_txt=True) + + # There should only ever be one result + self.assertEqual(len(results), 1) + actual = results[0] + + expected_keys = list(expected_data.keys()) + actual_keys = list(actual.keys()) + differing_map = {} + for key in expected_keys: + actual_value = actual.get(key) + if actual_value != expected_data[key]: + differing_map[key] = (actual_value, expected_data[key]) + + + if differing_map: + differing_string = '' + for differing_key in sorted(list(differing_map.keys())): + actual, expected = differing_map[differing_key] + differing_string += ' "%s": "%s" (expected "%s")\n' % (differing_key, actual, expected) + + assert_string = 'Expected data mismatch:\n\n{\n%s}' % (differing_string) + self.assertTrue(False, assert_string) + + + + def test_efm32pg_stk3401_jlink(self): + self.run_test('efm32pg_stk3401_jlink', { + 'target_id_usb_id': u'000440074453', + 'vendor_id': '1366', + 'product_id': '1015' + }, { + 'platform_name': 'EFM32PG_STK3401', + 'device_type': 'jlink', + 'target_id': '2035022D000122D5D475113A', + 'target_id_usb_id': '000440074453', + 'target_id_mbed_htm': '2035022D000122D5D475113A' + }) + + def test_lpc1768(self): + self.run_test('lpc1768', { + 'target_id_usb_id': u'101000000000000000000002F7F20DF3', + 'vendor_id': '0d28', + 'product_id': '0204' + }, { + 'platform_name': 'LPC1768', + 'device_type': 'daplink', + 'target_id': '101000000000000000000002F7F20DF3d51e6be5ac41795761dc44148e3b7000', + 'target_id_usb_id': '101000000000000000000002F7F20DF3', + 'target_id_mbed_htm': '101000000000000000000002F7F20DF3d51e6be5ac41795761dc44148e3b7000' + }) + + def test_nucleo_f411re_stlink(self): + self.run_test('nucleo_f411re_stlink', { + 'target_id_usb_id': u'0671FF554856805087112815', + 'vendor_id': '0483', + 'product_id': '374b' + }, { + 'platform_name': 'NUCLEO_F411RE', + 'device_type': 'stlink', + 'target_id': '07400221076061193824F764', + 'target_id_usb_id': '0671FF554856805087112815', + 'target_id_mbed_htm': '07400221076061193824F764' + }) + + def test_nrf51_microbit(self): + self.run_test('nrf51_microbit', { + 'target_id_usb_id': u'9900007031324e45000f9019000000340000000097969901', + 'vendor_id': '0d28', + 'product_id': '0204' + }, { + 'platform_name': 'NRF51_MICROBIT', + 'device_type': 'daplink', + 'target_id': '9900007031324e45000f9019000000340000000097969901', + 'target_id_usb_id': '9900007031324e45000f9019000000340000000097969901' + }) + + def test_k64f_daplink(self): + self.run_test('k64f_daplink', { + 'target_id_usb_id': u'0240000032044e45000a700a997b00356781000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, { + 'platform_name': 'K64F', + 'device_type': 'daplink', + 'target_id': '0240000032044e45000a700a997b00356781000097969900', + 'target_id_usb_id': '0240000032044e45000a700a997b00356781000097969900', + 'target_id_mbed_htm': '0240000032044e45000a700a997b00356781000097969900' + }) + + def test_nrf52_dk_daplink(self): + self.run_test('nrf52_dk_daplink', { + 'target_id_usb_id': u'110100004420312043574641323032203233303397969903', + 'vendor_id': '0d28', + 'product_id': '0204' + }, { + 'platform_name': 'NRF52_DK', + 'device_type': 'daplink', + 'target_id': '110100004420312043574641323032203233303397969903', + 'target_id_usb_id': '110100004420312043574641323032203233303397969903', + 'target_id_mbed_htm': '110100004420312043574641323032203233303397969903' + }) + + def test_nrf52_dk_jlink(self): + self.run_test('nrf52_dk_jlink', { + 'target_id_usb_id': u'000682546728', + 'vendor_id': '1366', + 'product_id': '1015' + }, { + 'platform_name': 'NRF52_DK', + 'device_type': 'jlink', + 'target_id': '000682546728', + 'target_id_usb_id': '000682546728' + }) + + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/MBED.HTM b/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/MBED.HTM new file mode 100644 index 0000000000..ffbc11f554 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/MBED.HTM @@ -0,0 +1,10 @@ + + + + + +mbed Website Shortcut + + + + diff --git a/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/README.TXT b/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/README.TXT new file mode 100644 index 0000000000..56446f38e9 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/README.TXT @@ -0,0 +1,15 @@ +Silicon Labs J-Link MSD volume. + +Copying a binary file here will flash it to the MCU. + +Supported binary file formats: + - Intel Hex (.hex) + - Motorola s-records (.mot) + - Binary (.bin/.par/.crd) + +Known limitations: + - Virtual COM port speed is currently fixed at 115200 bps + - Using other kit functionality such as energy profiling or debugger while flashing is + not supported. + Workaround: Pause any energy profiling and disconnect any connected debug sessions + before flashing. diff --git a/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/sl_kit.html b/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/sl_kit.html new file mode 100644 index 0000000000..b3ed4e0855 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/sl_kit.html @@ -0,0 +1,8 @@ + + + + EFM32PG-STK3401 - Pearl Gecko STK + + + + diff --git a/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/sl_qsg.html b/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/sl_qsg.html new file mode 100644 index 0000000000..a4b36c3067 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/efm32pg_stk3401_jlink/sl_qsg.html @@ -0,0 +1 @@ +Simplicity Studio Shortcut diff --git a/tools/python/python_tests/mbed_lstools/test_data/k64f_daplink/DETAILS.TXT b/tools/python/python_tests/mbed_lstools/test_data/k64f_daplink/DETAILS.TXT new file mode 100644 index 0000000000..bf3cc8e799 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/k64f_daplink/DETAILS.TXT @@ -0,0 +1,13 @@ +# DAPLink Firmware - see https://mbed.com/daplink +Unique ID: 0240000032044e45000a700a997b00356781000097969900 +HIC ID: 97969900 +Auto Reset: 0 +Automation allowed: 0 +Overflow detection: 0 +Daplink Mode: Interface +Interface Version: 0244 +Git SHA: 363abb00ee17ad50cb407c6d2e299407332e3c85 +Local Mods: 1 +USB Interfaces: MSD, CDC, HID +Interface CRC: 0xc44e96e1 +Remount count: 0 diff --git a/tools/python/python_tests/mbed_lstools/test_data/k64f_daplink/MBED.HTM b/tools/python/python_tests/mbed_lstools/test_data/k64f_daplink/MBED.HTM new file mode 100644 index 0000000000..b7c021ec87 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/k64f_daplink/MBED.HTM @@ -0,0 +1,13 @@ + + + + + +mbed Website Shortcut + + + + + diff --git a/tools/python/python_tests/mbed_lstools/test_data/lpc1768/MBED.HTM b/tools/python/python_tests/mbed_lstools/test_data/lpc1768/MBED.HTM new file mode 100644 index 0000000000..8b5bc0bd73 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/lpc1768/MBED.HTM @@ -0,0 +1,9 @@ + + + + +mbed Website Shortcut + + + + diff --git a/tools/python/python_tests/mbed_lstools/test_data/lpc1768/basic.bin b/tools/python/python_tests/mbed_lstools/test_data/lpc1768/basic.bin new file mode 100644 index 0000000000..a5f3fc283b Binary files /dev/null and b/tools/python/python_tests/mbed_lstools/test_data/lpc1768/basic.bin differ diff --git a/tools/python/python_tests/mbed_lstools/test_data/lpc1768/dirs.bin b/tools/python/python_tests/mbed_lstools/test_data/lpc1768/dirs.bin new file mode 100644 index 0000000000..508973922e Binary files /dev/null and b/tools/python/python_tests/mbed_lstools/test_data/lpc1768/dirs.bin differ diff --git a/tools/python/python_tests/mbed_lstools/test_data/nrf51_microbit/DETAILS.TXT b/tools/python/python_tests/mbed_lstools/test_data/nrf51_microbit/DETAILS.TXT new file mode 100644 index 0000000000..ffcd336220 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/nrf51_microbit/DETAILS.TXT @@ -0,0 +1,13 @@ +# DAPLink Firmware - see https://mbed.com/daplink +Unique ID: 9900007031324e45000f9019000000340000000097969901 +HIC ID: 97969901 +Auto Reset: 1 +Automation allowed: 0 +Overflow detection: 0 +Daplink Mode: Interface +Interface Version: 0244 +Git SHA: 72ea244a3a9219299c5655c614c955185451e98e +Local Mods: 1 +USB Interfaces: MSD, CDC, HID, WebUSB +Interface CRC: 0xfdd0c098 +Remount count: 0 diff --git a/tools/python/python_tests/mbed_lstools/test_data/nrf51_microbit/MICROBIT.HTM b/tools/python/python_tests/mbed_lstools/test_data/nrf51_microbit/MICROBIT.HTM new file mode 100644 index 0000000000..28e5e08c86 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/nrf51_microbit/MICROBIT.HTM @@ -0,0 +1,13 @@ + + + + + +mbed Website Shortcut + + + + + diff --git a/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_daplink/DETAILS.TXT b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_daplink/DETAILS.TXT new file mode 100644 index 0000000000..de7ce14dc5 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_daplink/DETAILS.TXT @@ -0,0 +1,13 @@ +# DAPLink Firmware - see https://mbed.com/daplink +Unique ID: 110100004420312043574641323032203233303397969903 +HIC ID: 97969903 +Auto Reset: 0 +Automation allowed: 0 +Overflow detection: 0 +Daplink Mode: Interface +Interface Version: 0245 +Git SHA: aace60cd16996cc881567f35eb84db063a9268b7 +Local Mods: 0 +USB Interfaces: MSD, CDC, HID +Interface CRC: 0xfa307a74 +Remount count: 0 diff --git a/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_daplink/MBED.HTM b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_daplink/MBED.HTM new file mode 100644 index 0000000000..f5754d0312 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_daplink/MBED.HTM @@ -0,0 +1,13 @@ + + + + + +mbed Website Shortcut + + + + + diff --git a/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/MBED.HTM b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/MBED.HTM new file mode 100644 index 0000000000..83d5285e1b --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/MBED.HTM @@ -0,0 +1 @@ +mbed Website Shortcut \ No newline at end of file diff --git a/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/README.TXT b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/README.TXT new file mode 100644 index 0000000000..ef2ec13f17 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/README.TXT @@ -0,0 +1,2 @@ +SEGGER J-Link MSD volume. +This volume is used to update the flash content of your target device. \ No newline at end of file diff --git a/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/Segger.html b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/Segger.html new file mode 100644 index 0000000000..824b1378b0 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/Segger.html @@ -0,0 +1 @@ +SEGGER Shortcut \ No newline at end of file diff --git a/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/User Guide.html b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/User Guide.html new file mode 100644 index 0000000000..42d1f4821d --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/nrf52_dk_jlink/User Guide.html @@ -0,0 +1 @@ +Starter Guide Shortcut \ No newline at end of file diff --git a/tools/python/python_tests/mbed_lstools/test_data/nucleo_f411re_stlink/DETAILS.TXT b/tools/python/python_tests/mbed_lstools/test_data/nucleo_f411re_stlink/DETAILS.TXT new file mode 100644 index 0000000000..b1cb7ce8aa --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/nucleo_f411re_stlink/DETAILS.TXT @@ -0,0 +1,2 @@ +Version: 0221 +Build: Jan 11 2016 16:12:36 diff --git a/tools/python/python_tests/mbed_lstools/test_data/nucleo_f411re_stlink/MBED.HTM b/tools/python/python_tests/mbed_lstools/test_data/nucleo_f411re_stlink/MBED.HTM new file mode 100644 index 0000000000..cbcf288551 --- /dev/null +++ b/tools/python/python_tests/mbed_lstools/test_data/nucleo_f411re_stlink/MBED.HTM @@ -0,0 +1,10 @@ + + + + +mbed Website Shortcut + + + + + \ No newline at end of file diff --git a/tools/python/python_tests/mbed_os_tools/__init__.py b/tools/python/python_tests/mbed_os_tools/__init__.py new file mode 100644 index 0000000000..3004b70a6e --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. diff --git a/tools/python/python_tests/mbed_os_tools/detect/__init__.py b/tools/python/python_tests/mbed_os_tools/detect/__init__.py new file mode 100644 index 0000000000..100e865c3e --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +"""! @package mbed-ls-test + +Unit tests for mbed-ls package + +""" diff --git a/tools/python/python_tests/mbed_os_tools/detect/details_txt.py b/tools/python/python_tests/mbed_os_tools/detect/details_txt.py new file mode 100644 index 0000000000..092eba5436 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/details_txt.py @@ -0,0 +1,109 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +import os +import errno +import logging + +from mbed_os_tools.detect.main import create + + + +class ParseMbedHTMTestCase(unittest.TestCase): + """ Unit tests checking HTML parsing code for 'mbed.htm' files + """ + + details_txt_0226 = """Version: 0226 +Build: Aug 24 2015 17:06:30 +Git Commit SHA: 27a236b9fe39c674a703c5c89655fbd26b8e27e1 +Git Local mods: Yes +""" + + details_txt_0240 = """# DAPLink Firmware - see https://mbed.com/daplink +Unique ID: 0240000029164e45002f0012706e0006f301000097969900 +HIF ID: 97969900 +Auto Reset: 0 +Automation allowed: 0 +Daplink Mode: Interface +Interface Version: 0240 +Git SHA: c765cbb590f57598756683254ca38b211693ae5e +Local Mods: 0 +USB Interfaces: MSD, CDC, HID +Interface CRC: 0x26764ebf +""" + + def setUp(self): + self.mbeds = create() + + def tearDown(self): + pass + + def test_simplified_daplink_txt_content(self): + # Fetch lines from DETAILS.TXT + lines = self.details_txt_0226.splitlines() + self.assertEqual(4, len(lines)) + + # Check parsing content + result = self.mbeds._parse_details(lines) + self.assertEqual(4, len(result)) + self.assertIn('Version', result) + self.assertIn('Build', result) + self.assertIn('Git Commit SHA', result) + self.assertIn('Git Local mods', result) + + # Check for daplink_version + self.assertEqual(result['Version'], "0226") + + def test_extended_daplink_txt_content(self): + # Fetch lines from DETAILS.TXT + lines = self.details_txt_0240.splitlines() + self.assertEqual(11, len(lines)) + + # Check parsing content + result = self.mbeds._parse_details(lines) + self.assertEqual(11, len(result)) # 12th would be comment + self.assertIn('Unique ID', result) + self.assertIn('HIF ID', result) + self.assertIn('Auto Reset', result) + self.assertIn('Automation allowed', result) + self.assertIn('Daplink Mode', result) + self.assertIn('Interface Version', result) + self.assertIn('Git SHA', result) + self.assertIn('Local Mods', result) + self.assertIn('USB Interfaces', result) + self.assertIn('Interface CRC', result) + + # Check if we parsed comment line: + # "# DAPLink Firmware - see https://mbed.com/daplink" + for key in result: + # Check if we parsed comment + self.assertFalse(key.startswith('#')) + # Check if we parsed + self.assertFalse('https://mbed.com/daplink' in result[key]) + + # Check for daplink_version + # DAPlink <240 compatibility + self.assertEqual(result['Interface Version'], "0240") + self.assertEqual(result['Version'], "0240") + + def test_(self): + pass + + def test_(self): + pass + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_os_tools/detect/detect_os.py b/tools/python/python_tests/mbed_os_tools/detect/detect_os.py new file mode 100644 index 0000000000..2317f3bc08 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/detect_os.py @@ -0,0 +1,62 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +import os +import sys +import errno +import logging + +import platform +from mbed_os_tools.detect.main import create +from mbed_os_tools.detect.main import mbed_os_support +from mbed_os_tools.detect.main import mbed_lstools_os_info + + +class DetectOSTestCase(unittest.TestCase): + """ Test cases for host OS related functionality. Helpful during porting + """ + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_porting_mbed_lstools_os_info(self): + self.assertNotEqual(None, mbed_lstools_os_info()) + + def test_porting_mbed_os_support(self): + self.assertNotEqual(None, mbed_os_support()) + + def test_porting_create(self): + self.assertNotEqual(None, create()) + + def test_supported_os_name(self): + os_names = ['Windows7', 'Ubuntu', 'LinuxGeneric', 'Darwin'] + self.assertIn(mbed_os_support(), os_names) + + def test_detect_os_support_ext(self): + os_info = (os.name, + platform.system(), + platform.release(), + platform.version(), + sys.platform) + + self.assertEqual(os_info, mbed_lstools_os_info()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_os_tools/detect/mbed_htm.py b/tools/python/python_tests/mbed_os_tools/detect/mbed_htm.py new file mode 100644 index 0000000000..4cdd2015be --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/mbed_htm.py @@ -0,0 +1,88 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +import os +import errno +import logging + +from mbed_os_tools.detect.main import create + + + + +class ParseMbedHTMTestCase(unittest.TestCase): + """ Unit tests checking HTML parsing code for 'mbed.htm' files + """ + + # DAPlink <0240 + test_mbed_htm_k64f_url_str = '' + test_mbed_htm_lpc1768_url_str = '' + test_mbed_htm_nrf51_url_str = '' + + # DAPLink 0240 + test_daplink_240_mbed_html_str = 'window.location.replace("https://mbed.org/device/?code=0240000029164e45002f0012706e0006f301000097969900?version=0240?target_id=0007ffffffffffff4e45315450090023");' + + def setUp(self): + self.mbeds = create() + + def tearDown(self): + pass + + def test_mbed_htm_k64f_url(self): + target_id = self.mbeds._target_id_from_htm(self.test_mbed_htm_k64f_url_str) + self.assertEqual('02400203D94B0E7724B7F3CF', target_id) + + def test_mbed_htm_lpc1768_url(self): + target_id = self.mbeds._target_id_from_htm(self.test_mbed_htm_lpc1768_url_str) + self.assertEqual('101000000000000000000002F7F1869557200730298d254d3ff3509e3fe4722d', target_id) + + def test_daplink_240_mbed_html(self): + target_id = self.mbeds._target_id_from_htm(self.test_daplink_240_mbed_html_str) + self.assertEqual('0240000029164e45002f0012706e0006f301000097969900', target_id) + + def test_mbed_htm_nrf51_url(self): + target_id = self.mbeds._target_id_from_htm(self.test_mbed_htm_nrf51_url_str) + self.assertEqual('1100021952333120353935373130313232323032AFD5DFD8', target_id) + + def get_mbed_htm_comment_section_ver_build(self): + # Incorrect data + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNone(ver_bld) + + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNone(ver_bld) + + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNone(ver_bld) + + # Correct data + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNotNone(ver_bld) + self.assertEqual(('0200', 'Mar 26 2014 13:22:20'), ver_bld) + + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNotNone(ver_bld) + self.assertEqual(('0219', 'Feb 2 2016 15:20:54'), ver_bld) + + ver_bld = self.mbeds._mbed_htm_comment_section_ver_build('') + self.assertIsNotNone(ver_bld) + self.assertEqual(('0.14.3', '471'), ver_bld) + + def test_(self): + pass + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_os_tools/detect/mbedls_toolsbase.py b/tools/python/python_tests/mbed_os_tools/detect/mbedls_toolsbase.py new file mode 100644 index 0000000000..a4221c2fd6 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/mbedls_toolsbase.py @@ -0,0 +1,575 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +import os +import errno +import logging +import re +import json +from io import StringIO +from mock import patch, mock_open, DEFAULT +from copy import deepcopy + +from mbed_os_tools.detect.lstools_base import MbedLsToolsBase, FSInteraction + +class DummyLsTools(MbedLsToolsBase): + return_value = [] + def find_candidates(self): + return self.return_value + +try: + basestring +except NameError: + # Python 3 + basestring = str + +class BasicTestCase(unittest.TestCase): + """ Basic test cases checking trivial asserts + """ + + def setUp(self): + self.base = DummyLsTools(force_mock=True) + + def tearDown(self): + pass + + def test_list_mbeds_valid_platform(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}, + {'mount_point': None, + 'target_id_usb_id': '00000000000', + 'serial_port': 'not_valid'}] + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase._read_htm_ids") as _read_htm,\ + patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch("mbed_os_tools.detect.lstools_base.PlatformDatabase.get") as _get,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir: + _mpr.return_value = True + _read_htm.return_value = (u'0241BEEFDEAD', {}) + _get.return_value = { + 'platform_name': 'foo_target' + } + _listdir.return_value = ['mbed.htm'] + to_check = self.base.list_mbeds() + _read_htm.assert_called_once_with('dummy_mount_point') + _get.assert_any_call('0241', device_type='daplink', verbose_data=True) + self.assertEqual(len(to_check), 1) + self.assertEqual(to_check[0]['target_id'], "0241BEEFDEAD") + self.assertEqual(to_check[0]['platform_name'], 'foo_target') + + def test_list_mbeds_invalid_tid(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}, + {'mount_point': 'dummy_mount_point', + 'target_id_usb_id': "", + 'serial_port': 'not_valid'}] + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase._read_htm_ids") as _read_htm,\ + patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch("mbed_os_tools.detect.lstools_base.PlatformDatabase.get") as _get,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir: + _mpr.return_value = True + _read_htm.side_effect = [(u'0241BEEFDEAD', {}), (None, {})] + _get.return_value = { + 'platform_name': 'foo_target' + } + _listdir.return_value = ['mbed.htm'] + to_check = self.base.list_mbeds() + _get.assert_any_call('0241', device_type='daplink', verbose_data=True) + self.assertEqual(len(to_check), 2) + self.assertEqual(to_check[0]['target_id'], "0241BEEFDEAD") + self.assertEqual(to_check[0]['platform_name'], 'foo_target') + self.assertEqual(to_check[1]['target_id'], "") + self.assertEqual(to_check[1]['platform_name'], None) + + def test_list_mbeds_invalid_platform(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'not_in_target_db', + 'serial_port': "dummy_serial_port"}] + for qos in [FSInteraction.BeforeFilter, FSInteraction.AfterFilter]: + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase._read_htm_ids") as _read_htm,\ + patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch("mbed_os_tools.detect.lstools_base.PlatformDatabase.get") as _get,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir: + _mpr.return_value = True + _read_htm.return_value = (u'not_in_target_db', {}) + _get.return_value = None + _listdir.return_value = ['MBED.HTM'] + to_check = self.base.list_mbeds() + _read_htm.assert_called_once_with('dummy_mount_point') + _get.assert_any_call('not_', device_type='daplink', verbose_data=True) + self.assertEqual(len(to_check), 1) + self.assertEqual(to_check[0]['target_id'], "not_in_target_db") + self.assertEqual(to_check[0]['platform_name'], None) + + def test_list_mbeds_unmount_mid_read(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}] + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir: + _mpr.return_value = True + _listdir.side_effect = OSError + to_check = self.base.list_mbeds() + self.assertEqual(len(to_check), 0) + + def test_list_mbeds_read_mbed_htm_failure(self): + def _test(mock): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}] + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir,\ + patch('mbed_os_tools.detect.lstools_base.open', mock, create=True): + _mpr.return_value = True + _listdir.return_value = ['MBED.HTM', 'DETAILS.TXT'] + to_check = self.base.list_mbeds() + mock.assert_called_once_with(os.path.join('dummy_mount_point', 'mbed.htm'), 'r') + self.assertEqual(len(to_check), 0) + + m = mock_open() + m.side_effect = OSError + _test(m) + + m.reset_mock() + m.side_effect = IOError + _test(m) + + def test_list_mbeds_read_no_mbed_htm(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}] + + details_txt_contents = '''\ +# DAPLink Firmware - see https://mbed.com/daplink +Unique ID: 0240000032044e4500257009997b00386781000097969900 +HIC ID: 97969900 +Auto Reset: 0 +Automation allowed: 1 +Overflow detection: 1 +Daplink Mode: Interface +Interface Version: 0246 +Bootloader Version: 0244 +Git SHA: 0beabef8aa4b382809d79e98321ecf6a28936812 +Local Mods: 0 +USB Interfaces: MSD, CDC, HID +Bootloader CRC: 0xb92403e6 +Interface CRC: 0x434eddd1 +Remount count: 0 +''' + def _handle_open(*args, **kwargs): + if args[0].lower() == os.path.join('dummy_mount_point', 'mbed.htm'): + raise OSError("(mocked open) No such file or directory: 'mbed.htm'") + else: + return DEFAULT + + m = mock_open(read_data=details_txt_contents) + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir,\ + patch('mbed_os_tools.detect.lstools_base.open', m, create=True) as mocked_open: + mocked_open.side_effect = _handle_open + _mpr.return_value = True + _listdir.return_value = ['PRODINFO.HTM', 'DETAILS.TXT'] + to_check = self.base.list_mbeds() + self.assertEqual(len(to_check), 1) + m.assert_called_once_with(os.path.join('dummy_mount_point', 'DETAILS.TXT'), 'r') + self.assertEqual(to_check[0]['target_id'], '0240000032044e4500257009997b00386781000097969900') + + def test_list_mbeds_read_details_txt_failure(self): + def _test(mock): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}] + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir,\ + patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase._update_device_from_htm") as _htm,\ + patch('mbed_os_tools.detect.lstools_base.open', mock, create=True): + _mpr.return_value = True + _htm.side_effect = None + _listdir.return_value = ['MBED.HTM', 'DETAILS.TXT'] + to_check = self.base.list_mbeds(read_details_txt=True) + mock.assert_called_once_with(os.path.join('dummy_mount_point', 'DETAILS.TXT'), 'r') + self.assertEqual(len(to_check), 0) + + m = mock_open() + m.side_effect = OSError + _test(m) + + m.reset_mock() + m.side_effect = IOError + _test(m) + + def test_list_mbeds_unmount_mid_read_list_unmounted(self): + self.base.list_unmounted = True + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port"}] + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir: + _mpr.return_value = True + _listdir.side_effect = OSError + to_check = self.base.list_mbeds() + self.assertEqual(len(to_check), 1) + self.assertEqual(to_check[0]['mount_point'], None) + self.assertEqual(to_check[0]['device_type'], 'unknown') + self.assertEqual(to_check[0]['platform_name'], 'K64F') + + def test_mock_manufacture_ids_default_multiple(self): + # oper='+' + for mid, platform_name in [('0341', 'TEST_PLATFORM_NAME_1'), + ('0342', 'TEST_PLATFORM_NAME_2'), + ('0343', 'TEST_PLATFORM_NAME_3')]: + self.base.mock_manufacture_id(mid, platform_name) + self.assertEqual(platform_name, self.base.plat_db.get(mid)) + + def test_mock_manufacture_ids_minus(self): + # oper='+' + for mid, platform_name in [('0341', 'TEST_PLATFORM_NAME_1'), + ('0342', 'TEST_PLATFORM_NAME_2'), + ('0343', 'TEST_PLATFORM_NAME_3')]: + self.base.mock_manufacture_id(mid, platform_name) + self.assertEqual(platform_name, self.base.plat_db.get(mid)) + + # oper='-' + mock_ids = self.base.mock_manufacture_id('0342', '', oper='-') + self.assertEqual('TEST_PLATFORM_NAME_1', self.base.plat_db.get("0341")) + self.assertEqual(None, self.base.plat_db.get("0342")) + self.assertEqual('TEST_PLATFORM_NAME_3', self.base.plat_db.get("0343")) + + def test_mock_manufacture_ids_star(self): + # oper='+' + for mid, platform_name in [('0341', 'TEST_PLATFORM_NAME_1'), + ('0342', 'TEST_PLATFORM_NAME_2'), + ('0343', 'TEST_PLATFORM_NAME_3')]: + self.base.mock_manufacture_id(mid, platform_name) + + self.assertEqual(platform_name, self.base.plat_db.get(mid)) + + # oper='-' + self.base.mock_manufacture_id('*', '', oper='-') + self.assertEqual(None, self.base.plat_db.get("0341")) + self.assertEqual(None, self.base.plat_db.get("0342")) + self.assertEqual(None, self.base.plat_db.get("0343")) + + + def test_update_device_from_fs_mid_unmount(self): + dummy_mount = 'dummy_mount' + device = { + 'mount_point': dummy_mount + } + + with patch('os.listdir') as _listdir: + _listdir.side_effect = OSError + self.base._update_device_from_fs(device, False) + self.assertEqual(device['mount_point'], None) + + def test_detect_device_test(self): + device_type = self.base._detect_device_type({ + 'vendor_id': '0483' + }) + self.assertEqual(device_type, 'stlink') + + device_type = self.base._detect_device_type({ + 'vendor_id': '0d28' + }) + self.assertEqual(device_type, 'daplink') + + device_type = self.base._detect_device_type({ + 'vendor_id': '1366' + }) + self.assertEqual(device_type, 'jlink') + + def test_device_type_unmounted(self): + self.base.list_unmounted = True + self.base.return_value = [{'mount_point': None, + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': "dummy_serial_port", + 'vendor_id': '0d28', + 'product_id': '0204'}] + with patch("mbed_os_tools.detect.lstools_base.PlatformDatabase.get") as _get,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir: + _get.return_value = { + 'platform_name': 'foo_target' + } + to_check = self.base.list_mbeds() + #_get.assert_any_call('0240', device_type='daplink', verbose_data=True) + self.assertEqual(len(to_check), 1) + self.assertEqual(to_check[0]['target_id'], "0240DEADBEEF") + self.assertEqual(to_check[0]['platform_name'], 'foo_target') + self.assertEqual(to_check[0]['device_type'], 'daplink') + + def test_update_device_details_jlink(self): + jlink_html_contents = ('' + 'NXP Product Page') + _open = mock_open(read_data=jlink_html_contents) + dummy_mount_point = 'dummy' + base_device = { + 'mount_point': dummy_mount_point + } + + with patch('mbed_os_tools.detect.lstools_base.open', _open, create=True): + device = deepcopy(base_device) + device['directory_entries'] = ['Board.html', 'User Guide.html'] + self.base._update_device_details_jlink(device, False) + self.assertEqual(device['url'], 'http://www.nxp.com/FRDM-KL27Z') + self.assertEqual(device['platform_name'], 'KL27Z') + _open.assert_called_once_with(os.path.join(dummy_mount_point, 'Board.html'), 'r') + + _open.reset_mock() + + device = deepcopy(base_device) + device['directory_entries'] = ['User Guide.html'] + self.base._update_device_details_jlink(device, False) + self.assertEqual(device['url'], 'http://www.nxp.com/FRDM-KL27Z') + self.assertEqual(device['platform_name'], 'KL27Z') + _open.assert_called_once_with(os.path.join(dummy_mount_point, 'User Guide.html'), 'r') + + _open.reset_mock() + + device = deepcopy(base_device) + device['directory_entries'] = ['unhelpful_file.html'] + self.base._update_device_details_jlink(device, False) + _open.assert_not_called() + + def test_fs_never(self): + device = { + 'target_id_usb_id': '024075309420ABCE', + 'mount_point': 'invalid_mount_point', + 'serial_port': 'invalid_serial_port' + } + self.base.return_value = [device] + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase._update_device_from_fs") as _up_fs,\ + patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as mount_point_ready: + mount_point_ready.return_value = True + + filter = None + ret = self.base.list_mbeds(FSInteraction.Never, filter, read_details_txt=False) + ret_with_details = self.base.list_mbeds(FSInteraction.Never, filter, read_details_txt=True) + self.assertIsNotNone(ret[0]) + self.assertIsNotNone(ret_with_details[0]) + self.assertEqual(ret[0]['target_id'], ret[0]['target_id_usb_id']) + self.assertEqual(ret[0]['platform_name'], "K64F") + self.assertEqual(ret[0], ret_with_details[0]) + _up_fs.assert_not_called() + + filter_in = lambda m: m['platform_name'] == 'K64F' + ret = self.base.list_mbeds(FSInteraction.Never, filter_in, read_details_txt=False) + ret_with_details = self.base.list_mbeds(FSInteraction.Never, filter_in, read_details_txt=True) + self.assertIsNotNone(ret[0]) + self.assertIsNotNone(ret_with_details[0]) + self.assertEqual(ret[0]['target_id'], ret[0]['target_id_usb_id']) + self.assertEqual(ret[0]['platform_name'], "K64F") + self.assertEqual(ret[0], ret_with_details[0]) + _up_fs.assert_not_called() + + filter_out = lambda m: m['platform_name'] != 'K64F' + ret = self.base.list_mbeds(FSInteraction.Never, filter_out, read_details_txt=False) + ret_with_details = self.base.list_mbeds(FSInteraction.Never, filter_out, read_details_txt=True) + _up_fs.assert_not_called() + self.assertEqual(ret, []) + self.assertEqual(ret, ret_with_details) + _up_fs.assert_not_called() + + def test_fs_after(self): + device = { + 'target_id_usb_id': '024075309420ABCE', + 'mount_point': 'invalid_mount_point', + 'serial_port': 'invalid_serial_port' + } + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase._read_htm_ids") as _read_htm,\ + patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase._details_txt") as _up_details,\ + patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as mount_point_ready,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir: + new_device_id = "00017531642046" + _read_htm.return_value = (new_device_id, {}) + _listdir.return_value = ['mbed.htm', 'details.txt'] + _up_details.return_value = { + 'automation_allowed': '0' + } + mount_point_ready.return_value = True + + filter = None + self.base.return_value = [deepcopy(device)] + ret = self.base.list_mbeds(FSInteraction.AfterFilter, filter, False, False) + _up_details.assert_not_called() + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds(FSInteraction.AfterFilter, filter, False, True) + + self.assertIsNotNone(ret[0]) + self.assertIsNotNone(ret_with_details[0]) + self.assertEqual(ret[0]['target_id'], new_device_id) + self.assertEqual(ret_with_details[0]['daplink_automation_allowed'], '0') + self.assertDictContainsSubset(ret[0], ret_with_details[0]) + _read_htm.assert_called_with(device['mount_point']) + _up_details.assert_called_with(device['mount_point']) + + _read_htm.reset_mock() + _up_details.reset_mock() + + filter_in = lambda m: m['target_id'] == device['target_id_usb_id'] + filter_details = lambda m: m.get('daplink_automation_allowed', None) == '0' + + self.base.return_value = [deepcopy(device)] + ret = self.base.list_mbeds( + FSInteraction.AfterFilter, filter_in, False, False) + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds( + FSInteraction.AfterFilter, filter_details, False, True) + + self.assertIsNotNone(ret[0]) + self.assertEqual(ret_with_details, []) + self.assertEqual(ret[0]['target_id'], new_device_id) + _read_htm.assert_called_with(device['mount_point']) + _up_details.assert_not_called() + + _read_htm.reset_mock() + _up_details.reset_mock() + + filter_out = lambda m: m['target_id'] == new_device_id + + self.base.return_value = [deepcopy(device)] + ret = self.base.list_mbeds( + FSInteraction.AfterFilter, filter_out, False, False) + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds( + FSInteraction.AfterFilter, filter_out, False, True) + + self.assertEqual(ret, []) + self.assertEqual(ret_with_details, []) + _read_htm.assert_not_called() + _up_details.assert_not_called() + + def test_get_supported_platforms(self): + supported_platforms = self.base.get_supported_platforms() + self.assertTrue(isinstance(supported_platforms, dict)) + self.assertEqual(supported_platforms['0240'], 'K64F') + + def test_fs_before(self): + device = { + 'target_id_usb_id': '024075309420ABCE', + 'mount_point': 'invalid_mount_point', + 'serial_port': 'invalid_serial_port' + } + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase._read_htm_ids") as _read_htm,\ + patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase._details_txt") as _up_details,\ + patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as mount_point_ready,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir: + new_device_id = u'00017575430420' + _read_htm.return_value = (new_device_id, {}) + _listdir.return_value = ['mbed.htm', 'details.txt'] + _up_details.return_value = { + 'automation_allowed': '0' + } + mount_point_ready.return_value = True + + filter = None + self.base.return_value = [deepcopy(device)] + ret = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter, False, False) + _up_details.assert_not_called() + + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter, False, True) + self.assertIsNotNone(ret[0]) + self.assertIsNotNone(ret_with_details[0]) + self.assertEqual(ret[0]['target_id'], new_device_id) + self.assertEqual(ret_with_details[0]['daplink_automation_allowed'], '0') + self.assertDictContainsSubset(ret[0], ret_with_details[0]) + _read_htm.assert_called_with(device['mount_point']) + _up_details.assert_called_with(device['mount_point']) + + _read_htm.reset_mock() + _up_details.reset_mock() + + filter_in = lambda m: m['target_id'] == '00017575430420' + filter_in_details = lambda m: m['daplink_automation_allowed'] == '0' + self.base.return_value = [deepcopy(device)] + ret = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter_in, False, False) + _up_details.assert_not_called() + + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter_in_details, False, True) + + self.assertIsNotNone(ret[0]) + self.assertIsNotNone(ret_with_details[0]) + self.assertEqual(ret[0]['target_id'], new_device_id) + self.assertEqual(ret_with_details[0]['daplink_automation_allowed'], '0') + self.assertDictContainsSubset(ret[0], ret_with_details[0]) + _read_htm.assert_called_with(device['mount_point']) + _up_details.assert_called_with(device['mount_point']) + + _read_htm.reset_mock() + _up_details.reset_mock() + + filter_out = lambda m: m['target_id'] == '024075309420ABCE' + filter_out_details = lambda m: m['daplink_automation_allowed'] == '1' + ret = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter_out, False, False) + _up_details.assert_not_called() + + self.base.return_value = [deepcopy(device)] + ret_with_details = self.base.list_mbeds( + FSInteraction.BeforeFilter, filter_out_details, False, True) + + self.assertEqual(ret, []) + self.assertEqual(ret_with_details, []) + _read_htm.assert_called_with(device['mount_point']) + +class RetargetTestCase(unittest.TestCase): + """ Test cases that makes use of retargetting + """ + + def setUp(self): + retarget_data = { + '0240DEADBEEF': { + 'serial_port' : 'valid' + } + } + + _open = mock_open(read_data=json.dumps(retarget_data)) + + with patch('mbed_os_tools.detect.lstools_base.isfile') as _isfile,\ + patch('mbed_os_tools.detect.lstools_base.open', _open, create=True): + self.base = DummyLsTools() + _open.assert_called() + + def tearDown(self): + pass + + def test_list_mbeds_valid_platform(self): + self.base.return_value = [{'mount_point': 'dummy_mount_point', + 'target_id_usb_id': u'0240DEADBEEF', + 'serial_port': None}] + with patch('mbed_os_tools.detect.lstools_base.MbedLsToolsBase._read_htm_ids') as _read_htm,\ + patch('mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready') as _mpr,\ + patch('mbed_os_tools.detect.lstools_base.PlatformDatabase.get') as _get,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir: + _mpr.return_value = True + _read_htm.return_value = (u'0240DEADBEEF', {}) + _get.return_value = { + 'platform_name': 'foo_target' + } + _listdir.return_value = ['mbed.htm'] + to_check = self.base.list_mbeds() + self.assertEqual(len(to_check), 1) + self.assertEqual(to_check[0]['serial_port'], 'valid') + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_os_tools/detect/os_darwin.py b/tools/python/python_tests/mbed_os_tools/detect/os_darwin.py new file mode 100644 index 0000000000..c952dfdf2a --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/os_darwin.py @@ -0,0 +1,169 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +import sys +import plistlib +from mock import MagicMock, patch +from six import BytesIO + +from mbed_os_tools.detect.darwin import MbedLsToolsDarwin + +class DarwinTestCase(unittest.TestCase): + """Tests for the Darwin port + """ + + def setUp(self): + with patch("platform.mac_ver") as _pv: + _pv.return_value = ["10.2.2"] + self.darwin = MbedLsToolsDarwin() + + def tearDown(self): + pass + + def test_a_k64f(self): + disks = { + 'AllDisks': ['disk0', 'disk0s1', 'disk0s2', 'disk0s3', 'disk1', 'disk2'], + 'AllDisksAndPartitions': [{ 'Content': 'GUID_partition_scheme', + 'DeviceIdentifier': 'disk0', + 'Partitions': [ + { 'Content': 'EFI', + 'DeviceIdentifier': 'disk0s1', + 'DiskUUID': 'nope', + 'Size': 209715200, + 'VolumeName': 'EFI', + 'VolumeUUID': 'nu-uh'}, + { 'Content': 'Apple_CoreStorage', + 'DeviceIdentifier': 'disk0s2', + 'DiskUUID': 'nodda', + 'Size': 250006216704}, + { 'Content': 'Apple_Boot', + 'DeviceIdentifier': 'disk0s3', + 'DiskUUID': 'no soup for you!', + 'Size': 650002432, + 'VolumeName': 'Recovery HD', + 'VolumeUUID': 'Id rather not'}], + 'Size': 251000193024}, + { 'Content': 'Apple_HFS', + 'DeviceIdentifier': 'disk1', + 'MountPoint': '/', + 'Size': 249653772288, + 'VolumeName': 'Mac HD'}, + { 'Content': '', + 'DeviceIdentifier': 'disk2', + 'MountPoint': '/Volumes/DAPLINK', + 'Size': 67174400, + 'VolumeName': 'DAPLINK'}], + 'VolumesFromDisks': ['Mac HD', 'DAPLINK'], + 'WholeDisks': ['disk0', 'disk1', 'disk2'] + } + usb_tree = [{ + 'IORegistryEntryName': 'DAPLink CMSIS-DAP', + 'USB Serial Number': '0240000034544e45003a00048e3800525a91000097969900', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'AppleUSBHostLegacyClient'}, + {'IORegistryEntryName': 'AppleUSBHostCompositeDevice'}, + {'IORegistryEntryName': 'USB_MSC', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOUSBMassStorageInterfaceNub', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOUSBMassStorageDriverNub', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOUSBMassStorageDriver', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOSCSILogicalUnitNub', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOSCSIPeripheralDeviceType00', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOBlockStorageServices', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOBlockStorageDriver', + 'IORegistryEntryChildren': [ + {'BSD Name': 'disk2', + 'IORegistryEntryName': 'MBED VFS Media', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOMediaBSDClient'}], + }], + }], + }], + }], + }], + }], + }], + }], + }, + {'IORegistryEntryName': 'CMSIS-DAP', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOUSBHostHIDDevice', + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOHIDInterface'}, + {'IORegistryEntryName': 'IOHIDLibUserClient'}, + {'IORegistryEntryName': 'IOHIDLibUserClient'}], + }], + }, + {'IORegistryEntryName': 'mbed Serial Port', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + { 'IORegistryEntryName': 'AppleUSBACMControl'}], + }, + {'IORegistryEntryName': 'mbed Serial Port', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'AppleUSBACMData', + 'idProduct': 516, + 'idVendor': 3368, + 'IORegistryEntryChildren': [ + {'IORegistryEntryName': 'IOModemSerialStreamSync', + 'IORegistryEntryChildren': [ + {'IODialinDevice': '/dev/tty.usbmodem1422', + 'IORegistryEntryName': 'IOSerialBSDClient'}], + }], + }], + }], + } + ] + + with patch("subprocess.Popen") as _popen: + def do_popen(command, *args, **kwargs): + to_ret = MagicMock() + to_ret.wait.return_value = 0 + to_ret.stdout = BytesIO() + plistlib.dump( + {'diskutil': disks, + 'ioreg': usb_tree}[command[0]], + to_ret.stdout) + to_ret.stdout.seek(0) + to_ret.communicate.return_value = (to_ret.stdout.getvalue(), "") + return to_ret + _popen.side_effect = do_popen + candidates = self.darwin.find_candidates() + self.assertIn({'mount_point': '/Volumes/DAPLINK', + 'serial_port': '/dev/tty.usbmodem1422', + 'target_id_usb_id': '0240000034544e45003a00048e3800525a91000097969900', + 'vendor_id': '0d28', + 'product_id': '0204'}, + candidates) diff --git a/tools/python/python_tests/mbed_os_tools/detect/os_linux_generic.py b/tools/python/python_tests/mbed_os_tools/detect/os_linux_generic.py new file mode 100644 index 0000000000..ef1cad548b --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/os_linux_generic.py @@ -0,0 +1,543 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +import sys +import os +from mock import patch, mock_open +from mbed_os_tools.detect.linux import MbedLsToolsLinuxGeneric + + +class LinuxPortTestCase(unittest.TestCase): + ''' Basic test cases checking trivial asserts + ''' + + def setUp(self): + self.linux_generic = MbedLsToolsLinuxGeneric() + + def tearDown(self): + pass + + vfat_devices = [ + b'/dev/sdb on /media/usb0 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdd on /media/usb2 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sde on /media/usb3 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdc on /media/usb1 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)' + ] + + def test_get_mount_point_basic(self): + with patch('mbed_os_tools.detect.linux.MbedLsToolsLinuxGeneric._run_cli_process') as _cliproc: + _cliproc.return_value = (b'\n'.join(self.vfat_devices), None, 0) + mount_dict = dict(self.linux_generic._fat_mounts()) + _cliproc.assert_called_once_with('mount') + self.assertEqual('/media/usb0', mount_dict['/dev/sdb']) + self.assertEqual('/media/usb2', mount_dict['/dev/sdd']) + self.assertEqual('/media/usb3', mount_dict['/dev/sde']) + self.assertEqual('/media/usb1', mount_dict['/dev/sdc']) + + + vfat_devices_ext = [ + b'/dev/sdb on /media/MBED_xxx type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdd on /media/MBED___x type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sde on /media/MBED-xxx type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdc on /media/MBED_x-x type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + + b'/dev/sda on /mnt/NUCLEO type vfat (rw,relatime,uid=999,fmask=0133,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,utf8,flush,errors=remount-ro,uhelper=ldm)', + b'/dev/sdf on /mnt/NUCLEO_ type vfat (rw,relatime,uid=999,fmask=0133,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,utf8,flush,errors=remount-ro,uhelper=ldm)', + b'/dev/sdg on /mnt/DAPLINK type vfat (rw,relatime,sync,uid=999,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro,uhelper=ldm)', + b'/dev/sdh on /mnt/DAPLINK_ type vfat (rw,relatime,sync,uid=999,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro,uhelper=ldm)', + b'/dev/sdi on /mnt/DAPLINK__ type vfat (rw,relatime,sync,uid=999,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro,uhelper=ldm)', + ] + + def test_get_mount_point_ext(self): + with patch('mbed_os_tools.detect.linux.MbedLsToolsLinuxGeneric._run_cli_process') as _cliproc: + _cliproc.return_value = (b'\n'.join(self.vfat_devices_ext), None, 0) + mount_dict = dict(self.linux_generic._fat_mounts()) + _cliproc.assert_called_once_with('mount') + self.assertEqual('/media/MBED_xxx', mount_dict['/dev/sdb']) + self.assertEqual('/media/MBED___x', mount_dict['/dev/sdd']) + self.assertEqual('/media/MBED-xxx', mount_dict['/dev/sde']) + self.assertEqual('/media/MBED_x-x', mount_dict['/dev/sdc']) + + self.assertEqual('/mnt/NUCLEO', mount_dict['/dev/sda']) + self.assertEqual('/mnt/NUCLEO_', mount_dict['/dev/sdf']) + self.assertEqual('/mnt/DAPLINK', mount_dict['/dev/sdg']) + self.assertEqual('/mnt/DAPLINK_', mount_dict['/dev/sdh']) + self.assertEqual('/mnt/DAPLINK__', mount_dict['/dev/sdi']) + + def find_candidates_with_patch(self, mount_list, link_dict, listdir_dict, open_dict): + if not getattr(sys.modules['os'], 'readlink', None): + sys.modules['os'].readlink = None + + def do_open(path, mode='r'): + path = path.replace('\\', '/') + file_object = mock_open(read_data=open_dict[path]).return_value + file_object.__iter__.return_value = open_dict[path].splitlines(True) + return file_object + + with patch('mbed_os_tools.detect.linux.MbedLsToolsLinuxGeneric._run_cli_process') as _cliproc,\ + patch('os.readlink') as _readlink,\ + patch('os.listdir') as _listdir,\ + patch('os.path.abspath') as _abspath,\ + patch('mbed_os_tools.detect.linux.open', do_open) as _,\ + patch('os.path.isdir') as _isdir: + _isdir.return_value = True + _cliproc.return_value = (b'\n'.join(mount_list), None, 0) + def do_readlink(link): + # Fix for testing on Windows + link = link.replace('\\', '/') + return link_dict[link] + _readlink.side_effect = do_readlink + def do_listdir(dir): + # Fix for testing on Windows + dir = dir.replace('\\', '/') + return listdir_dict[dir] + _listdir.side_effect = do_listdir + def do_abspath(dir): + _, path = os.path.splitdrive( + os.path.normpath(os.path.join(os.getcwd(), dir))) + path = path.replace('\\', '/') + return path + _abspath.side_effect = do_abspath + ret_val = self.linux_generic.find_candidates() + _cliproc.assert_called_once_with('mount') + return ret_val + + + listdir_dict_rpi = { + '/dev/disk/by-id': [ + 'usb-MBED_VFS_0240000028634e4500135006691700105f21000097969900-0:0', + 'usb-MBED_VFS_0240000028884e450018700f6bf000338021000097969900-0:0', + 'usb-MBED_VFS_0240000028884e45001f700f6bf000118021000097969900-0:0', + 'usb-MBED_VFS_0240000028884e450036700f6bf000118021000097969900-0:0', + 'usb-MBED_VFS_0240000029164e45001b0012706e000df301000097969900-0:0', + 'usb-MBED_VFS_0240000029164e45002f0012706e0006f301000097969900-0:0', + 'usb-MBED_VFS_9900000031864e45000a100e0000003c0000000097969901-0:0' + ], + '/dev/serial/by-id': [ + 'usb-ARM_DAPLink_CMSIS-DAP_0240000028634e4500135006691700105f21000097969900-if01', + 'usb-ARM_DAPLink_CMSIS-DAP_0240000028884e450018700f6bf000338021000097969900-if01', + 'usb-ARM_DAPLink_CMSIS-DAP_0240000028884e450036700f6bf000118021000097969900-if01', + 'usb-ARM_DAPLink_CMSIS-DAP_0240000029164e45001b0012706e000df301000097969900-if01', + 'usb-ARM_BBC_micro:bit_CMSIS-DAP_9900000031864e45000a100e0000003c0000000097969901-if01' + ], + '/sys/class/block': [ + 'sdb', + 'sdc', + 'sdd', + 'sde', + 'sdf', + 'sdg', + 'sdh', + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-1/1-1.2/1-1.2.6': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-7': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-8': [ + 'idVendor', + 'idProduct' + ] + } + + open_dict_rpi = { + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-1/1-1.2/1-1.2.6/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-1/1-1.2/1-1.2.6/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-7/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-7/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-8/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-8/idProduct': '0204\n' + } + + link_dict_rpi = { + '/dev/disk/by-id/usb-MBED_VFS_0240000028634e4500135006691700105f21000097969900-0:0': '../../sdb', + '/dev/disk/by-id/usb-MBED_VFS_0240000028884e450018700f6bf000338021000097969900-0:0': '../../sdc', + '/dev/disk/by-id/usb-MBED_VFS_0240000028884e45001f700f6bf000118021000097969900-0:0': '../../sdd', + '/dev/disk/by-id/usb-MBED_VFS_0240000028884e450036700f6bf000118021000097969900-0:0': '../../sde', + '/dev/disk/by-id/usb-MBED_VFS_0240000029164e45001b0012706e000df301000097969900-0:0': '../../sdf', + '/dev/disk/by-id/usb-MBED_VFS_0240000029164e45002f0012706e0006f301000097969900-0:0': '../../sdg', + '/dev/disk/by-id/usb-MBED_VFS_9900000031864e45000a100e0000003c0000000097969901-0:0': '../../sdh', + '/dev/serial/by-id/usb-ARM_DAPLink_CMSIS-DAP_0240000028634e4500135006691700105f21000097969900-if01': '../../ttyACM0', + '/dev/serial/by-id/usb-ARM_DAPLink_CMSIS-DAP_0240000028884e450018700f6bf000338021000097969900-if01': '../../ttyACM1', + '/dev/serial/by-id/usb-ARM_DAPLink_CMSIS-DAP_0240000028884e450036700f6bf000118021000097969900-if01': '../../ttyACM3', + '/dev/serial/by-id/usb-ARM_DAPLink_CMSIS-DAP_0240000029164e45001b0012706e000df301000097969900-if01': '../../ttyACM2', + '/dev/serial/by-id/usb-ARM_BBC_micro:bit_CMSIS-DAP_9900000031864e45000a100e0000003c0000000097969901-if01': '../../ttyACM4', + '/sys/class/block/sdb': '../../devices/pci0000:00/0000:00:06.0/usb1/1-1/1-1.2/1-1.2.6/1-1.2.6:1.0/host8568/target8568:0:0/8568:0:0:0/block/sdb', + '/sys/class/block/sdc': '../../devices/pci0000:00/0000:00:06.0/usb1/1-3/1-3:1.0/host4/target4:0:0/4:0:0:0/block/sdc', + '/sys/class/block/sdd': '../../devices/pci0000:00/0000:00:06.0/usb1/1-4/1-4:1.0/host5/target5:0:0/5:0:0:0/block/sdd', + '/sys/class/block/sde': '../../devices/pci0000:00/0000:00:06.0/usb1/1-5/1-5:1.0/host6/target6:0:0/6:0:0:0/block/sde', + '/sys/class/block/sdf': '../../devices/pci0000:00/0000:00:06.0/usb1/1-6/1-6:1.0/host7/target7:0:0/7:0:0:0/block/sdf', + '/sys/class/block/sdg': '../../devices/pci0000:00/0000:00:06.0/usb1/1-7/1-7:1.0/host8/target8:0:0/8:0:0:0/block/sdg', + '/sys/class/block/sdh': '../../devices/pci0000:00/0000:00:06.0/usb1/1-8/1-7:1.0/host9/target9:0:0/9:0:0:0/block/sdh' + } + + mount_list_rpi = [ + b'/dev/sdb on /media/usb0 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdc on /media/usb1 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdd on /media/usb2 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sde on /media/usb3 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdf on /media/usb4 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdg on /media/usb5 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdh on /media/usb6 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)' + ] + def test_get_detected_rpi(self): + mbed_det = self.find_candidates_with_patch( + self.mount_list_rpi, self.link_dict_rpi, self.listdir_dict_rpi, self.open_dict_rpi) + + self.assertIn({ + 'mount_point': '/media/usb0', + 'serial_port': '/dev/ttyACM0', + 'target_id_usb_id': '0240000028634e4500135006691700105f21000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + self.assertIn({ + 'mount_point': '/media/usb1', + 'serial_port': '/dev/ttyACM1', + 'target_id_usb_id': '0240000028884e450018700f6bf000338021000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + self.assertIn({ + 'mount_point': '/media/usb4', + 'serial_port': '/dev/ttyACM2', + 'target_id_usb_id': '0240000029164e45001b0012706e000df301000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + self.assertIn({ + 'mount_point': '/media/usb3', + 'serial_port': '/dev/ttyACM3', + 'target_id_usb_id': '0240000028884e450036700f6bf000118021000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + self.assertIn({ + 'mount_point': '/media/usb6', + 'serial_port': '/dev/ttyACM4', + 'target_id_usb_id': '9900000031864e45000a100e0000003c0000000097969901', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + + + listdir_dict_1 = { + '/dev/disk/by-id': [ + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part1', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part2', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part5', + '/dev/disk/by-id/ata-TSSTcorpDVD-ROM_TS-H352C', + '/dev/disk/by-id/usb-MBED_FDi_sk_A000000001-0:0', + '/dev/disk/by-id/usb-MBED_microcontroller_0240020152986E5EAF6693E6-0:0', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part1', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part2', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part5', + ], + '/dev/serial/by-id': [ + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_0240020152986E5EAF6693E6-if01', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_A000000001-if01', + ], + '/sys/class/block': [ + 'sdb', + 'sdc' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3': [ + 'idVendor', + 'idProduct' + ] + } + + link_dict_1 = { + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM': '../../sda', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part1': '../../sda1', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part2': '../../sda2', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part5': '../../sda5', + '/dev/disk/by-id/ata-TSSTcorpDVD-ROM_TS-H352C': '../../sr0', + '/dev/disk/by-id/usb-MBED_FDi_sk_A000000001-0:0': '../../sdc', + '/dev/disk/by-id/usb-MBED_microcontroller_0240020152986E5EAF6693E6-0:0': '../../sdb', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77': '../../sda', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part1': '../../sda1', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part2': '../../sda2', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part5': '../../sda5', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_0240020152986E5EAF6693E6-if01': '../../ttyACM1', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_A000000001-if01': '../../ttyACM0', + '/sys/class/block/sdb': '../../devices/pci0000:00/0000:00:06.0/usb1/1-2/1-2:1.0/host3/target3:0:0/3:0:0:0/block/sdb', + '/sys/class/block/sdc': '../../devices/pci0000:00/0000:00:06.0/usb1/1-3/1-3:1.0/host4/target4:0:0/4:0:0:0/block/sdc' + } + + open_dict_1 = { + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idProduct': '0204\n' + } + + mount_list_1 = [ + b'/dev/sdb on /media/usb0 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdc on /media/usb1 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)' + ] + def test_get_detected_1_k64f(self): + mbed_det = self.find_candidates_with_patch( + self.mount_list_1, self.link_dict_1, self.listdir_dict_1, self.open_dict_1) + self.assertIn({ + 'mount_point': '/media/usb0', + 'serial_port': '/dev/ttyACM1', + 'target_id_usb_id': '0240020152986E5EAF6693E6', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + + self.assertIn({ + 'mount_point': '/media/usb1', + 'serial_port': '/dev/ttyACM0', + 'target_id_usb_id': 'A000000001', + 'vendor_id': '0d28', + 'product_id': '0204' + }, mbed_det) + + + listdir_dict_2 = { + '/dev/disk/by-id': [ + 'ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM', + 'ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part1', + 'ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part2', + 'ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part5', + 'ata-TSSTcorpDVD-ROM_TS-H352C', + 'usb-MBED_FDi_sk_A000000001-0:0', + 'usb-MBED_microcontroller_02400201489A1E6CB564E3D4-0:0', + 'usb-MBED_microcontroller_0240020152986E5EAF6693E6-0:0', + 'usb-MBED_microcontroller_0240020152A06E54AF5E93EC-0:0', + 'usb-MBED_microcontroller_0672FF485649785087171742-0:0', + 'wwn-0x5000cca30ccffb77', + 'wwn-0x5000cca30ccffb77-part1', + 'wwn-0x5000cca30ccffb77-part2', + 'wwn-0x5000cca30ccffb77-part5' + ], + '/dev/serial/by-id': [ + 'usb-MBED_MBED_CMSIS-DAP_02400201489A1E6CB564E3D4-if01', + 'usb-MBED_MBED_CMSIS-DAP_0240020152986E5EAF6693E6-if01', + 'usb-MBED_MBED_CMSIS-DAP_0240020152A06E54AF5E93EC-if01', + 'usb-MBED_MBED_CMSIS-DAP_A000000001-if01', + 'usb-STMicroelectronics_STM32_STLink_0672FF485649785087171742-if02' + ], + '/sys/class/block': [ + 'sdb', + 'sdc', + 'sdd', + 'sde', + 'sdf' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5': [ + 'idVendor', + 'idProduct' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6': [ + 'idVendor', + 'idProduct' + ] + } + + open_dict_2 = { + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-3/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-4/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-5/idProduct': '0204\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-6/idProduct': '0204\n' + } + + link_dict_2 = { + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM': '../../sda', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part1': '../../sda1', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part2': '../../sda2', + '/dev/disk/by-id/ata-HDS728080PLA380_40Y9028LEN_PFDB32S7S44XLM-part5': '../../sda5', + '/dev/disk/by-id/ata-TSSTcorpDVD-ROM_TS-H352C': '../../sr0', + '/dev/disk/by-id/usb-MBED_FDi_sk_A000000001-0:0': '../../sdc', + '/dev/disk/by-id/usb-MBED_microcontroller_02400201489A1E6CB564E3D4-0:0': '../../sde', + '/dev/disk/by-id/usb-MBED_microcontroller_0240020152986E5EAF6693E6-0:0': '../../sdb', + '/dev/disk/by-id/usb-MBED_microcontroller_0240020152A06E54AF5E93EC-0:0': '../../sdf', + '/dev/disk/by-id/usb-MBED_microcontroller_0672FF485649785087171742-0:0': '../../sdd', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77': '../../sda', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part1': '../../sda1', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part2': '../../sda2', + '/dev/disk/by-id/wwn-0x5000cca30ccffb77-part5': '../../sda5', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_02400201489A1E6CB564E3D4-if01': '../../ttyACM3', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_0240020152986E5EAF6693E6-if01': '../../ttyACM1', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_0240020152A06E54AF5E93EC-if01': '../../ttyACM4', + '/dev/serial/by-id/usb-MBED_MBED_CMSIS-DAP_A000000001-if01': '../../ttyACM0', + '/dev/serial/by-id/usb-STMicroelectronics_STM32_STLink_0672FF485649785087171742-if02': '../../ttyACM2', + '/sys/class/block/sdb': '../../devices/pci0000:00/0000:00:06.0/usb1/1-2/1-2:1.0/host3/target3:0:0/3:0:0:0/block/sdb', + '/sys/class/block/sdc': '../../devices/pci0000:00/0000:00:06.0/usb1/1-3/1-3:1.0/host4/target4:0:0/4:0:0:0/block/sdc', + '/sys/class/block/sdd': '../../devices/pci0000:00/0000:00:06.0/usb1/1-4/1-4:1.0/host5/target5:0:0/5:0:0:0/block/sdd', + '/sys/class/block/sde': '../../devices/pci0000:00/0000:00:06.0/usb1/1-5/1-5:1.0/host6/target6:0:0/6:0:0:0/block/sde', + '/sys/class/block/sdf': '../../devices/pci0000:00/0000:00:06.0/usb1/1-6/1-6:1.0/host7/target7:0:0/7:0:0:0/block/sdf' + } + + mount_list_2 = [ + b'/dev/sdb on /media/usb0 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdc on /media/usb1 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdd on /media/usb2 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sde on /media/usb3 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)', + b'/dev/sdf on /media/usb4 type vfat (rw,noexec,nodev,sync,noatime,nodiratime,gid=1000,uid=1000,dmask=000,fmask=000)' + ] + def test_get_detected_2_k64f(self): + mbed_det = self.find_candidates_with_patch( + self.mount_list_2, self.link_dict_2, self.listdir_dict_2, self.open_dict_2) + + self.assertIn({ + 'mount_point': '/media/usb1', + 'serial_port': '/dev/ttyACM0', + 'target_id_usb_id': 'A000000001', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + self.assertIn({ + 'mount_point': '/media/usb2', + 'serial_port': '/dev/ttyACM2', + 'target_id_usb_id': '0672FF485649785087171742', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + + self.assertIn({ + 'mount_point': '/media/usb4', + 'serial_port': '/dev/ttyACM4', + 'target_id_usb_id': '0240020152A06E54AF5E93EC', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + + self.assertIn({ + 'mount_point': '/media/usb3', + 'serial_port': '/dev/ttyACM3', + 'target_id_usb_id': '02400201489A1E6CB564E3D4', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + + self.assertIn({ + 'mount_point': '/media/usb0', + 'serial_port': '/dev/ttyACM1', + 'target_id_usb_id': '0240020152986E5EAF6693E6', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + + + listdir_dict_4 = { + '/dev/disk/by-id': [ + 'ata-VMware_Virtual_SATA_CDRW_Drive_00000000000000000001', + 'ata-VMware_Virtual_SATA_CDRW_Drive_01000000000000000001', + 'usb-MBED_VFS_0240000033514e45001f500585d40014e981000097969900-0:0' + ], + '/dev/serial/by-id': [ + 'pci-ARM_DAPLink_CMSIS-DAP_0240000033514e45001f500585d40014e981000097969900-if01' + ], + '/sys/class/block': [ + 'sdb' + ], + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2': [ + 'idVendor', + 'idProduct' + ], + } + + open_dict_4 = { + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idVendor': '0d28\n', + '/sys/class/block/../../devices/pci0000:00/0000:00:06.0/usb1/1-2/idProduct': '0204\n' + } + + link_dict_4 = { + '/dev/disk/by-id/ata-VMware_Virtual_SATA_CDRW_Drive_00000000000000000001': '../../sr0', + '/dev/disk/by-id/ata-VMware_Virtual_SATA_CDRW_Drive_01000000000000000001': '../../sr1', + '/dev/disk/by-id/usb-MBED_VFS_0240000033514e45001f500585d40014e981000097969900-0:0': '../../sdb', + '/dev/serial/by-id/pci-ARM_DAPLink_CMSIS-DAP_0240000033514e45001f500585d40014e981000097969900-if01': '../../ttyACM0', + '/sys/class/block/sdb': '../../devices/pci0000:00/0000:00:06.0/usb1/1-2/1-2:1.0/host3/target3:0:0/3:0:0:0/block/sdb' + } + + mount_list_4 = [ + b'/dev/sdb on /media/przemek/DAPLINK type vfat (rw,nosuid,nodev,relatime,uid=1000,gid=1000,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,showexec,utf8,flush,errors=remount-ro,uhelper=udisks2)' + ] + def test_get_detected_3_k64f(self): + mbed_det = self.find_candidates_with_patch( + self.mount_list_4, self.link_dict_4, self.listdir_dict_4, self.open_dict_4) + + self.assertIn({ + 'mount_point': '/media/przemek/DAPLINK', + 'serial_port': '/dev/ttyACM0', + 'target_id_usb_id': '0240000033514e45001f500585d40014e981000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, + mbed_det) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_os_tools/detect/os_win7.py b/tools/python/python_tests/mbed_os_tools/detect/os_win7.py new file mode 100644 index 0000000000..48de3d34c0 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/os_win7.py @@ -0,0 +1,441 @@ +# coding: utf-8 +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +import sys +import os +from mock import MagicMock, patch, call + +# Mock the winreg and _winreg module for non-windows python +_winreg = MagicMock() +sys.modules['_winreg'] = _winreg +sys.modules['winreg'] = _winreg + +from mbed_os_tools.detect.windows import (MbedLsToolsWin7, CompatibleIDsNotFoundException, + _get_cached_mounted_points, _is_mbed_volume, _get_values_with_numeric_keys, + _get_disks, _get_usb_storage_devices, _determine_valid_non_composite_devices, + _determine_subdevice_capability) + +class Win7TestCase(unittest.TestCase): + """ Basic test cases checking trivial asserts + """ + + def setUp(self): + self.lstool = MbedLsToolsWin7() + import logging + logging.basicConfig() + root_logger = logging.getLogger("mbedls") + root_logger.setLevel(logging.DEBUG) + del logging + _winreg.HKEY_LOCAL_MACHINE = None + _winreg.OpenKey.reset_mock() + _winreg.OpenKey.side_effect = None + _winreg.EnumValue.reset_mock() + _winreg.EnumValue.side_effect = None + _winreg.EnumKey.reset_mock() + _winreg.EnumKey.side_effect = None + _winreg.QueryValue.reset_mock() + _winreg.QueryValue.side_effect = None + _winreg.QueryValueEx.reset_mock() + _winreg.QueryInfoKey.reset_mock() + _winreg.QueryInfoKey.side_effect = None + _winreg.CreateKey.reset_mock() + _winreg.CreateKeyEx.reset_mock() + _winreg.DeleteKey.reset_mock() + _winreg.DeleteKeyEx.reset_mock() + _winreg.DeleteValue.reset_mock() + _winreg.SetValue.reset_mock() + _winreg.SetValueEx.reset_mock() + _winreg.SaveKey.reset_mock() + + def test_os_supported(self): + pass + + def test_empty_reg(self): + value_dict = { + (None, 'SYSTEM\\MountedDevices'): [ + ('\\DosDevices\\F:', + u'_??_USBSTOR#Disk&Ven_MBED&Prod_VFS&Rev_0.1#9&215b8c47&0&0240000032044e4500257009997b00386781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}'.encode('utf-16le')), + ], + (None, 'SYSTEM\\CurrentControlSet\\Services\\volume\\Enum'): [], + (None, 'SYSTEM\\CurrentControlSet\\Services\\USBSTOR\\Enum'): [] + } + self.setUpRegistry(value_dict, {}) + candidates = self.lstool.find_candidates() + self.assertEqual(_winreg.OpenKey.mock_calls, [ + call(_winreg.HKEY_LOCAL_MACHINE, 'SYSTEM\\MountedDevices'), + call(_winreg.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum'), + call(_winreg.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Services\\USBSTOR\\Enum') + ]) + self.assertEqual(candidates, []) + + def assertNoRegMut(self): + """Assert that the registry was not mutated in this test""" + _winreg.CreateKey.assert_not_called() + _winreg.CreateKeyEx.assert_not_called() + _winreg.DeleteKey.assert_not_called() + _winreg.DeleteKeyEx.assert_not_called() + _winreg.DeleteValue.assert_not_called() + _winreg.SetValue.assert_not_called() + _winreg.SetValueEx.assert_not_called() + _winreg.SaveKey.assert_not_called() + + def setUpRegistry(self, value_dict, key_dict): + all_keys = set(value_dict.keys()) | set(key_dict.keys()) + def open_key_effect(key, subkey): + if ((key, subkey) in all_keys or key in all_keys): + return key, subkey + else: + raise OSError((key, subkey)) + _winreg.OpenKey.side_effect = open_key_effect + def enum_value(key, index): + try: + a, b = value_dict[key][index] + return a, b, None + except KeyError: + raise OSError + _winreg.EnumValue.side_effect = enum_value + def enum_key(key, index): + try: + return key_dict[key][index] + except KeyError: + raise OSError + _winreg.EnumKey.side_effect = enum_key + def query_value(key, subkey): + try: + return value_dict[(key, subkey)] + except KeyError: + raise OSError + _winreg.QueryValueEx.side_effect = query_value + def query_info_key(key): + return (len(key_dict.get(key, [])), + len(value_dict.get(key, []))) + _winreg.QueryInfoKey.side_effect = query_info_key + + def test_get_values_with_numeric_keys(self): + dummy_key = 'dummy_key' + with patch('mbed_os_tools.detect.windows._iter_vals') as _iter_vals: + _iter_vals.return_value = [ + ('0', True), + ('1', True), + ('Count', False), + ('NextInstance', False), + ] + + values = _get_values_with_numeric_keys(dummy_key) + _iter_vals.assert_called_once_with(dummy_key) + self.assertEqual(len(values), 2) + self.assertTrue(all(v is True for v in values)) + + _iter_vals.reset_mock() + _iter_vals.side_effect = OSError + + self.assertEqual(_get_values_with_numeric_keys(dummy_key), []) + + def test_is_mbed_volume(self): + self.assertTrue(_is_mbed_volume(u'_??_USBSTOR#Disk&Ven_MBED&Prod_VFS&Rev_0.1#0240000032044e4500367009997b00086781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}')) + self.assertTrue(_is_mbed_volume(u'_??_USBSTOR#Disk&Ven_mbed&Prod_VFS&Rev_0.1#0240000032044e4500367009997b00086781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}')) + self.assertFalse(_is_mbed_volume(u'_??_USBSTOR#Disk&Ven_Invalid&Prod_VFS&Rev_0.1#0240000032044e4500367009997b00086781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}')) + self.assertFalse(_is_mbed_volume(u'_??_USBSTOR#Disk&Ven_invalid&Prod_VFS&Rev_0.1#0240000032044e4500367009997b00086781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}')) + + def test_get_cached_mount_points(self): + dummy_val = 'dummy_val' + volume_string_1 = u'_??_USBSTOR#Disk&Ven_MBED&Prod_VFS&Rev_0.1#0240000032044e4500367009997b00086781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}' + volume_string_2 = u'_??_USBSTOR#Disk&Ven_MBED&Prod_VFS&Rev_0.1#1234000032044e4500367009997b00086781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}' + with patch('mbed_os_tools.detect.windows._iter_vals') as _iter_vals, \ + patch('mbed_os_tools.detect.windows._is_mbed_volume') as _is_mbed_volume: + _winreg.OpenKey.return_value = dummy_val + _iter_vals.return_value = [ + ('dummy_device', 'this is not a valid volume string'), + ('\\DosDevices\\D:', + volume_string_1.encode('utf-16le')), + ('\\DosDevices\\invalid_drive', + volume_string_2.encode('utf-16le')) + ] + _is_mbed_volume.return_value = True + + result = _get_cached_mounted_points() + + _winreg.OpenKey.assert_called_once_with(_winreg.HKEY_LOCAL_MACHINE, + 'SYSTEM\\MountedDevices') + _iter_vals.assert_called_once_with(dummy_val) + _is_mbed_volume.assert_has_calls([ + call(volume_string_1), + call(volume_string_2) + ]) + self.assertEqual(result, [ + { + 'mount_point': 'D:', + 'volume_string': volume_string_1 + } + ]) + + _winreg.OpenKey.reset_mock() + _winreg.OpenKey.side_effect = OSError + self.assertEqual(_get_cached_mounted_points(), []) + + + def test_get_disks(self): + dummy_key = 'dummy_key' + volume_strings = [ + 'dummy_volume_1', + 'dummy_volume_2', + ] + with patch('mbed_os_tools.detect.windows._get_values_with_numeric_keys') as _num_keys, \ + patch('mbed_os_tools.detect.windows._is_mbed_volume') as _is_mbed_volume: + _winreg.OpenKey.return_value = dummy_key + _num_keys.return_value = volume_strings + _is_mbed_volume.return_value = True + + result = _get_disks() + + _winreg.OpenKey.assert_called_once_with(_winreg.HKEY_LOCAL_MACHINE, + 'SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum') + _num_keys.assert_called_once_with(dummy_key) + self.assertEqual(result, volume_strings) + + _winreg.OpenKey.reset_mock() + _winreg.OpenKey.side_effect = OSError + _num_keys.reset_mock() + + result = _get_disks() + + _winreg.OpenKey.assert_called_once_with(_winreg.HKEY_LOCAL_MACHINE, + 'SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum') + _num_keys.assert_not_called() + self.assertEqual(result, []) + + def test_get_usb_storage_devices(self): + dummy_key = 'dummy_key' + volume_strings = [ + 'dummy_usb_storage_1', + 'dummy_usb_storage_2', + ] + with patch('mbed_os_tools.detect.windows._get_values_with_numeric_keys') as _num_keys, \ + patch('mbed_os_tools.detect.windows._is_mbed_volume') as _is_mbed_volume: + _winreg.OpenKey.return_value = dummy_key + _num_keys.return_value = volume_strings + _is_mbed_volume.return_value = True + + result = _get_usb_storage_devices() + + _winreg.OpenKey.assert_called_once_with(_winreg.HKEY_LOCAL_MACHINE, + 'SYSTEM\\CurrentControlSet\\Services\\USBSTOR\\Enum') + _num_keys.assert_called_once_with(dummy_key) + self.assertEqual(result, volume_strings) + + _winreg.OpenKey.reset_mock() + _winreg.OpenKey.side_effect = OSError + _num_keys.reset_mock() + + result = _get_usb_storage_devices() + + _winreg.OpenKey.assert_called_once_with(_winreg.HKEY_LOCAL_MACHINE, + 'SYSTEM\\CurrentControlSet\\Services\\USBSTOR\\Enum') + _num_keys.assert_not_called() + self.assertEqual(result, []) + + def test_determine_valid_non_composite_devices(self): + dummy_full_path = 'dummy_full_path' + dummy_target_id = 'dummy_target_id' + dummy_mount_point = 'dummy_mount_point' + devices = [ + { + 'full_path': dummy_full_path, + 'entry_key_string': dummy_target_id + } + ] + target_id_usb_id_mount_point_map = { + dummy_target_id: dummy_mount_point + } + dummy_key = 'dummy_key' + + _winreg.OpenKey.return_value = dummy_key + + with patch('mbed_os_tools.detect.windows._determine_subdevice_capability') as _capability: + _capability.return_value = 'msd' + + result = _determine_valid_non_composite_devices(devices, target_id_usb_id_mount_point_map) + + device_key_string = 'SYSTEM\\CurrentControlSet\\Enum\\' + dummy_full_path + _winreg.OpenKey.assert_called_once_with(_winreg.HKEY_LOCAL_MACHINE, + device_key_string) + _capability.assert_called_once_with(dummy_key) + self.assertEqual(result, { + dummy_target_id: { + 'target_id_usb_id': dummy_target_id, + 'mount_point': dummy_mount_point + } + }) + + def test_determine_subdevice_capability(self): + dummy_key = 'dummy_key' + + _winreg.QueryValueEx.return_value = ( + [ + u'USB\\DevClass_00&SubClass_00&Prot_00', + u'USB\\DevClass_00&SubClass_00', + u'USB\\DevClass_00', + u'USB\\COMPOSITE' + ], + 7 + ) + capability = _determine_subdevice_capability(dummy_key) + _winreg.QueryValueEx.assert_called_once_with(dummy_key, 'CompatibleIDs') + self.assertEqual(capability, 'composite') + + _winreg.QueryValueEx.reset_mock() + _winreg.QueryValueEx.return_value = ( + [ + u'USB\\Class_08&SubClass_06&Prot_50', + u'USB\\Class_08&SubClass_06', + u'USB\\Class_08' + ], + 7 + ) + capability = _determine_subdevice_capability(dummy_key) + _winreg.QueryValueEx.assert_called_once_with(dummy_key, 'CompatibleIDs') + self.assertEqual(capability, 'msd') + + _winreg.QueryValueEx.reset_mock() + _winreg.QueryValueEx.return_value = ( + [ + u'USB\\Class_02&SubClass_02&Prot_01', + u'USB\\Class_02&SubClass_02', + u'USB\\Class_02' + ], + 7 + ) + capability = _determine_subdevice_capability(dummy_key) + _winreg.QueryValueEx.assert_called_once_with(dummy_key, 'CompatibleIDs') + self.assertEqual(capability, 'serial') + + _winreg.QueryValueEx.reset_mock() + _winreg.QueryValueEx.side_effect = OSError + try: + _determine_subdevice_capability(dummy_key) + exception = False + except CompatibleIDsNotFoundException as e: + exception = True + self.assertTrue(exception) + _winreg.QueryValueEx.assert_called_once_with(dummy_key, 'CompatibleIDs') + + + def test_one_composite_dev(self): + value_dict = { + (None, 'SYSTEM\\MountedDevices'): [ + ('\\DosDevices\\C:', u'NOT A VALID MBED DRIVE'.encode('utf-16le')), + ('\\DosDevices\\F:', + u'_??_USBSTOR#Disk&Ven_MBED&Prod_VFS&Rev_0.1#9&215b8c47&0&0240000032044e4500257009997b00386781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}'.encode('utf-16le')) + ], + (None, 'SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum'): [ + ('0', 'USBSTOR\\Disk&Ven_MBED&Prod_VFS&Rev_0.1\\9&215b8c47&0&0240000032044e4500257009997b00386781000097969900&0') + ], + (None, 'SYSTEM\\CurrentControlSet\\Services\\USBSTOR\\Enum'): [ + ('0', 'USB\\VID_0D28&PID_0204&MI_00\\8&26b12a60&0&0000') + ], + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'): [], + (((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'), + '0240000032044e4500257009997b00386781000097969900'), + 'ParentIdPrefix'): ('8&26b12a60&0', None), + (((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'), + '0240000032044e4500257009997b00386781000097969900'), + 'CompatibleIDs'): ([u'USB\\DevClass_00&SubClass_00&Prot_00', u'USB\\DevClass_00&SubClass_00', u'USB\\DevClass_00', u'USB\\COMPOSITE'], 7), + (((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_00'), '8&26b12a60&0&0000'), 'CompatibleIDs'): ([u'USB\\Class_08&SubClass_06&Prot_50', u'USB\\Class_08&SubClass_06', u'USB\\Class_08'], 7), + (((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_01'), + '8&26b12a60&0&0001'), + 'CompatibleIDs'): ([u'USB\\CLASS_02&SUBCLASS_02&PROT_01', u'USB\\CLASS_02&SUBCLASS_02', u'USB\\CLASS_02'], 7), + ((((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_01'), + '8&26b12a60&0&0001'), + 'Device Parameters'), + 'PortName'): ('COM7', None) + } + key_dict = { + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'): + ['0240000032044e4500257009997b00386781000097969900'], + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_00'): [], + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_01'): [], + (((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204&MI_01'), + '8&26b12a60&0&0001'), + 'Device Parameters'): [] + } + self.setUpRegistry(value_dict, key_dict) + + with patch('mbed_os_tools.detect.windows.MbedLsToolsWin7._run_cli_process') as _cliproc: + _cliproc.return_value = ("", "", 0) + expected_info = { + 'mount_point': 'F:', + 'serial_port': 'COM7', + 'target_id_usb_id': u'0240000032044e4500257009997b00386781000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + } + + devices = self.lstool.find_candidates() + self.assertIn(expected_info, devices) + self.assertNoRegMut() + + def test_one_non_composite_dev(self): + value_dict = { + (None, 'SYSTEM\\MountedDevices'): [ + ('\\DosDevices\\C:', u'NOT A VALID MBED DRIVE'.encode('utf-16le')), + ('\\DosDevices\\F:', + u'_??_USBSTOR#Disk&Ven_MBED&Prod_VFS&Rev_0.1#0000000032044e4500257009997b00386781000097969900&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}'.encode('utf-16le')) + ], + (None, 'SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum'): [ + ('0', 'USBSTOR\Disk&Ven_MBED&Prod_VFS&Rev_0.1\\0000000032044e4500257009997b00386781000097969900&0') + ], + (None, 'SYSTEM\\CurrentControlSet\\Services\\USBSTOR\\Enum'): [ + ('0', 'USB\\VID_0D28&PID_0204\\0000000032044e4500257009997b00386781000097969900') + ], + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'): [], + ((None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204\\0000000032044e4500257009997b00386781000097969900'), + 'CompatibleIDs'): ([u'USB\\Class_08&SubClass_06&Prot_50', u'USB\\Class_08&SubClass_06', u'USB\\Class_08'], 7) + } + key_dict = { + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204'): + ['0000000032044e4500257009997b00386781000097969900'], + (None, 'SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_0D28&PID_0204\\0000000032044e4500257009997b00386781000097969900'): [] + } + self.setUpRegistry(value_dict, key_dict) + + with patch('mbed_os_tools.detect.windows.MbedLsToolsWin7._run_cli_process') as _cliproc: + _cliproc.return_value = ("", "", 0) + expected_info = { + 'mount_point': 'F:', + 'serial_port': None, + 'target_id_usb_id': u'0000000032044e4500257009997b00386781000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + } + + devices = self.lstool.find_candidates() + self.assertIn(expected_info, devices) + self.assertNoRegMut() + + def test_mount_point_ready(self): + with patch('mbed_os_tools.detect.windows.MbedLsToolsWin7._run_cli_process') as _cliproc: + _cliproc.return_value = ("dummy", "", 0) + self.assertTrue(self.lstool.mount_point_ready("dummy")) + + _cliproc.reset_mock() + + _cliproc.return_value = ("", "dummy", 1) + self.assertFalse(self.lstool.mount_point_ready("dummy")) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_os_tools/detect/platform_database.py b/tools/python/python_tests/mbed_os_tools/detect/platform_database.py new file mode 100644 index 0000000000..4921dc01ee --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/platform_database.py @@ -0,0 +1,370 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +import os +import errno +import logging +import tempfile +import json +import shutil +from mock import patch, MagicMock, DEFAULT +from io import StringIO + +from mbed_os_tools.detect.platform_database import PlatformDatabase, DEFAULT_PLATFORM_DB,\ + LOCAL_PLATFORM_DATABASE + +try: + unicode +except NameError: + unicode = str + +class EmptyPlatformDatabaseTests(unittest.TestCase): + """ Basic test cases with an empty database + """ + + def setUp(self): + self.tempd_dir = tempfile.mkdtemp() + self.base_db_path = os.path.join(self.tempd_dir, 'base') + self.base_db = open(self.base_db_path, 'w+b') + self.base_db.write(b'{}') + self.base_db.seek(0) + self.pdb = PlatformDatabase([self.base_db_path]) + + def tearDown(self): + self.base_db.close() + shutil.rmtree(self.tempd_dir) + + def test_broken_database_io(self): + """Verify that the platform database still works without a + working backing file + """ + with patch("mbed_os_tools.detect.platform_database.open") as _open: + _open.side_effect = IOError("Bogus") + self.pdb = PlatformDatabase([self.base_db_path]) + self.pdb.add("1234", "MYTARGET") + self.assertEqual(self.pdb.get("1234"), "MYTARGET") + + def test_broken_database_bad_json(self): + """Verify that the platform database still works without a + working backing file + """ + self.base_db.write(b'{}') + self.base_db.seek(0) + self.pdb = PlatformDatabase([self.base_db_path]) + self.pdb.add("1234", "MYTARGET") + self.assertEqual(self.pdb.get("1234"), "MYTARGET") + + def test_broken_database(self): + """Verify that the platform database correctly reset's its database + """ + with patch("mbed_os_tools.detect.platform_database.open") as _open,\ + patch("mbed_os_tools.detect.platform_database._older_than_me") as _older: + _older.return_value = False + stringio = MagicMock() + _open.side_effect = (IOError("Bogus"), stringio) + self.pdb = PlatformDatabase([LOCAL_PLATFORM_DATABASE]) + stringio.__enter__.return_value.write.assert_called_with( + unicode(json.dumps(DEFAULT_PLATFORM_DB))) + self.pdb.add("1234", "MYTARGET") + self.assertEqual(self.pdb.get("1234"), "MYTARGET") + + def test_extra_broken_database(self): + """Verify that the platform database falls back to the built in database + even when it can't write to disk + """ + with patch("mbed_os_tools.detect.platform_database.open") as _open: + _open.side_effect = IOError("Bogus") + self.pdb = PlatformDatabase([LOCAL_PLATFORM_DATABASE]) + self.pdb.add("1234", "MYTARGET") + self.assertEqual(self.pdb.get("1234"), "MYTARGET") + + def test_old_database(self): + """Verify that the platform database correctly updates's its database + """ + with patch("mbed_os_tools.detect.platform_database.open") as _open,\ + patch("mbed_os_tools.detect.platform_database.getmtime") as _getmtime: + stringio = MagicMock() + _open.return_value = stringio + _getmtime.side_effect = (0, 1000000) + self.pdb = PlatformDatabase([LOCAL_PLATFORM_DATABASE]) + stringio.__enter__.return_value.write.assert_called_with( + unicode(json.dumps(DEFAULT_PLATFORM_DB))) + + def test_bogus_database(self): + """Basic empty database test + """ + self.assertEqual(list(self.pdb.items()), []) + self.assertEqual(list(self.pdb.all_ids()), []) + self.assertEqual(self.pdb.get('Also_Junk', None), None) + + def test_add(self): + """Test that what was added can later be queried + """ + self.assertEqual(self.pdb.get('4753', None), None) + self.pdb.add('4753', 'Test_Platform', permanent=False) + self.assertEqual(self.pdb.get('4753', None), 'Test_Platform') + + def test_remove(self): + """Test that once something is removed it no longer shows up when queried + """ + self.assertEqual(self.pdb.get('4753', None), None) + self.pdb.add('4753', 'Test_Platform', permanent=False) + self.assertEqual(self.pdb.get('4753', None), 'Test_Platform') + self.assertEqual(self.pdb.remove('4753', permanent=False), 'Test_Platform') + self.assertEqual(self.pdb.get('4753', None), None) + + def test_remove_all(self): + """Test that multiple entries can be removed at once + """ + self.assertEqual(self.pdb.get('4753', None), None) + self.assertEqual(self.pdb.get('4754', None), None) + self.pdb.add('4753', 'Test_Platform1', permanent=False) + self.pdb.add('4754', 'Test_Platform2', permanent=False) + self.assertEqual(self.pdb.get('4753', None), 'Test_Platform1') + self.assertEqual(self.pdb.get('4754', None), 'Test_Platform2') + self.pdb.remove('*', permanent=False) + self.assertEqual(self.pdb.get('4753', None), None) + self.assertEqual(self.pdb.get('4754', None), None) + + def test_remove_permanent(self): + """Test that once something is removed permanently it no longer shows up + when queried + """ + self.assertEqual(self.pdb.get('4753', None), None) + self.pdb.add('4753', 'Test_Platform', permanent=True) + self.assertEqual(self.pdb.get('4753', None), 'Test_Platform') + + # Recreate platform database to simulate rerunning mbedls + self.pdb = PlatformDatabase([self.base_db_path]) + self.assertEqual(self.pdb.get('4753', None), 'Test_Platform') + self.assertEqual(self.pdb.remove('4753', permanent=True), 'Test_Platform') + self.assertEqual(self.pdb.get('4753', None), None) + + # Recreate platform database to simulate rerunning mbedls + self.pdb = PlatformDatabase([self.base_db_path]) + self.assertEqual(self.pdb.get('4753', None), None) + + def test_remove_all_permanent(self): + """Test that multiple entries can be removed permanently at once + """ + self.assertEqual(self.pdb.get('4753', None), None) + self.assertEqual(self.pdb.get('4754', None), None) + self.pdb.add('4753', 'Test_Platform1', permanent=True) + self.pdb.add('4754', 'Test_Platform2', permanent=True) + self.assertEqual(self.pdb.get('4753', None), 'Test_Platform1') + self.assertEqual(self.pdb.get('4754', None), 'Test_Platform2') + + # Recreate platform database to simulate rerunning mbedls + self.pdb = PlatformDatabase([self.base_db_path]) + self.assertEqual(self.pdb.get('4753', None), 'Test_Platform1') + self.assertEqual(self.pdb.get('4754', None), 'Test_Platform2') + self.pdb.remove('*', permanent=True) + self.assertEqual(self.pdb.get('4753', None), None) + self.assertEqual(self.pdb.get('4754', None), None) + + # Recreate platform database to simulate rerunning mbedls + self.pdb = PlatformDatabase([self.base_db_path]) + self.assertEqual(self.pdb.get('4753', None), None) + self.assertEqual(self.pdb.get('4754', None), None) + + def test_bogus_add(self): + """Test that add requires properly formatted platform ids + """ + self.assertEqual(self.pdb.get('NOTVALID', None), None) + with self.assertRaises(ValueError): + self.pdb.add('NOTVALID', 'Test_Platform', permanent=False) + + def test_bogus_remove(self): + """Test that removing a not present platform does nothing + """ + self.assertEqual(self.pdb.get('NOTVALID', None), None) + self.assertEqual(self.pdb.remove('NOTVALID', permanent=False), None) + + def test_simplify_verbose_data(self): + """Test that fetching a verbose entry without verbose data correctly + returns just the 'platform_name' + """ + platform_data = { + 'platform_name': 'VALID', + 'other_data': 'data' + } + self.pdb.add('1337', platform_data, permanent=False) + self.assertEqual(self.pdb.get('1337', verbose_data=True), platform_data) + self.assertEqual(self.pdb.get('1337'), platform_data['platform_name']) + +class OverriddenPlatformDatabaseTests(unittest.TestCase): + """ Test that for one database overriding another + """ + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.base_db_path = os.path.join(self.temp_dir, 'base') + self.base_db = open(self.base_db_path, 'w+b') + self.base_db.write(json.dumps(dict([('0123', 'Base_Platform')])). + encode('utf-8')) + self.base_db.seek(0) + self.overriding_db_path = os.path.join(self.temp_dir, 'overriding') + self.overriding_db = open(self.overriding_db_path, 'w+b') + self.overriding_db.write(b'{}') + self.overriding_db.seek(0) + self.pdb = PlatformDatabase([self.overriding_db_path, self.base_db_path], + primary_database=self.overriding_db_path) + self.base_db.seek(0) + self.overriding_db.seek(0) + + def tearDown(self): + self.base_db.close() + self.overriding_db.close() + + def assertBaseUnchanged(self): + """Assert that the base database has not changed + """ + self.base_db.seek(0) + self.assertEqual(self.base_db.read(), + json.dumps(dict([('0123', 'Base_Platform')])) + .encode('utf-8')) + + def assertOverrideUnchanged(self): + """Assert that the override database has not changed + """ + self.overriding_db.seek(0) + self.assertEqual(self.overriding_db.read(), b'{}') + + def test_basline(self): + """Sanity check that the base database does what we expect + """ + self.assertEqual(list(self.pdb.items()), [('0123', 'Base_Platform')]) + self.assertEqual(list(self.pdb.all_ids()), ['0123']) + + def test_add_non_override(self): + """Check that adding keys goes to the Override database + """ + self.pdb.add('1234', 'Another_Platform') + self.assertEqual(list(self.pdb.items()), [('1234', 'Another_Platform'), ('0123', 'Base_Platform')]) + self.assertEqual(set(self.pdb.all_ids()), set(['0123', '1234'])) + self.assertBaseUnchanged() + + def test_load_override(self): + """Check that adding a platform goes to the Override database and + you can no longer query for the base database definition and + that the override database was not written to disk + """ + self.overriding_db.write(json.dumps(dict([('0123', 'Overriding_Platform')])). + encode('utf-8')) + self.overriding_db.seek(0) + self.pdb = PlatformDatabase([self.overriding_db_path, self.base_db_path], + primary_database=self.overriding_db_path) + self.assertIn(('0123', 'Overriding_Platform'), list(self.pdb.items())) + self.assertEqual(set(self.pdb.all_ids()), set(['0123'])) + self.assertEqual(self.pdb.get('0123'), 'Overriding_Platform') + self.assertBaseUnchanged() + + def test_add_override_permanent(self): + """Check that adding a platform goes to the Override database and + you can no longer query for the base database definition and + that the override database was written to disk + """ + self.pdb.add('0123', 'Overriding_Platform', permanent=True) + self.assertIn(('0123', 'Overriding_Platform'), list(self.pdb.items())) + self.assertEqual(set(self.pdb.all_ids()), set(['0123'])) + self.assertEqual(self.pdb.get('0123'), 'Overriding_Platform') + self.overriding_db.seek(0) + self.assertEqual(self.overriding_db.read(), + json.dumps(dict([('daplink', dict([('0123', 'Overriding_Platform')]))])) + .encode('utf-8')) + self.assertBaseUnchanged() + + def test_remove_override(self): + """Check that removing a platform from the Override database allows you to query + the original base database definition and that + that the override database was not written to disk + """ + self.pdb.add('0123', 'Overriding_Platform') + self.assertIn(('0123', 'Overriding_Platform'), list(self.pdb.items())) + self.assertEqual(set(self.pdb.all_ids()), set(['0123'])) + self.assertEqual(self.pdb.get('0123'), 'Overriding_Platform') + self.assertEqual(self.pdb.remove('0123'), 'Overriding_Platform') + self.assertEqual(self.pdb.get('0123'), 'Base_Platform') + self.assertOverrideUnchanged() + self.assertBaseUnchanged() + + def test_remove_from_base(self): + """Check that removing a platform from the base database no longer allows you to query + the original base database definition and that that the base database + was not written to disk + """ + self.assertEqual(self.pdb.remove('0123'), 'Base_Platform') + self.assertEqual(self.pdb.get('0123'), None) + self.assertOverrideUnchanged() + self.assertBaseUnchanged() + + def test_remove_from_base_permanent(self): + """Check that removing a platform from the base database no longer allows you to query + the original base database definition and that that the base database + was not modified on disk + """ + self.assertEqual(self.pdb.remove('0123', permanent=True), 'Base_Platform') + self.assertEqual(self.pdb.get('0123'), None) + self.assertBaseUnchanged() + +class InternalLockingChecks(unittest.TestCase): + + def setUp(self): + self.mocked_lock = patch('mbed_os_tools.detect.platform_database.InterProcessLock', spec=True).start() + self.acquire = self.mocked_lock.return_value.acquire + self.release = self.mocked_lock.return_value.release + + self.base_db_path = os.path.join(tempfile.mkdtemp(), 'base') + self.base_db = open(self.base_db_path, 'w+b') + self.base_db.write(b'{}') + self.base_db.seek(0) + self.pdb = PlatformDatabase([self.base_db_path]) + self.addCleanup(patch.stopall) + + def tearDown(self): + self.base_db.close() + + def test_no_update(self): + """Test that no locks are used when no modifications are specified + """ + self.pdb.add('7155', 'Junk') + self.acquire.assert_not_called() + self.release.assert_not_called() + + def test_update(self): + """Test that locks are used when modifications are specified + """ + self.pdb.add('7155', 'Junk', permanent=True) + assert self.acquire.called, 'Lock acquire should have been called' + assert self.release.called + + def test_update_fail_acquire(self): + """Test that the backing file is not updated when lock acquisition fails + """ + self.acquire.return_value = False + self.pdb.add('7155', 'Junk', permanent=True) + assert self.acquire.called, 'Lock acquire should have been called' + self.base_db.seek(0) + self.assertEqual(self.base_db.read(), b'{}') + + def test_update_ambiguous(self): + """Test that the backing file is not updated when lock acquisition fails + """ + self.pdb._prim_db = None + self.pdb.add('7155', 'Junk', permanent=True) + self.acquire.assert_not_called() + self.release.assert_not_called() + self.assertEqual(self.base_db.read(), b'{}') diff --git a/tools/python/python_tests/mbed_os_tools/detect/platform_detection.py b/tools/python/python_tests/mbed_os_tools/detect/platform_detection.py new file mode 100644 index 0000000000..960553b4cd --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/platform_detection.py @@ -0,0 +1,211 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +import os +import copy +from mock import patch, mock_open, DEFAULT + +from mbed_os_tools.detect.lstools_base import MbedLsToolsBase + +TEST_DATA_PATH = 'test_data' + +class DummyLsTools(MbedLsToolsBase): + return_value = [] + def find_candidates(self): + return self.return_value + +try: + basestring +except NameError: + # Python 3 + basestring = str + + +def get_case_insensitive_path(path, file_name): + for entry in os.listdir(path): + if entry.lower() == file_name.lower(): + return os.path.join(path, entry) + + raise Exception('No matching file for %s found in $s' % (file_name, path)) + + +class PlatformDetectionTestCase(unittest.TestCase): + """ Basic test cases checking trivial asserts + """ + + def setUp(self): + self.base = DummyLsTools() + + def tearDown(self): + pass + + def run_test(self, test_data_case, candidate_data, expected_data): + # Add necessary candidate data + candidate_data['mount_point'] = 'dummy_mount_point' + + # Find the test data in the test_data folder + test_script_path = os.path.dirname(os.path.abspath(__file__)) + test_data_path = os.path.join(test_script_path, TEST_DATA_PATH) + test_data_cases = os.listdir(test_data_path) + self.assertTrue(test_data_case in test_data_cases, 'Expected %s to be present in %s folder' % (test_data_case, test_data_path)) + test_data_case_path = os.path.join(test_data_path, test_data_case) + + # NOTE a limitation of this mocked test is that it only allows mocking of one directory level. + # This is enough at the moment because all firmwares seem to use a flat file structure. + # If this changes in the future, this mocking framework can be extended to support this. + + test_data_case_file_names = os.listdir(test_data_case_path) + mocked_open_file_paths = [os.path.join(candidate_data['mount_point'], file_name ) for file_name in test_data_case_file_names] + + # Setup all the mocks + self.base.return_value = [candidate_data] + + def do_open(path, mode='r'): + file_name = os.path.basename(path) + try: + with open(get_case_insensitive_path(test_data_case_path, file_name), 'r') as test_data_file: + test_data_file_data = test_data_file.read() + except OSError: + raise OSError("(mocked open) No such file or directory: '%s'" % (path)) + + file_object = mock_open(read_data=test_data_file_data).return_value + file_object.__iter__.return_value = test_data_file_data.splitlines(True) + return file_object + + with patch("mbed_os_tools.detect.lstools_base.MbedLsToolsBase.mount_point_ready") as _mpr,\ + patch('mbed_os_tools.detect.lstools_base.open', do_open) as _,\ + patch('mbed_os_tools.detect.lstools_base.listdir') as _listdir: + _mpr.return_value = True + _listdir.return_value = test_data_case_file_names + results = self.base.list_mbeds(read_details_txt=True) + + # There should only ever be one result + self.assertEqual(len(results), 1) + actual = results[0] + + expected_keys = list(expected_data.keys()) + actual_keys = list(actual.keys()) + differing_map = {} + for key in expected_keys: + actual_value = actual.get(key) + if actual_value != expected_data[key]: + differing_map[key] = (actual_value, expected_data[key]) + + + if differing_map: + differing_string = '' + for differing_key in sorted(list(differing_map.keys())): + actual, expected = differing_map[differing_key] + differing_string += ' "%s": "%s" (expected "%s")\n' % (differing_key, actual, expected) + + assert_string = 'Expected data mismatch:\n\n{\n%s}' % (differing_string) + self.assertTrue(False, assert_string) + + + + def test_efm32pg_stk3401_jlink(self): + self.run_test('efm32pg_stk3401_jlink', { + 'target_id_usb_id': u'000440074453', + 'vendor_id': '1366', + 'product_id': '1015' + }, { + 'platform_name': 'EFM32PG_STK3401', + 'device_type': 'jlink', + 'target_id': '2035022D000122D5D475113A', + 'target_id_usb_id': '000440074453', + 'target_id_mbed_htm': '2035022D000122D5D475113A' + }) + + def test_lpc1768(self): + self.run_test('lpc1768', { + 'target_id_usb_id': u'101000000000000000000002F7F20DF3', + 'vendor_id': '0d28', + 'product_id': '0204' + }, { + 'platform_name': 'LPC1768', + 'device_type': 'daplink', + 'target_id': '101000000000000000000002F7F20DF3d51e6be5ac41795761dc44148e3b7000', + 'target_id_usb_id': '101000000000000000000002F7F20DF3', + 'target_id_mbed_htm': '101000000000000000000002F7F20DF3d51e6be5ac41795761dc44148e3b7000' + }) + + def test_nucleo_f411re_stlink(self): + self.run_test('nucleo_f411re_stlink', { + 'target_id_usb_id': u'0671FF554856805087112815', + 'vendor_id': '0483', + 'product_id': '374b' + }, { + 'platform_name': 'NUCLEO_F411RE', + 'device_type': 'stlink', + 'target_id': '07400221076061193824F764', + 'target_id_usb_id': '0671FF554856805087112815', + 'target_id_mbed_htm': '07400221076061193824F764' + }) + + def test_nrf51_microbit(self): + self.run_test('nrf51_microbit', { + 'target_id_usb_id': u'9900007031324e45000f9019000000340000000097969901', + 'vendor_id': '0d28', + 'product_id': '0204' + }, { + 'platform_name': 'NRF51_MICROBIT', + 'device_type': 'daplink', + 'target_id': '9900007031324e45000f9019000000340000000097969901', + 'target_id_usb_id': '9900007031324e45000f9019000000340000000097969901' + }) + + def test_k64f_daplink(self): + self.run_test('k64f_daplink', { + 'target_id_usb_id': u'0240000032044e45000a700a997b00356781000097969900', + 'vendor_id': '0d28', + 'product_id': '0204' + }, { + 'platform_name': 'K64F', + 'device_type': 'daplink', + 'target_id': '0240000032044e45000a700a997b00356781000097969900', + 'target_id_usb_id': '0240000032044e45000a700a997b00356781000097969900', + 'target_id_mbed_htm': '0240000032044e45000a700a997b00356781000097969900' + }) + + def test_nrf52_dk_daplink(self): + self.run_test('nrf52_dk_daplink', { + 'target_id_usb_id': u'110100004420312043574641323032203233303397969903', + 'vendor_id': '0d28', + 'product_id': '0204' + }, { + 'platform_name': 'NRF52_DK', + 'device_type': 'daplink', + 'target_id': '110100004420312043574641323032203233303397969903', + 'target_id_usb_id': '110100004420312043574641323032203233303397969903', + 'target_id_mbed_htm': '110100004420312043574641323032203233303397969903' + }) + + def test_nrf52_dk_jlink(self): + self.run_test('nrf52_dk_jlink', { + 'target_id_usb_id': u'000682546728', + 'vendor_id': '1366', + 'product_id': '1015' + }, { + 'platform_name': 'NRF52_DK', + 'device_type': 'jlink', + 'target_id': '000682546728', + 'target_id_usb_id': '000682546728' + }) + + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/MBED.HTM b/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/MBED.HTM new file mode 100644 index 0000000000..ffbc11f554 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/MBED.HTM @@ -0,0 +1,10 @@ + + + + + +mbed Website Shortcut + + + + diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/README.TXT b/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/README.TXT new file mode 100644 index 0000000000..56446f38e9 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/README.TXT @@ -0,0 +1,15 @@ +Silicon Labs J-Link MSD volume. + +Copying a binary file here will flash it to the MCU. + +Supported binary file formats: + - Intel Hex (.hex) + - Motorola s-records (.mot) + - Binary (.bin/.par/.crd) + +Known limitations: + - Virtual COM port speed is currently fixed at 115200 bps + - Using other kit functionality such as energy profiling or debugger while flashing is + not supported. + Workaround: Pause any energy profiling and disconnect any connected debug sessions + before flashing. diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/sl_kit.html b/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/sl_kit.html new file mode 100644 index 0000000000..b3ed4e0855 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/sl_kit.html @@ -0,0 +1,8 @@ + + + + EFM32PG-STK3401 - Pearl Gecko STK + + + + diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/sl_qsg.html b/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/sl_qsg.html new file mode 100644 index 0000000000..a4b36c3067 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/efm32pg_stk3401_jlink/sl_qsg.html @@ -0,0 +1 @@ +Simplicity Studio Shortcut diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/k64f_daplink/DETAILS.TXT b/tools/python/python_tests/mbed_os_tools/detect/test_data/k64f_daplink/DETAILS.TXT new file mode 100644 index 0000000000..bf3cc8e799 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/k64f_daplink/DETAILS.TXT @@ -0,0 +1,13 @@ +# DAPLink Firmware - see https://mbed.com/daplink +Unique ID: 0240000032044e45000a700a997b00356781000097969900 +HIC ID: 97969900 +Auto Reset: 0 +Automation allowed: 0 +Overflow detection: 0 +Daplink Mode: Interface +Interface Version: 0244 +Git SHA: 363abb00ee17ad50cb407c6d2e299407332e3c85 +Local Mods: 1 +USB Interfaces: MSD, CDC, HID +Interface CRC: 0xc44e96e1 +Remount count: 0 diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/k64f_daplink/MBED.HTM b/tools/python/python_tests/mbed_os_tools/detect/test_data/k64f_daplink/MBED.HTM new file mode 100644 index 0000000000..b7c021ec87 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/k64f_daplink/MBED.HTM @@ -0,0 +1,13 @@ + + + + + +mbed Website Shortcut + + + + + diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/lpc1768/MBED.HTM b/tools/python/python_tests/mbed_os_tools/detect/test_data/lpc1768/MBED.HTM new file mode 100644 index 0000000000..8b5bc0bd73 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/lpc1768/MBED.HTM @@ -0,0 +1,9 @@ + + + + +mbed Website Shortcut + + + + diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/lpc1768/basic.bin b/tools/python/python_tests/mbed_os_tools/detect/test_data/lpc1768/basic.bin new file mode 100644 index 0000000000..a5f3fc283b Binary files /dev/null and b/tools/python/python_tests/mbed_os_tools/detect/test_data/lpc1768/basic.bin differ diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/lpc1768/dirs.bin b/tools/python/python_tests/mbed_os_tools/detect/test_data/lpc1768/dirs.bin new file mode 100644 index 0000000000..508973922e Binary files /dev/null and b/tools/python/python_tests/mbed_os_tools/detect/test_data/lpc1768/dirs.bin differ diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf51_microbit/DETAILS.TXT b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf51_microbit/DETAILS.TXT new file mode 100644 index 0000000000..ffcd336220 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf51_microbit/DETAILS.TXT @@ -0,0 +1,13 @@ +# DAPLink Firmware - see https://mbed.com/daplink +Unique ID: 9900007031324e45000f9019000000340000000097969901 +HIC ID: 97969901 +Auto Reset: 1 +Automation allowed: 0 +Overflow detection: 0 +Daplink Mode: Interface +Interface Version: 0244 +Git SHA: 72ea244a3a9219299c5655c614c955185451e98e +Local Mods: 1 +USB Interfaces: MSD, CDC, HID, WebUSB +Interface CRC: 0xfdd0c098 +Remount count: 0 diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf51_microbit/MICROBIT.HTM b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf51_microbit/MICROBIT.HTM new file mode 100644 index 0000000000..28e5e08c86 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf51_microbit/MICROBIT.HTM @@ -0,0 +1,13 @@ + + + + + +mbed Website Shortcut + + + + + diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_daplink/DETAILS.TXT b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_daplink/DETAILS.TXT new file mode 100644 index 0000000000..de7ce14dc5 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_daplink/DETAILS.TXT @@ -0,0 +1,13 @@ +# DAPLink Firmware - see https://mbed.com/daplink +Unique ID: 110100004420312043574641323032203233303397969903 +HIC ID: 97969903 +Auto Reset: 0 +Automation allowed: 0 +Overflow detection: 0 +Daplink Mode: Interface +Interface Version: 0245 +Git SHA: aace60cd16996cc881567f35eb84db063a9268b7 +Local Mods: 0 +USB Interfaces: MSD, CDC, HID +Interface CRC: 0xfa307a74 +Remount count: 0 diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_daplink/MBED.HTM b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_daplink/MBED.HTM new file mode 100644 index 0000000000..f5754d0312 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_daplink/MBED.HTM @@ -0,0 +1,13 @@ + + + + + +mbed Website Shortcut + + + + + diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/MBED.HTM b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/MBED.HTM new file mode 100644 index 0000000000..83d5285e1b --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/MBED.HTM @@ -0,0 +1 @@ +mbed Website Shortcut \ No newline at end of file diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/README.TXT b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/README.TXT new file mode 100644 index 0000000000..ef2ec13f17 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/README.TXT @@ -0,0 +1,2 @@ +SEGGER J-Link MSD volume. +This volume is used to update the flash content of your target device. \ No newline at end of file diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/Segger.html b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/Segger.html new file mode 100644 index 0000000000..824b1378b0 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/Segger.html @@ -0,0 +1 @@ +SEGGER Shortcut \ No newline at end of file diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/User Guide.html b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/User Guide.html new file mode 100644 index 0000000000..42d1f4821d --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/nrf52_dk_jlink/User Guide.html @@ -0,0 +1 @@ +Starter Guide Shortcut \ No newline at end of file diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/nucleo_f411re_stlink/DETAILS.TXT b/tools/python/python_tests/mbed_os_tools/detect/test_data/nucleo_f411re_stlink/DETAILS.TXT new file mode 100644 index 0000000000..b1cb7ce8aa --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/nucleo_f411re_stlink/DETAILS.TXT @@ -0,0 +1,2 @@ +Version: 0221 +Build: Jan 11 2016 16:12:36 diff --git a/tools/python/python_tests/mbed_os_tools/detect/test_data/nucleo_f411re_stlink/MBED.HTM b/tools/python/python_tests/mbed_os_tools/detect/test_data/nucleo_f411re_stlink/MBED.HTM new file mode 100644 index 0000000000..cbcf288551 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/detect/test_data/nucleo_f411re_stlink/MBED.HTM @@ -0,0 +1,10 @@ + + + + +mbed Website Shortcut + + + + + \ No newline at end of file diff --git a/tools/python/python_tests/mbed_os_tools/test/__init__.py b/tools/python/python_tests/mbed_os_tools/test/__init__.py new file mode 100644 index 0000000000..d541fe2621 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +"""! @package mbed-host-tests-test + +Unit tests for mbed-host-tests package + +""" diff --git a/tools/python/python_tests/mbed_os_tools/test/host_test_black_box.py b/tools/python/python_tests/mbed_os_tools/test/host_test_black_box.py new file mode 100644 index 0000000000..a4bdf89fea --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/host_test_black_box.py @@ -0,0 +1,62 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 copy import copy +from mbed_os_tools.test import init_host_test_cli_params +from mbed_os_tools.test.host_tests_runner.host_test_default import DefaultTestSelector + +from .mocks.environment.linux import MockTestEnvironmentLinux +from .mocks.environment.darwin import MockTestEnvironmentDarwin +from .mocks.environment.windows import MockTestEnvironmentWindows + +mock_platform_info = { + "platform_name": "K64F", + "target_id": "0240000031754e45000c0018948500156461000097969900", + "mount_point": "/mnt/DAPLINK", + "serial_port": "/dev/ttyACM0", +} +mock_image_path = "BUILD/tests/K64F/GCC_ARM/TESTS/network/interface/interface.bin" + +class BlackBoxHostTestTestCase(unittest.TestCase): + + def _run_host_test(self, environment): + with environment as _env: + test_selector = DefaultTestSelector(init_host_test_cli_params()) + result = test_selector.execute() + test_selector.finish() + + self.assertEqual(result, 0) + + def test_host_test_linux(self): + self._run_host_test( + MockTestEnvironmentLinux(self, mock_platform_info, mock_image_path) + ) + + def test_host_test_darwin(self): + self._run_host_test( + MockTestEnvironmentDarwin(self, mock_platform_info, mock_image_path) + ) + + def test_host_test_windows(self): + win_mock_platform_info = copy(mock_platform_info) + win_mock_platform_info["serial_port"] = "COM5" + + self._run_host_test( + MockTestEnvironmentWindows(self, win_mock_platform_info, mock_image_path) + ) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_os_tools/test/host_test_default.py b/tools/python/python_tests/mbed_os_tools/test/host_test_default.py new file mode 100644 index 0000000000..ef9836ec7a --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/host_test_default.py @@ -0,0 +1,54 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 mbed_os_tools.test.host_tests_runner.host_test_default import DefaultTestSelector + + +class HostTestDefaultTestCase(unittest.TestCase): + + def test_os_info(self): + expected = { + "grm_module" : "module_name", + "grm_host" : "10.2.123.43", + "grm_port" : "3334", + } + + # Case that includes an IP address but no protocol + arg = [expected["grm_module"], expected["grm_host"], expected["grm_port"]] + result = DefaultTestSelector._parse_grm(":".join(arg)) + self.assertEqual(result, expected) + + # Case that includes an IP address but no protocol nor a no port + expected["grm_port"] = None + arg = [expected["grm_module"], expected["grm_host"]] + result = DefaultTestSelector._parse_grm(":".join(arg)) + self.assertEqual(result, expected) + + # Case that includes an IP address and a protocol + expected["grm_host"] = "https://10.2.123.43" + expected["grm_port"] = "443" + arg = [expected["grm_module"], expected["grm_host"], expected["grm_port"]] + result = DefaultTestSelector._parse_grm(":".join(arg)) + self.assertEqual(result, expected) + + # Case that includes an IP address and a protocol, but no port + expected["grm_port"] = None + arg = [expected["grm_module"], expected["grm_host"]] + result = DefaultTestSelector._parse_grm(":".join(arg)) + self.assertEqual(result, expected) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_os_tools/test/mocks/__init__.py b/tools/python/python_tests/mbed_os_tools/test/mocks/__init__.py new file mode 100644 index 0000000000..3004b70a6e --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/mocks/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. diff --git a/tools/python/python_tests/mbed_os_tools/test/mocks/environment/__init__.py b/tools/python/python_tests/mbed_os_tools/test/mocks/environment/__init__.py new file mode 100644 index 0000000000..adb2527bef --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/mocks/environment/__init__.py @@ -0,0 +1,110 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 tempfile +import os +import shutil + +from mock import patch, MagicMock +from copy import copy +from ..serial import MockSerial +from ..mbed_device import MockMbedDevice +from ..process import MockProcess + +class MockTestEnvironment(object): + + def __init__(self, test_case, platform_info, image_path): + self._test_case = test_case + self._tempdir = tempfile.mkdtemp() + self._patch_definitions = [] + self.patches = {} + self._platform_info = copy(platform_info) + + # Clean and retarget path to tempdir + self._image_path = self._clean_path(image_path) + self._platform_info['mount_point'] = self._clean_path( + self._platform_info['mount_point'] + ) + + # Need to remove the drive letter in this case + self._platform_info['serial_port'] = os.path.splitdrive( + self._clean_path(self._platform_info['serial_port']) + )[1] + + args = ( + 'mbedhtrun -m {} -p {}:9600 -f ' + '"{}" -e "TESTS/host_tests" -d {} -c default ' + '-t {} -r default ' + '-C 4 --sync 5 -P 60' + ).format( + self._platform_info['platform_name'], + self._platform_info['serial_port'], + self._image_path, + self._platform_info['mount_point'], + self._platform_info['target_id'] + ).split() + self.patch('sys.argv', new=args) + + # Mock detect + detect_mock = MagicMock() + detect_mock.return_value.list_mbeds.return_value = [ + self._platform_info + ] + self.patch('mbed_os_tools.detect.create', new=detect_mock) + + # Mock process calls and move them to threads to preserve mocks + self.patch( + 'mbed_os_tools.test.host_tests_runner.host_test_default.Process', + new=MagicMock(side_effect=self._process_side_effect) + ) + self.patch( + 'mbed_os_tools.test.host_tests_plugins.host_test_plugins.call', + new=MagicMock(return_value=0) + ) + + mock_serial = MockSerial() + mock_device = MockMbedDevice(mock_serial) + self.patch( + 'mbed_os_tools.test.host_tests_conn_proxy.conn_primitive_serial.Serial', + new=MagicMock(return_value=mock_serial) + ) + + def _clean_path(self, path): + # Remove the drive letter and ensure separators are consistent + path = os.path.splitdrive(os.path.normpath(path))[1] + return os.path.join(self._tempdir, path.lstrip(os.sep)) + + @staticmethod + def _process_side_effect(target=None, args=None): + return MockProcess(target=target, args=args) + + def patch(self, path, **kwargs): + self._patch_definitions.append((path, patch(path, **kwargs))) + + def __enter__(self): + os.makedirs(os.path.dirname(self._image_path)) + with open(self._image_path, 'w') as _: + pass + + os.makedirs(self._platform_info['mount_point']) + + for path, patcher in self._patch_definitions: + self.patches[path] = patcher.start() + + def __exit__(self, type, value, traceback): + for _, patcher in self._patch_definitions: + patcher.stop() + + shutil.rmtree(self._tempdir) diff --git a/tools/python/python_tests/mbed_os_tools/test/mocks/environment/darwin.py b/tools/python/python_tests/mbed_os_tools/test/mocks/environment/darwin.py new file mode 100644 index 0000000000..e0b0346a7c --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/mocks/environment/darwin.py @@ -0,0 +1,47 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from builtins import super +from mock import MagicMock + +from .posix import MockTestEnvironmentPosix + +class MockTestEnvironmentDarwin(MockTestEnvironmentPosix): + + def __init__(self, test_case, platform_info, image_path): + super().__init__(test_case, platform_info, image_path) + + self.patch( + 'os.uname', + new=MagicMock(return_value=('Darwin',)), + create=True + ) + + def __exit__(self, type, value, traceback): + super().__exit__(type, value, traceback) + + if value: + return False + + # Assert for proper image copy + mocked_call = self.patches[ + 'mbed_os_tools.test.host_tests_plugins.host_test_plugins.call' + ] + + second_call_args = mocked_call.call_args_list[1][0][0] + self._test_case.assertEqual(second_call_args, ["sync"]) + + # Ensure only two subprocesses were started + self._test_case.assertEqual(len(mocked_call.call_args_list), 2) diff --git a/tools/python/python_tests/mbed_os_tools/test/mocks/environment/linux.py b/tools/python/python_tests/mbed_os_tools/test/mocks/environment/linux.py new file mode 100644 index 0000000000..1c91f8bbd0 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/mocks/environment/linux.py @@ -0,0 +1,58 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +from builtins import super +from mock import MagicMock + +from .posix import MockTestEnvironmentPosix + +class MockTestEnvironmentLinux(MockTestEnvironmentPosix): + + def __init__(self, test_case, platform_info, image_path): + super().__init__(test_case, platform_info, image_path) + + self.patch( + 'os.uname', + new=MagicMock(return_value=('Linux',)), + create=True + ) + + def __exit__(self, type, value, traceback): + super().__exit__(type, value, traceback) + + if value: + return False + + # Assert for proper image copy + mocked_call = self.patches[ + 'mbed_os_tools.test.host_tests_plugins.host_test_plugins.call' + ] + + second_call_args = mocked_call.call_args_list[1][0][0] + destination_path = os.path.normpath( + os.path.join( + self._platform_info["mount_point"], + os.path.basename(self._image_path) + ) + ) + + self._test_case.assertEqual( + second_call_args, + ["sync", "-f", destination_path] + ) + + # Ensure only two subprocesses were started + self._test_case.assertEqual(len(mocked_call.call_args_list), 2) diff --git a/tools/python/python_tests/mbed_os_tools/test/mocks/environment/posix.py b/tools/python/python_tests/mbed_os_tools/test/mocks/environment/posix.py new file mode 100644 index 0000000000..6806b5764c --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/mocks/environment/posix.py @@ -0,0 +1,43 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +from builtins import super + +from . import MockTestEnvironment + +class MockTestEnvironmentPosix(MockTestEnvironment): + + def __init__(self, test_case, platform_info, image_path): + super().__init__(test_case, platform_info, image_path) + + self.patch('os.name', new='posix') + + def __exit__(self, type, value, traceback): + super().__exit__(type, value, traceback) + + if value: + return False + + # Assert for proper image copy + mocked_call = self.patches[ + 'mbed_os_tools.test.host_tests_plugins.host_test_plugins.call' + ] + + first_call_args = mocked_call.call_args_list[0][0][0] + self._test_case.assertEqual(first_call_args[0], "cp") + self._test_case.assertEqual(first_call_args[1], self._image_path) + self._test_case.assertTrue(first_call_args[2].startswith(self._platform_info["mount_point"])) + self._test_case.assertTrue(first_call_args[2].endswith(os.path.splitext(self._image_path)[1])) diff --git a/tools/python/python_tests/mbed_os_tools/test/mocks/environment/windows.py b/tools/python/python_tests/mbed_os_tools/test/mocks/environment/windows.py new file mode 100644 index 0000000000..590dc4f425 --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/mocks/environment/windows.py @@ -0,0 +1,46 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +from builtins import super + +from . import MockTestEnvironment + +class MockTestEnvironmentWindows(MockTestEnvironment): + + def __init__(self, test_case, platform_info, image_path): + super().__init__(test_case, platform_info, image_path) + + self.patch('os.name', new='nt') + + def __exit__(self, type, value, traceback): + super().__exit__(type, value, traceback) + + if value: + return False + + # Assert for proper image copy + mocked_call = self.patches[ + 'mbed_os_tools.test.host_tests_plugins.host_test_plugins.call' + ] + + first_call_args = mocked_call.call_args_list[0][0][0] + self._test_case.assertEqual(first_call_args[0], "copy") + self._test_case.assertEqual(first_call_args[1], self._image_path) + self._test_case.assertTrue(first_call_args[2].startswith(self._platform_info["mount_point"])) + self._test_case.assertTrue(first_call_args[2].endswith(os.path.splitext(self._image_path)[1])) + + # Ensure only one subprocess was started + self._test_case.assertEqual(len(mocked_call.call_args_list), 1) diff --git a/tools/python/python_tests/mbed_os_tools/test/mocks/mbed_device.py b/tools/python/python_tests/mbed_os_tools/test/mocks/mbed_device.py new file mode 100644 index 0000000000..d1dde7119b --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/mocks/mbed_device.py @@ -0,0 +1,53 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 re + +class MockMbedDevice(object): + + KV_REGEX = re.compile("\{\{([\w\d_-]+);([^\}]+)\}\}") + + def __init__(self, serial): + self._synced = False + self._kvs = [] + self._serial = serial + self._serial.on_upstream_write(self.on_write) + + def handle_kv(self, key, value): + if not self._synced: + if key == "__sync": + self._synced = True + self.send_kv(key, value) + self.on_sync() + else: + pass + + def send_kv(self, key, value): + self._serial.downstream_write("{{{{{};{}}}}}\r\n".format(key, value)) + + def on_write(self, data): + kvs = self.KV_REGEX.findall(data.decode("utf-8")) + + for key, value in kvs: + self.handle_kv(key, value) + self._kvs.append((key, value)) + + def on_sync(self): + self._serial.downstream_write( + "{{__timeout;15}}\r\n" + "{{__host_test_name;default_auto}}\r\n" + "{{end;success}}\n" + "{{__exit;0}}\r\n" + ) diff --git a/tools/python/python_tests/mbed_os_tools/test/mocks/process.py b/tools/python/python_tests/mbed_os_tools/test/mocks/process.py new file mode 100644 index 0000000000..a7b7dc937d --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/mocks/process.py @@ -0,0 +1,27 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + + +from builtins import super +from threading import Thread + +class MockProcess(Thread): + def __init__(self, target=None, args=None): + super().__init__(target=target, args=args) + self._terminates = 0 + self.exitcode = 0 + + def terminate(self): + self._terminates += 1 diff --git a/tools/python/python_tests/mbed_os_tools/test/mocks/serial.py b/tools/python/python_tests/mbed_os_tools/test/mocks/serial.py new file mode 100644 index 0000000000..777a552b7d --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/mocks/serial.py @@ -0,0 +1,49 @@ +# Copyright (c) 2018, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +from builtins import super + +class MockSerial(object): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._args = args + self._kwargs = kwargs + self._open = True + self._rx_counter = 0 + self._tx_buffer = b"" + self._rx_buffer = b"" + self._upstream_write_cb = None + + def read(self, count): + contents = self._rx_buffer[self._rx_counter:count] + self._rx_counter += len(contents) + return contents + + def write(self, data): + self._tx_buffer += data + if self._upstream_write_cb: + self._upstream_write_cb(data) + + def close(self): + self._open = False + + def downstream_write(self, data): + self._rx_buffer += data.encode("utf-8") + + def downstream_write_bytes(self, data): + self._rx_buffer += data + + def on_upstream_write(self, func): + self._upstream_write_cb = func diff --git a/tools/python/python_tests/mbed_os_tools/test/test_conn_primitive_serial.py b/tools/python/python_tests/mbed_os_tools/test/test_conn_primitive_serial.py new file mode 100644 index 0000000000..30d57ae2bc --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/test_conn_primitive_serial.py @@ -0,0 +1,86 @@ +# Copyright (c) 2018-2021, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +import mock + +from mbed_os_tools.test.host_tests_conn_proxy.conn_primitive_serial import SerialConnectorPrimitive +from mbed_os_tools.test.host_tests_conn_proxy.conn_primitive import ConnectorPrimitiveException + +@mock.patch("mbed_os_tools.test.host_tests_conn_proxy.conn_primitive_serial.Serial") +@mock.patch("mbed_os_tools.test.host_tests_plugins.host_test_plugins.detect") +class ConnPrimitiveSerialTestCase(unittest.TestCase): + def test_provided_serial_port_used_with_target_id(self, mock_detect, mock_serial): + platform_name = "irrelevant" + target_id = "1234" + port = "COM256" + baudrate = "9600" + + # The mock list_mbeds() call needs to return a list of dictionaries, + # and each dictionary must have a "serial_port", or else the + # check_serial_port_ready() function we are testing will sleep waiting + # for the serial port to become ready. + mock_detect.create().list_mbeds.return_value = [ + {"target_id": target_id, + "serial_port": port, + "platform_name": platform_name}, + ] + + # Set skip_reset to avoid the use of a physical serial port. + config = { + "port": port, + "baudrate": baudrate, + "image_path": "test.bin", + "platform_name": "kaysixtyfoureff", + "target_id": "9900", + "skip_reset": True, + } + connector = SerialConnectorPrimitive("SERI", port, baudrate, config=config) + + mock_detect.create().list_mbeds.assert_not_called() + + def test_discovers_serial_port_with_target_id(self, mock_detect, mock_serial): + platform_name = "kaysixtyfoureff" + target_id = "9900" + port = "COM256" + baudrate = "9600" + + mock_detect.create().list_mbeds.return_value = [ + {"target_id": target_id, + "serial_port": port, + "platform_name": platform_name}, + ] + + # Set skip_reset to avoid the use of a physical serial port. Don't pass + # in a port, so that auto-detection based on target_id will find the + # port for us (using our mock list_mbeds data). + config = { + "port": None, + "baudrate": baudrate, + "image_path": "test.bin", + "platform_name": platform_name, + "target_id": target_id, + "skip_reset": True, + } + try: + connector = SerialConnectorPrimitive("SERI", None, baudrate, config=config) + except ConnectorPrimitiveException: + # lol bad + pass + + mock_detect.create().list_mbeds.assert_called_once() + +if __name__ == '__main__': + unittest.main() diff --git a/tools/python/python_tests/mbed_os_tools/test/test_mbed_base.py b/tools/python/python_tests/mbed_os_tools/test/test_mbed_base.py new file mode 100644 index 0000000000..56daa4b26b --- /dev/null +++ b/tools/python/python_tests/mbed_os_tools/test/test_mbed_base.py @@ -0,0 +1,112 @@ +# Copyright (c) 2021, Arm Limited and affiliates. +# SPDX-License-Identifier: Apache-2.0 +# +# 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 shutil +import mock +import os +import unittest +from tempfile import mkdtemp + +from mbed_os_tools.test.host_tests_runner.mbed_base import Mbed + +class TemporaryDirectory(object): + def __init__(self): + self.fname = "tempdir" + + def __enter__(self): + self.fname = mkdtemp() + return self.fname + + def __exit__(self, *args, **kwargs): + shutil.rmtree(self.fname) + +@mock.patch("mbed_os_tools.test.host_tests_runner.mbed_base.ht_plugins") +@mock.patch("mbed_os_tools.test.host_tests_runner.mbed_base.detect") +class TestMbed(unittest.TestCase): + def test_skips_discover_mbed_if_non_mbed_copy_method_used( + self, mock_detect, mock_ht_plugins + ): + with TemporaryDirectory() as tmpdir: + image_path = os.path.join(tmpdir, "test.elf") + with open(image_path, "w") as f: + f.write("1234") + + options = mock.Mock( + copy_method="pyocd", + image_path=image_path, + disk=None, + port="port", + micro="mcu", + target_id="BK99", + polling_timeout=5, + program_cycle_s=None, + json_test_configuration=None, + format="blah", + ) + + mbed = Mbed(options) + mbed.copy_image() + + mock_detect.create().list_mbeds.assert_not_called() + mock_ht_plugins.call_plugin.assert_called_once_with( + "CopyMethod", + options.copy_method, + image_path=options.image_path, + mcu=options.micro, + serial=options.port, + destination_disk=options.disk, + target_id=options.target_id, + pooling_timeout=options.polling_timeout, + format=options.format, + ) + + def test_discovers_mbed_if_mbed_copy_method_used( + self, mock_detect, mock_ht_plugins + ): + with TemporaryDirectory() as tmpdir: + image_path = os.path.join(tmpdir, "test.elf") + with open(image_path, "w") as f: + f.write("1234") + options = mock.Mock( + copy_method="shell", + image_path=image_path, + disk=None, + port="port", + micro="mcu", + target_id="BK99", + polling_timeout=5, + program_cycle_s=None, + json_test_configuration=None, + format="blah", + ) + + mbed = Mbed(options) + mbed.copy_image() + + mock_detect.create().list_mbeds.assert_called() + mock_ht_plugins.call_plugin.assert_called_once_with( + "CopyMethod", + options.copy_method, + image_path=options.image_path, + mcu=options.micro, + serial=options.port, + destination_disk=options.disk, + target_id=options.target_id, + pooling_timeout=options.polling_timeout, + format=options.format, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/python/python_tests/mbed_tools/__init__.py b/tools/python/python_tests/mbed_tools/__init__.py new file mode 100644 index 0000000000..0368d0f1a0 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tools/python/python_tests/mbed_tools/cli/__init__.py b/tools/python/python_tests/mbed_tools/cli/__init__.py new file mode 100644 index 0000000000..0368d0f1a0 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/cli/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tools/python/python_tests/mbed_tools/cli/test_build.py b/tools/python/python_tests/mbed_tools/cli/test_build.py new file mode 100644 index 0000000000..bb6b70aa96 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/cli/test_build.py @@ -0,0 +1,338 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import contextlib +import os +import pathlib + +from tempfile import TemporaryDirectory +from unittest import TestCase, mock + +from click.testing import CliRunner + +import sys +print(sys.path) + +from mbed_tools.cli.build import build +from mbed_tools.project._internal.project_data import BUILD_DIR +from mbed_tools.build.config import CMAKE_CONFIG_FILE + + +DEFAULT_BUILD_ARGS = ["-t", "GCC_ARM", "-m", "K64F"] +DEFAULT_BUILD_SUBDIR = pathlib.Path("K64F", "develop", "GCC_ARM") + + +@contextlib.contextmanager +def mock_project_directory( + program, mbed_config_exists=False, build_tree_exists=False, build_subdir=DEFAULT_BUILD_SUBDIR +): + with TemporaryDirectory() as tmp_dir: + root = pathlib.Path(tmp_dir, "test-project") + root.mkdir() + program.root = root + program.files.cmake_build_dir = root / BUILD_DIR / build_subdir + program.files.cmake_config_file = root / BUILD_DIR / build_subdir / CMAKE_CONFIG_FILE + if mbed_config_exists: + program.files.cmake_config_file.parent.mkdir(exist_ok=True, parents=True) + program.files.cmake_config_file.touch(exist_ok=True) + + if build_tree_exists: + program.files.cmake_build_dir.mkdir(exist_ok=True, parents=True) + + yield + + +@mock.patch("mbed_tools.cli.build.generate_build_system") +@mock.patch("mbed_tools.cli.build.build_project") +@mock.patch("mbed_tools.cli.build.MbedProgram") +@mock.patch("mbed_tools.cli.build.generate_config") +class TestBuildCommand(TestCase): + def test_searches_for_mbed_program_at_default_project_path( + self, generate_config, mbed_program, build_project, generate_build_system + ): + runner = CliRunner() + runner.invoke(build, DEFAULT_BUILD_ARGS) + + mbed_program.from_existing.assert_called_once_with(pathlib.Path(os.getcwd()), DEFAULT_BUILD_SUBDIR) + + def test_searches_for_mbed_program_at_user_defined_project_root( + self, generate_config, mbed_program, build_project, generate_build_system + ): + project_path = "my-blinky" + + runner = CliRunner() + runner.invoke(build, ["-p", project_path, *DEFAULT_BUILD_ARGS]) + + mbed_program.from_existing.assert_called_once_with(pathlib.Path(project_path), DEFAULT_BUILD_SUBDIR) + + def test_calls_generate_build_system_if_build_tree_nonexistent( + self, generate_config, mbed_program, build_project, generate_build_system + ): + program = mbed_program.from_existing() + with mock_project_directory(program, mbed_config_exists=True, build_tree_exists=False): + generate_config.return_value = [mock.MagicMock(), mock.MagicMock()] + + runner = CliRunner() + runner.invoke(build, DEFAULT_BUILD_ARGS) + + generate_build_system.assert_called_once_with(program.root, program.files.cmake_build_dir, "develop") + + def test_generate_config_called_if_config_script_nonexistent( + self, generate_config, mbed_program, build_project, generate_build_system + ): + program = mbed_program.from_existing() + with mock_project_directory( + program, mbed_config_exists=False, build_subdir=pathlib.Path("K64F", "develop", "GCC_ARM") + ): + target = "k64f" + toolchain = "gcc_arm" + + runner = CliRunner() + runner.invoke(build, ["-t", toolchain, "-m", target]) + + generate_config.assert_called_once_with(target.upper(), toolchain.upper(), program) + + def test_raises_if_gen_config_toolchain_not_passed_when_required( + self, generate_config, mbed_program, build_project, generate_build_system + ): + program = mbed_program.from_existing() + with mock_project_directory(program, mbed_config_exists=False): + target = "k64f" + + runner = CliRunner() + result = runner.invoke(build, ["-m", target]) + + self.assertIsNotNone(result.exception) + self.assertRegex(result.output, "--toolchain") + + def test_raises_if_gen_config_target_not_passed_when_required( + self, generate_config, mbed_program, build_project, generate_build_system + ): + program = mbed_program.from_existing() + with mock_project_directory(program, mbed_config_exists=False): + toolchain = "gcc_arm" + + runner = CliRunner() + result = runner.invoke(build, ["-t", toolchain]) + + self.assertIsNotNone(result.exception) + self.assertRegex(result.output, "--mbed-target") + + def test_raises_if_target_identifier_not_int( + self, generate_config, mbed_program, build_project, generate_build_system + ): + target = "K64F[foo]" + + result = CliRunner().invoke(build, ["-m", target, "-t", "gcc_arm"]) + self.assertIsNotNone(result.exception) + self.assertRegex(result.output, "ID") + + def test_raises_if_target_identifier_negative( + self, generate_config, mbed_program, build_project, generate_build_system + ): + target = "K64F[-1]" + + result = CliRunner().invoke(build, ["-m", target, "-t", "gcc_arm"]) + self.assertIsNotNone(result.exception) + self.assertRegex(result.output, "ID") + + def test_build_system_regenerated_when_mbed_os_path_passed( + self, generate_config, mbed_program, build_project, generate_build_system + ): + program = mbed_program.from_existing() + with mock_project_directory( + program, + mbed_config_exists=True, + build_tree_exists=True, + build_subdir=pathlib.Path("K64F", "develop", "GCC_ARM"), + ): + generate_config.return_value = [mock.MagicMock(), mock.MagicMock()] + + toolchain = "gcc_arm" + target = "k64f" + mbed_os_path = "./extern/mbed-os" + + runner = CliRunner() + runner.invoke(build, ["-t", toolchain, "-m", target, "--mbed-os-path", mbed_os_path]) + + generate_config.assert_called_once_with(target.upper(), toolchain.upper(), program) + generate_build_system.assert_called_once_with(program.root, program.files.cmake_build_dir, "develop") + + def test_custom_targets_location_used_when_passed( + self, generate_config, mbed_program, build_project, generate_build_system + ): + program = mbed_program.from_existing() + with mock_project_directory(program, mbed_config_exists=True, build_tree_exists=True): + toolchain = "gcc_arm" + target = "k64f" + custom_targets_json_path = pathlib.Path("custom", "custom_targets.json") + + runner = CliRunner() + runner.invoke(build, ["-t", toolchain, "-m", target, "--custom-targets-json", custom_targets_json_path]) + + generate_config.assert_called_once_with(target.upper(), toolchain.upper(), program) + self.assertEqual(program.files.custom_targets_json, custom_targets_json_path) + + def test_app_config_used_when_passed( + self, generate_config, mbed_program, build_project, generate_build_system + ): + program = mbed_program.from_existing() + with mock_project_directory(program, mbed_config_exists=True, build_tree_exists=True): + toolchain = "gcc_arm" + target = "k64f" + app_config_path = pathlib.Path("alternative_config.json") + + runner = CliRunner() + runner.invoke(build, ["-t", toolchain, "-m", target, "--app-config", app_config_path]) + + generate_config.assert_called_once_with(target.upper(), toolchain.upper(), program) + self.assertEqual(program.files.app_config_file, app_config_path) + + def test_profile_used_when_passed( + self, generate_config, mbed_program, build_project, generate_build_system + ): + program = mbed_program.from_existing() + mbed_program.reset_mock() # clear call count from previous line + + with mock_project_directory(program, mbed_config_exists=True, build_tree_exists=True): + generate_config.return_value = [mock.MagicMock(), mock.MagicMock()] + + toolchain = "gcc_arm" + target = "k64f" + profile = "release" + + runner = CliRunner() + runner.invoke(build, ["-t", toolchain, "-m", target, "--profile", profile]) + + mbed_program.from_existing.assert_called_once_with( + pathlib.Path(os.getcwd()), + pathlib.Path(target.upper(), profile, toolchain.upper()) + ) + generate_config.assert_called_once_with(target.upper(), toolchain.upper(), program) + generate_build_system.assert_called_once_with(program.root, program.files.cmake_build_dir, profile) + + def test_build_folder_removed_when_clean_flag_passed( + self, generate_config, mbed_program, build_project, generate_build_system + ): + program = mbed_program.from_existing() + with mock_project_directory( + program, + mbed_config_exists=True, + build_tree_exists=True, + build_subdir=pathlib.Path("K64F", "develop", "GCC_ARM"), + ): + generate_config.return_value = [mock.MagicMock(), mock.MagicMock()] + + toolchain = "gcc_arm" + target = "k64f" + + runner = CliRunner() + runner.invoke(build, ["-t", toolchain, "-m", target, "-c"]) + + generate_config.assert_called_once_with(target.upper(), toolchain.upper(), program) + generate_build_system.assert_called_once_with(program.root, program.files.cmake_build_dir, "develop") + self.assertFalse(program.files.cmake_build_dir.exists()) + + @mock.patch("mbed_tools.cli.build.flash_binary") + @mock.patch("mbed_tools.cli.build.find_all_connected_devices") + def test_build_flash_options_bin_target( + self, + mock_find_devices, + flash_binary, + generate_config, + mbed_program, + build_project, + generate_build_system, + ): + # A target with bin images does not need OUTPUT_EXT defined + generate_config.return_value = [mock.MagicMock(), mock.MagicMock()] + mock_find_devices.return_value = [mock.MagicMock()] + runner = CliRunner() + runner.invoke(build, ["--flash", *DEFAULT_BUILD_ARGS]) + call_args = flash_binary.call_args + args, kwargs = call_args + flash_binary.assert_called_once_with(args[0], args[1], args[2], args[3], False) + + @mock.patch("mbed_tools.cli.build.flash_binary") + @mock.patch("mbed_tools.cli.build.find_all_connected_devices") + def test_build_flash_options_hex_target( + self, + mock_find_devices, + flash_binary, + generate_config, + mbed_program, + build_project, + generate_build_system, + ): + generate_config.return_value = [{"OUTPUT_EXT": "hex"}, mock.MagicMock()] + mock_find_devices.return_value = [mock.MagicMock()] + runner = CliRunner() + runner.invoke(build, ["--flash", *DEFAULT_BUILD_ARGS]) + call_args = flash_binary.call_args + args, kwargs = call_args + flash_binary.assert_called_once_with(args[0], args[1], args[2], args[3], True) + + @mock.patch("mbed_tools.cli.build.flash_binary") + @mock.patch("mbed_tools.cli.build.find_all_connected_devices") + def test_build_flash_both_two_devices( + self, + mock_find_devices, + flash_binary, + generate_config, + mbed_program, + build_project, + generate_build_system, + ): + generate_config.return_value = [mock.MagicMock(), mock.MagicMock()] + mock_find_devices.return_value = [mock.MagicMock(), mock.MagicMock()] + runner = CliRunner() + runner.invoke(build, ["--flash", *DEFAULT_BUILD_ARGS]) + self.assertEqual(flash_binary.call_count, 2) + + @mock.patch("mbed_tools.cli.build.flash_binary") + @mock.patch("mbed_tools.cli.build.find_connected_device") + def test_build_flash_only_identifier_device( + self, + mock_find_device, + flash_binary, + generate_config, + mbed_program, + build_project, + generate_build_system, + ): + generate_config.return_value = [mock.MagicMock(), mock.MagicMock()] + mock_find_device.return_value = mock.MagicMock() + runner = CliRunner() + runner.invoke(build, ["--flash", "-m", "K64F[1]", "-t", "GCC_ARM"]) + self.assertEqual(flash_binary.call_count, 1) + + @mock.patch("mbed_tools.cli.build.terminal") + @mock.patch("mbed_tools.cli.build.find_connected_device") + def test_sterm_is_started_when_flag_passed( + self, mock_find_device, mock_terminal, generate_config, mbed_program, build_project, generate_build_system + ): + target = "K64F" + serial_port = "tty.k64f" + baud = 115200 + mock_find_device.return_value = mock.Mock(serial_port=serial_port) + generate_config.return_value = [mock.MagicMock(), mock.MagicMock()] + + CliRunner().invoke(build, ["-m", target, "-t", "gcc_arm", "--sterm", "--baudrate", baud]) + + mock_find_device.assert_called_once_with(target, None) + mock_terminal.run.assert_called_once_with(serial_port, baud) + + @mock.patch("mbed_tools.cli.build.terminal") + @mock.patch("mbed_tools.cli.build.find_connected_device") + def test_raises_if_device_does_not_have_serial_port_and_sterm_flag_given( + self, mock_find_device, mock_terminal, generate_config, mbed_program, build_project, generate_build_system + ): + target = "K64F" + serial_port = None + mock_find_device.return_value = mock.Mock(serial_port=serial_port) + generate_config.return_value = [mock.MagicMock(), mock.MagicMock()] + + output = CliRunner().invoke(build, ["-m", target, "-t", "gcc_arm", "--sterm"]) + self.assertEqual(type(output.exception), SystemExit) + mock_terminal.assert_not_called() diff --git a/tools/python/python_tests/mbed_tools/cli/test_configure.py b/tools/python/python_tests/mbed_tools/cli/test_configure.py new file mode 100644 index 0000000000..0483e99c01 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/cli/test_configure.py @@ -0,0 +1,81 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import pathlib + +from unittest import TestCase, mock + +from click.testing import CliRunner + +from mbed_tools.cli.configure import configure + + +class TestConfigureCommand(TestCase): + @mock.patch("mbed_tools.cli.configure.generate_config") + @mock.patch("mbed_tools.cli.configure.MbedProgram") + def test_generate_config_called_with_correct_arguments(self, program, generate_config): + CliRunner().invoke(configure, ["-m", "k64f", "-t", "gcc_arm"]) + + generate_config.assert_called_once_with("K64F", "GCC_ARM", program.from_existing()) + + @mock.patch("mbed_tools.cli.configure.generate_config") + @mock.patch("mbed_tools.cli.configure.MbedProgram") + def test_generate_config_called_with_mbed_os_path(self, program, generate_config): + CliRunner().invoke(configure, ["-m", "k64f", "-t", "gcc_arm", "--mbed-os-path", "./extern/mbed-os"]) + + generate_config.assert_called_once_with("K64F", "GCC_ARM", program.from_existing()) + + @mock.patch("mbed_tools.cli.configure.generate_config") + @mock.patch("mbed_tools.cli.configure.MbedProgram") + def test_custom_targets_location_used_when_passed(self, program, generate_config): + program = program.from_existing() + custom_targets_json_path = pathlib.Path("custom", "custom_targets.json") + CliRunner().invoke( + configure, ["-t", "gcc_arm", "-m", "k64f", "--custom-targets-json", custom_targets_json_path] + ) + + generate_config.assert_called_once_with("K64F", "GCC_ARM", program) + self.assertEqual(program.files.custom_targets_json, custom_targets_json_path) + + @mock.patch("mbed_tools.cli.configure.generate_config") + @mock.patch("mbed_tools.cli.configure.MbedProgram") + def test_custom_output_directory_used_when_passed(self, program, generate_config): + program = program.from_existing() + output_dir = pathlib.Path("build") + CliRunner().invoke(configure, ["-t", "gcc_arm", "-m", "k64f", "-o", output_dir]) + + generate_config.assert_called_once_with("K64F", "GCC_ARM", program) + self.assertEqual(program.files.cmake_build_dir, output_dir) + + @mock.patch("mbed_tools.cli.configure.generate_config") + @mock.patch("mbed_tools.cli.configure.MbedProgram") + def test_app_config_used_when_passed(self, program, generate_config): + program = program.from_existing() + app_config_path = pathlib.Path("alternative_config.json") + CliRunner().invoke( + configure, ["-t", "gcc_arm", "-m", "k64f", "--app-config", app_config_path] + ) + + generate_config.assert_called_once_with("K64F", "GCC_ARM", program) + self.assertEqual(program.files.app_config_file, app_config_path) + + @mock.patch("mbed_tools.cli.configure.generate_config") + @mock.patch("mbed_tools.cli.configure.MbedProgram") + def test_profile_used_when_passed(self, program, generate_config): + test_program = program.from_existing() + program.reset_mock() # clear call count from previous line + + toolchain = "gcc_arm" + target = "k64f" + profile = "release" + + CliRunner().invoke( + configure, ["-t", toolchain, "-m", target, "--profile", profile] + ) + + program.from_existing.assert_called_once_with( + pathlib.Path("."), + pathlib.Path(target.upper(), profile, toolchain.upper()) + ) + generate_config.assert_called_once_with("K64F", "GCC_ARM", test_program) diff --git a/tools/python/python_tests/mbed_tools/cli/test_devices_command_integration.py b/tools/python/python_tests/mbed_tools/cli/test_devices_command_integration.py new file mode 100644 index 0000000000..b6d1351afa --- /dev/null +++ b/tools/python/python_tests/mbed_tools/cli/test_devices_command_integration.py @@ -0,0 +1,41 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from unittest import TestCase, mock + +import click +from click.testing import CliRunner + +from mbed_tools.cli import main +from mbed_tools.cli.list_connected_devices import list_connected_devices +from mbed_tools.lib.exceptions import ToolsError + + +class TestDevicesCommandIntegration(TestCase): + def test_devices_is_integrated(self): + self.assertEqual(main.cli.commands["detect"], list_connected_devices) + + +class TestClickGroupWithExceptionHandling(TestCase): + @mock.patch("mbed_tools.cli.main.LOGGER.error", autospec=True) + def test_logs_tools_errors(self, logger_error): + def callback(): + raise ToolsError() + + mock_cli = click.Command("test", callback=callback) + main.cli.add_command(mock_cli, "test") + + runner = CliRunner() + result = runner.invoke(main.cli, ["test"]) + + self.assertEqual(1, result.exit_code) + logger_error.assert_called_once() + + +class TestVersionCommand(TestCase): + def test_version_command(self): + runner = CliRunner() + result = runner.invoke(main.cli, ["--version"]) + self.assertTrue(result.output) + self.assertEqual(0, result.exit_code) diff --git a/tools/python/python_tests/mbed_tools/cli/test_list_connected_devices.py b/tools/python/python_tests/mbed_tools/cli/test_list_connected_devices.py new file mode 100644 index 0000000000..38402ed9bd --- /dev/null +++ b/tools/python/python_tests/mbed_tools/cli/test_list_connected_devices.py @@ -0,0 +1,284 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import json +import pathlib +import pytest +from click.testing import CliRunner +from mbed_tools.devices.device import ConnectedDevices +from mbed_tools.targets import Board +from unittest import mock + +from mbed_tools.cli.list_connected_devices import list_connected_devices +from mbed_tools.devices import Device + + +@pytest.fixture +def get_connected_devices(): + with mock.patch("mbed_tools.cli.list_connected_devices.get_connected_devices") as cd: + yield cd + + +def create_fake_device( + serial_number="8675309", + serial_port="/dev/ttyUSB/default", + mount_points=(pathlib.Path("/media/you/DISCO"), pathlib.Path("/media/you/NUCLEO")), + interface_version="VA3JME", + board_type="BoardType", + board_name="BoardName", + product_code="0786", + target_type="TargetType", + slug="slug", + build_variant=("NS", "S"), + mbed_os_support=("mbed1", "mbed2"), + mbed_enabled=("baseline", "extended"), +): + device_attrs = { + "serial_number": serial_number, + "serial_port": serial_port, + "mount_points": mount_points, + "interface_version": interface_version, + } + board_attrs = { + "board_type": board_type, + "board_name": board_name, + "product_code": product_code, + "target_type": target_type, + "slug": slug, + "build_variant": build_variant, + "mbed_os_support": mbed_os_support, + "mbed_enabled": mbed_enabled, + } + return Device(Board(**board_attrs), **device_attrs) + + +class TestListConnectedDevices: + def test_informs_when_no_devices_are_connected(self, get_connected_devices): + get_connected_devices.return_value = ConnectedDevices() + + result = CliRunner().invoke(list_connected_devices) + + assert result.exit_code == 0 + assert "No connected Mbed devices found." in result.output + + +class TestListConnectedDevicesTabularOutput: + @pytest.mark.parametrize( + "header, device_attr", + [ + ("Serial number", "serial_number"), + ("Serial port", "serial_port"), + ("Interface Version", "interface_version"), + ], + ) + def test_device_attr_included(self, header, device_attr, get_connected_devices): + heading_name = header + device = create_fake_device() + get_connected_devices.return_value = ConnectedDevices(identified_devices=[device]) + attr = getattr(device, device_attr) + + result = CliRunner().invoke(list_connected_devices) + + output_lines = result.output.splitlines() + + assert result.exit_code == 0 + assert heading_name in output_lines[0] + assert attr in output_lines[2] + assert output_lines[0].find(heading_name) == output_lines[2].find(attr) + + @pytest.mark.parametrize("header, board_attr", [("Board name", "board_name")]) + def test_board_attr_included(self, header, board_attr, get_connected_devices): + heading_name = header + device = create_fake_device() + get_connected_devices.return_value = ConnectedDevices(identified_devices=[device]) + attr = getattr(device.mbed_board, board_attr) + + result = CliRunner().invoke(list_connected_devices) + + output_lines = result.output.splitlines() + assert result.exit_code == 0 + assert heading_name in output_lines[0] + assert attr in output_lines[2] + assert output_lines[0].find(heading_name) == output_lines[2].find(attr) + + @pytest.mark.parametrize("header, device_attr", [("Mount point(s)", "mount_points")]) + def test_multiline_device_attr_included(self, header, device_attr, get_connected_devices): + heading_name = header + device = create_fake_device() + get_connected_devices.return_value = ConnectedDevices(identified_devices=[device]) + attr = getattr(device, device_attr) + + result = CliRunner().invoke(list_connected_devices) + + output_lines = result.output.splitlines() + assert result.exit_code == 0 + assert heading_name in output_lines[0] + for i, a in enumerate(attr): + assert str(a) in output_lines[2 + i] + assert output_lines[0].find(heading_name) == output_lines[2 + i].find(str(a)) + + def test_build_target_included(self, get_connected_devices): + heading_name = "Build target(s)" + device = create_fake_device() + board = device.mbed_board + get_connected_devices.return_value = ConnectedDevices(identified_devices=[device]) + build_targets = [ + f"{board.board_type}_{board.build_variant[0]}", + f"{board.board_type}_{board.build_variant[1]}", + f"{board.board_type}", + ] + + result = CliRunner().invoke(list_connected_devices) + + output_lines = result.output.splitlines() + assert result.exit_code == 0 + assert heading_name in output_lines[0] + for i, bt in enumerate(build_targets): + assert bt in output_lines[2 + i] + assert output_lines[0].find(heading_name) == output_lines[2 + i].find(bt) + + def test_appends_identifier_when_identical_boards_found(self, get_connected_devices): + heading_name = "Build target(s)" + device_a = create_fake_device() + device_b = create_fake_device() + board = device_a.mbed_board + get_connected_devices.return_value = ConnectedDevices(identified_devices=[device_a, device_b]) + build_targets = [ + f"{board.board_type}_{board.build_variant[0]}[0]", + f"{board.board_type}_{board.build_variant[1]}[0]", + f"{board.board_type}[0]", + f"{board.board_type}_{board.build_variant[0]}[1]", + f"{board.board_type}_{board.build_variant[1]}[1]", + f"{board.board_type}[1]", + ] + + result = CliRunner().invoke(list_connected_devices) + + output_lines = result.output.splitlines() + assert result.exit_code == 0 + assert heading_name in output_lines[0] + for i, bt in enumerate(build_targets): + assert bt in output_lines[2 + i] + assert output_lines[0].find(heading_name) == output_lines[2 + i].find(bt) + + def test_sort_order(self, get_connected_devices): + devices = [ + create_fake_device( + serial_number="11111111", + board_name="AAAAAAA1FirstBoard", + build_variant=(), + mount_points=(pathlib.Path("/media/you/First"),), + ), + create_fake_device( + serial_number="22222222", + board_name="SameBoard[0]", + build_variant=(), + mount_points=(pathlib.Path("/media/you/Second"),), + ), + create_fake_device( + serial_number="33333333", + board_name="SameBoard[1]", + build_variant=(), + mount_points=(pathlib.Path("/media/you/Third"),), + ), + ] + get_connected_devices.return_value = ConnectedDevices(identified_devices=[devices[2], devices[0], devices[1]]) + + result = CliRunner().invoke(list_connected_devices, "--show-all") + output_lines = result.output.splitlines() + assert devices[0].mbed_board.board_name in output_lines[2] + assert devices[1].mbed_board.board_name in output_lines[3] + assert devices[2].mbed_board.board_name in output_lines[4] + + def test_displays_unknown_serial_port_value(self, get_connected_devices): + heading_name = "Serial port" + device = create_fake_device(serial_port=None) + get_connected_devices.return_value = ConnectedDevices(identified_devices=[device]) + + result = CliRunner().invoke(list_connected_devices) + + output_lines = result.output.splitlines() + assert result.exit_code == 0 + assert heading_name in output_lines[0] + assert output_lines[0].find(heading_name) == output_lines[2].find("") + + +class TestListConnectedDevicesJSONOutput: + @pytest.mark.parametrize("device_attr", ("serial_number", "serial_port", "mount_points", "interface_version")) + def test_device_attr_included(self, device_attr, get_connected_devices): + device = create_fake_device() + get_connected_devices.return_value = ConnectedDevices(identified_devices=[device]) + attr = getattr(device, device_attr) + # JSON encoder converts tuples to lists, so we need to convert the test data also to match + attr = [str(a) for a in attr] if isinstance(attr, tuple) else str(attr) + + result = CliRunner().invoke(list_connected_devices, "--format=json") + + assert result.exit_code == 0 + assert attr == json.loads(result.output)[0][device_attr] + + @pytest.mark.parametrize( + "board_attr", ("product_code", "board_type", "board_name", "mbed_os_support", "mbed_enabled") + ) + def test_board_attr_included(self, board_attr, get_connected_devices): + device = create_fake_device() + get_connected_devices.return_value = ConnectedDevices(identified_devices=[device]) + attr = getattr(device.mbed_board, board_attr) + # JSON encoder converts tuples to lists, so we need to convert the test data also to match + attr = [str(a) for a in attr] if isinstance(attr, tuple) else str(attr) + + result = CliRunner().invoke(list_connected_devices, "--format=json") + + assert result.exit_code == 0 + assert attr == json.loads(result.output)[0]["mbed_board"][board_attr] + + def test_build_targets_included(self, get_connected_devices): + device = create_fake_device() + board = device.mbed_board + get_connected_devices.return_value = ConnectedDevices(identified_devices=[device]) + + result = CliRunner().invoke(list_connected_devices, "--format=json") + + expected_output = [ + { + "build_targets": [ + f"{board.board_type}_{board.build_variant[0]}", + f"{board.board_type}_{board.build_variant[1]}", + f"{board.board_type}", + ], + }, + ] + + assert result.exit_code == 0 + for actual, expected in zip(json.loads(result.output), expected_output): + assert actual["mbed_board"]["build_targets"] == expected["build_targets"] + + def test_identifiers_appended_when_identical_boards_found(self, get_connected_devices): + device = create_fake_device() + device_2 = create_fake_device() + board = device.mbed_board + get_connected_devices.return_value = ConnectedDevices(identified_devices=[device, device_2]) + + result = CliRunner().invoke(list_connected_devices, "--format=json") + + expected_output = [ + { + "build_targets": [ + f"{board.board_type}_{board.build_variant[0]}[0]", + f"{board.board_type}_{board.build_variant[1]}[0]", + f"{board.board_type}[0]", + ], + }, + { + "build_targets": [ + f"{board.board_type}_{board.build_variant[0]}[1]", + f"{board.board_type}_{board.build_variant[1]}[1]", + f"{board.board_type}[1]", + ], + }, + ] + + assert result.exit_code == 0 + for actual, expected in zip(json.loads(result.output), expected_output): + assert actual["mbed_board"]["build_targets"] == expected["build_targets"] diff --git a/tools/python/python_tests/mbed_tools/cli/test_project_management.py b/tools/python/python_tests/mbed_tools/cli/test_project_management.py new file mode 100644 index 0000000000..48708e97e2 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/cli/test_project_management.py @@ -0,0 +1,113 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import pathlib + +from textwrap import dedent +from unittest import mock + +import pytest + +from click.testing import CliRunner + +from mbed_tools.cli.project_management import new, import_, deploy + + +@pytest.fixture +def mock_initialise_project(): + with mock.patch("mbed_tools.cli.project_management.initialise_project") as init_proj: + yield init_proj + + +@pytest.fixture +def mock_import_project(): + with mock.patch("mbed_tools.cli.project_management.import_project") as import_proj: + yield import_proj + + +@pytest.fixture +def mock_deploy_project(): + with mock.patch("mbed_tools.cli.project_management.deploy_project") as deploy_proj: + yield deploy_proj + + +@pytest.fixture +def mock_get_libs(): + with mock.patch("mbed_tools.cli.project_management.get_known_libs") as get_libs: + yield get_libs + + +class TestNewCommand: + def test_calls_new_function_with_correct_args(self, mock_initialise_project): + CliRunner().invoke(new, ["path", "--create-only"]) + mock_initialise_project.assert_called_once_with(pathlib.Path("path").resolve(), True) + + def test_echos_mbed_os_message_when_required(self, mock_initialise_project): + expected = ( + "Creating a new Mbed program at path " + + "'" + + str(pathlib.Path("path").resolve()) + + "'" + + ".\nDownloading mbed-os and adding it to the project.\n" + ) + + result = CliRunner().invoke(new, ["path"]) + + assert result.output == expected + + +class TestImportCommand: + def test_calls_clone_function_with_correct_args(self, mock_import_project): + CliRunner().invoke(import_, ["url", "dst"]) + mock_import_project.assert_called_once_with("url", pathlib.Path("dst"), True) + + def test_prints_fetched_libs(self, mock_import_project, mock_get_libs): + mock_methods = {"get_git_reference.return_value": mock.Mock(ref="abcdef", repo_url="https://repo/url")} + mock_get_libs.return_value = [ + mock.Mock(reference_file=pathlib.Path("test"), source_code_path=pathlib.Path("source"), **mock_methods) + ] + expected = """ + Library Name Repository URL Path Git Reference + -------------- ---------------- ------ --------------- + test https://repo/url source abcdef + """ + + runner = CliRunner() + ret = runner.invoke(import_, ["url", "dst"]) + + assert dedent(expected) in ret.output + + def test_does_not_print_libs_table_when_skip_resolve_specified(self, mock_import_project, mock_get_libs): + expected = """ + Library Name Repository URL Path Git Reference + -------------- ---------------- ------ --------------- + """ + + runner = CliRunner() + ret = runner.invoke(import_, ["url", "dst", "-s"]) + + assert dedent(expected) not in ret.output + mock_get_libs.assert_not_called() + + +class TestDeployCommand: + def test_calls_deploy_function_with_correct_args(self, mock_deploy_project): + CliRunner().invoke(deploy, ["path", "--force"]) + mock_deploy_project.assert_called_once_with(pathlib.Path("path"), True) + + def test_prints_fetched_libs(self, mock_deploy_project, mock_get_libs): + mock_methods = {"get_git_reference.return_value": mock.Mock(ref="abcdef", repo_url="https://repo/url")} + mock_get_libs.return_value = [ + mock.Mock(reference_file=pathlib.Path("test"), source_code_path=pathlib.Path("source"), **mock_methods) + ] + expected = """ + Library Name Repository URL Path Git Reference + -------------- ---------------- ------ --------------- + test https://repo/url source abcdef + """ + + runner = CliRunner() + ret = runner.invoke(deploy, ["path", "--force"]) + + assert dedent(expected) in ret.output diff --git a/tools/python/python_tests/mbed_tools/cli/test_sterm.py b/tools/python/python_tests/mbed_tools/cli/test_sterm.py new file mode 100644 index 0000000000..74ab966e3e --- /dev/null +++ b/tools/python/python_tests/mbed_tools/cli/test_sterm.py @@ -0,0 +1,104 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from unittest import mock + +import pytest + +from click.testing import CliRunner + +from mbed_tools.cli.sterm import sterm +from mbed_tools.devices.exceptions import MbedDevicesError + + +@pytest.fixture +def mock_terminal(): + with mock.patch("mbed_tools.cli.sterm.terminal") as term: + yield term + + +@pytest.fixture +def mock_get_devices(): + with mock.patch("mbed_tools.cli.sterm.get_connected_devices") as get_devs: + yield get_devs + + +@pytest.fixture +def mock_find_device(): + with mock.patch("mbed_tools.cli.sterm.find_connected_device") as find_dev: + yield find_dev + + +def test_launches_terminal_on_given_serial_port(mock_terminal): + port = "tty.1111" + CliRunner().invoke(sterm, ["--port", port]) + + mock_terminal.run.assert_called_once_with(port, 9600, echo=True) + + +def test_launches_terminal_with_given_baud_rate(mock_terminal): + port = "tty.1111" + baud = 115200 + CliRunner().invoke(sterm, ["--port", port, "--baudrate", baud]) + + mock_terminal.run.assert_called_once_with(port, baud, echo=True) + + +def test_launches_terminal_with_echo_off_when_specified(mock_terminal): + port = "tty.1111" + CliRunner().invoke(sterm, ["--port", port, "--echo", "off"]) + + mock_terminal.run.assert_called_once_with(port, 9600, echo=False) + + +def test_attempts_to_detect_device_if_no_port_given(mock_get_devices, mock_terminal): + CliRunner().invoke(sterm, []) + + mock_get_devices.assert_called_once() + + +def test_attempts_to_find_connected_target_if_target_given(mock_find_device, mock_terminal): + expected_port = "tty.k64f" + mock_find_device.return_value = mock.Mock(serial_port=expected_port, mbed_board=mock.Mock(board_type="K64F")) + + CliRunner().invoke(sterm, ["-m", "K64F"]) + + mock_terminal.run.assert_called_once_with(expected_port, 9600, echo=True) + + +def test_returns_serial_port_for_first_device_detected_if_no_target_given(mock_get_devices, mock_terminal): + expected_port = "tty.k64f" + mock_get_devices.return_value = mock.Mock( + identified_devices=[ + mock.Mock(serial_port=expected_port, mbed_board=mock.Mock(board_type="K64F")), + mock.Mock(serial_port="tty.disco", mbed_board=mock.Mock(board_type="DISCO")), + ] + ) + + CliRunner().invoke(sterm, []) + + mock_terminal.run.assert_called_once_with(expected_port, 9600, echo=True) + + +def test_returns_serial_port_for_device_if_identifier_given(mock_find_device, mock_terminal): + expected_port = "tty.k64f" + mock_find_device.return_value = mock.Mock(serial_port=expected_port, mbed_board=mock.Mock(board_type="K64F")) + + CliRunner().invoke(sterm, ["-m", "K64F[1]"]) + + mock_terminal.run.assert_called_once_with(expected_port, 9600, echo=True) + + +def test_raises_when_fails_to_find_default_device(mock_get_devices, mock_terminal): + mock_get_devices.return_value = mock.Mock(identified_devices=[]) + + with pytest.raises(MbedDevicesError): + CliRunner().invoke(sterm, [], catch_exceptions=False) + + +def test_not_run_if_target_identifier_not_int(mock_get_devices, mock_terminal): + target = "K64F[foo]" + CliRunner().invoke(sterm, ["-m", target], catch_exceptions=False) + mock_get_devices.assert_not_called() + mock_terminal.assert_not_called() diff --git a/tools/python/python_tests/mbed_tools/devices/__init__.py b/tools/python/python_tests/mbed_tools/devices/__init__.py new file mode 100644 index 0000000000..5d3f123a36 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""test module.""" diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/__init__.py b/tools/python/python_tests/mbed_tools/devices/_internal/__init__.py new file mode 100644 index 0000000000..1c0bcf306b --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Internal implementation tests.""" diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/darwin/__init__.py b/tools/python/python_tests/mbed_tools/devices/_internal/darwin/__init__.py new file mode 100644 index 0000000000..0368d0f1a0 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/darwin/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_device_detector.py b/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_device_detector.py new file mode 100644 index 0000000000..1bf3bf2e96 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_device_detector.py @@ -0,0 +1,132 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import pathlib +from unittest import TestCase, mock + +from python_tests.mbed_tools.devices.factories import CandidateDeviceFactory +from mbed_tools.devices._internal.candidate_device import CandidateDevice +from mbed_tools.devices._internal.darwin import system_profiler, diskutil, ioreg +from mbed_tools.devices._internal.darwin.device_detector import ( + DarwinDeviceDetector, + InvalidCandidateDeviceDataError, + _assemble_candidate_data, + _build_candidate, + _build_ioreg_device_name, + _get_mount_points, + _get_serial_port, +) + + +@mock.patch("mbed_tools.devices._internal.darwin.device_detector._build_candidate") +@mock.patch("mbed_tools.devices._internal.darwin.device_detector.system_profiler", spec_set=system_profiler) +class TestDarwinDeviceDetector(TestCase): + def test_find_candidates_successful_build_yields_candidate(self, system_profiler, _build_candidate): + device_data = {"some": "data"} + system_profiler.get_end_usb_devices_data.return_value = [device_data] + candidate = CandidateDeviceFactory() + _build_candidate.return_value = candidate + self.assertEqual(DarwinDeviceDetector().find_candidates(), [candidate]) + _build_candidate.assert_called_with(device_data) + + def test_find_candidates_does_not_yield_failed_candidate_builds(self, system_profiler, _build_candidate): + device_data = {"other": "data"} + system_profiler.get_end_usb_devices_data.return_value = [device_data] + _build_candidate.side_effect = InvalidCandidateDeviceDataError + self.assertEqual(DarwinDeviceDetector().find_candidates(), []) + _build_candidate.assert_called_with(device_data) + + +@mock.patch("mbed_tools.devices._internal.darwin.device_detector._assemble_candidate_data") +class TestBuildCandidateDevice(TestCase): + def test_builds_candidate_using_assembled_data(self, _assemble_candidate_data): + device_data = { + "vendor_id": "0xff", + "product_id": "0x24", + "serial_number": "123456", + "mount_points": ["/Volumes/A"], + "serial_port": "port-1", + } + _assemble_candidate_data.return_value = device_data + + self.assertEqual( + _build_candidate(device_data), CandidateDevice(**device_data), + ) + + @mock.patch("mbed_tools.devices._internal.darwin.device_detector.CandidateDevice") + def test_raises_if_candidate_cannot_be_built(self, CandidateDevice, _assemble_candidate_data): + CandidateDevice.side_effect = ValueError + with self.assertRaises(InvalidCandidateDeviceDataError): + _build_candidate({}) + + +@mock.patch("mbed_tools.devices._internal.darwin.device_detector._get_serial_port") +@mock.patch("mbed_tools.devices._internal.darwin.device_detector._get_mount_points") +class TestAssembleCandidateDeviceData(TestCase): + def test_glues_device_data_from_various_sources(self, _get_mount_points, _get_serial_port): + device_data = { + "vendor_id": "0xff", + "product_id": "0x24", + "serial_num": "123456", + } + _get_serial_port.return_value = "port-1" + _get_mount_points.return_value = ["/Volumes/A"] + + self.assertEqual( + _assemble_candidate_data(device_data), + { + "vendor_id": device_data.get("vendor_id"), + "product_id": device_data.get("product_id"), + "serial_number": device_data.get("serial_num"), + "serial_port": _get_serial_port.return_value, + "mount_points": _get_mount_points.return_value, + }, + ) + + def test_formats_vendor_id_containing_vendor_name(self, _get_mount_points, _get_serial_port): + device_data = {"vendor_id": "0x12 (SomeVendor)"} + result = _assemble_candidate_data(device_data) + self.assertEqual(result["vendor_id"], "0x12") + + +class TestGetMountPoints(TestCase): + @mock.patch("mbed_tools.devices._internal.darwin.device_detector.diskutil", spec_set=diskutil) + def test_maps_storage_identifiers_to_mount_points(self, diskutil): + device_data = {"Media": [{"bsd_name": "disk1"}, {"bsd_name": "disk2"}]} + diskutil.get_mount_point.side_effect = ["/Volumes/Disk1", "/Volumes/Disk2"] + + self.assertEqual( + _get_mount_points(device_data), (pathlib.Path("/Volumes/Disk1"), pathlib.Path("/Volumes/Disk2")) + ) + diskutil.get_mount_point.assert_has_calls([mock.call("disk1"), mock.call("disk2")]) + + +class TestGetSerialPort(TestCase): + @mock.patch("mbed_tools.devices._internal.darwin.device_detector.ioreg", spec_set=ioreg) + def test_returns_retrieved_io_dialin_device(self, ioreg): + """Given enough data, it constructs an ioreg device name and fetches serial port information.""" + device_data = { + "location_id": "0x12345 / 2", + "_name": "SomeDevice", + } + serial_port = "/dev/tty.usb1234" + ioreg.get_io_dialin_device.return_value = serial_port + ioreg_device_name = _build_ioreg_device_name( + device_name=device_data["_name"], location_id=device_data["location_id"], + ) + + self.assertEqual(_get_serial_port(device_data), serial_port) + ioreg.get_io_dialin_device.assert_called_once_with(ioreg_device_name) + + @mock.patch("mbed_tools.devices._internal.darwin.device_detector.ioreg", spec_set=ioreg) + def test_returns_none_when_cant_determine_ioreg_name(self, ioreg): + self.assertIsNone(_get_serial_port({})) + + +class TestBuildIoregDeviceName(TestCase): + def test_builds_ioreg_device_name_from_system_profiler_data(self): + self.assertEqual( + _build_ioreg_device_name(device_name="VeryNiceDevice Really", location_id="0x14420000 / 2",), + "VeryNiceDevice Really@14420000", + ) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_diskutil.py b/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_diskutil.py new file mode 100644 index 0000000000..919ac7ff61 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_diskutil.py @@ -0,0 +1,98 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import plistlib +from unittest import TestCase, mock + +from mbed_tools.devices._internal.darwin.diskutil import ( + get_all_external_disks_data, + get_all_external_volumes_data, + get_mount_point, +) + + +class TestGetAllExternalDisksData(TestCase): + @mock.patch("mbed_tools.devices._internal.darwin.diskutil.subprocess.check_output") + def test_returns_disk_data_from_diskutil_call(self, check_output): + check_output.return_value = b""" + + AllDisksAndPartitions + + le_data + + + """ + + self.assertEqual(get_all_external_disks_data(), ["le_data"]) + + +class TestGetAllExternalVolumesData(TestCase): + @mock.patch("mbed_tools.devices._internal.darwin.diskutil.get_all_external_disks_data") + def test_returns_information_about_end_devices(self, get_all_external_disks_data): + plist = b""" + + + DeviceIdentifier + disk2 + MountPoint + /Volumes/Foo + + + DeviceIdentifier + disk3 + Partitions + + + DeviceIdentifier + disk3s1 + VolumeName + EFI + + + DeviceIdentifier + disk3s2 + MountPoint + /Volumes/Untitled1 + + + DeviceIdentifier + disk3s3 + MountPoint + /Volumes/Untitled2 + + + + + """ + get_all_external_disks_data.return_value = plistlib.loads(plist) + + self.assertEqual( + get_all_external_volumes_data(), + [ + {"DeviceIdentifier": "disk2", "MountPoint": "/Volumes/Foo"}, + {"DeviceIdentifier": "disk3s1", "VolumeName": "EFI"}, + {"DeviceIdentifier": "disk3s2", "MountPoint": "/Volumes/Untitled1"}, + {"DeviceIdentifier": "disk3s3", "MountPoint": "/Volumes/Untitled2"}, + ], + ) + + +class TestGetMountPoint(TestCase): + @mock.patch("mbed_tools.devices._internal.darwin.diskutil.get_all_external_volumes_data") + def test_returns_mountpoint_if_avaiable(self, get_all_external_volumes_data): + get_all_external_volumes_data.return_value = [{"DeviceIdentifier": "disk123", "MountPoint": "/here"}] + + self.assertEqual(get_mount_point("disk123"), "/here") + + @mock.patch("mbed_tools.devices._internal.darwin.diskutil.get_all_external_volumes_data") + def test_returns_none_if_no_mountpoint(self, get_all_external_volumes_data): + get_all_external_volumes_data.return_value = [] + + self.assertIsNone(get_mount_point("disk123"), None) + + @mock.patch("mbed_tools.devices._internal.darwin.diskutil.get_all_external_volumes_data") + def test_handles_partial_data(self, get_all_external_volumes_data): + get_all_external_volumes_data.return_value = [{"DeviceIdentifier": "disk4"}] + + self.assertIsNone(get_mount_point("disk4"), None) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_ioreg.py b/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_ioreg.py new file mode 100644 index 0000000000..77277195df --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_ioreg.py @@ -0,0 +1,60 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import plistlib +from unittest import TestCase, mock + +from mbed_tools.devices._internal.darwin.ioreg import get_data, get_io_dialin_device + + +@mock.patch("mbed_tools.devices._internal.darwin.ioreg.subprocess.check_output") +class TestGetData(TestCase): + def test_returns_data_from_ioreg_call(self, check_output): + check_output.return_value = b""" + + foo + + """ + + self.assertEqual(get_data("some device"), ["foo"]) + check_output.assert_called_once_with(["ioreg", "-a", "-r", "-n", "some device", "-l"]) + + def test_handles_corrupt_data_gracefully(self, check_output): + check_output.return_value = b""" + S\xc3\xbfn\x06P\xc2\x87TT%A\t\xc2\x87 + """ + + self.assertEqual(get_data("doesn't matter"), []) + + +class TestGetIoDialinDevice(TestCase): + @mock.patch("mbed_tools.devices._internal.darwin.ioreg.get_data") + def test_identifies_nested_io_dialin_device_in_given_ioreg_data(self, get_data): + plist = b""" + + + IORegistryEntryChildren + + + + IORegistryEntryChildren + + + IORegistryEntryChildren + + + IODialinDevice + /dev/tty.usbmodem1234 + + + + + + + """ + data = plistlib.loads(plist) + get_data.return_value = data + + self.assertEqual(get_io_dialin_device("some_device"), "/dev/tty.usbmodem1234") + get_data.assert_called_once_with("some_device") diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_system_profiler.py b/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_system_profiler.py new file mode 100644 index 0000000000..e1887b199b --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/darwin/test_system_profiler.py @@ -0,0 +1,111 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import plistlib +from unittest import TestCase, mock + +from mbed_tools.devices._internal.darwin.system_profiler import ( + get_all_usb_devices_data, + get_end_usb_devices_data, +) + + +class TestGetAllUSBDevicesData(TestCase): + @mock.patch("mbed_tools.devices._internal.darwin.system_profiler.subprocess.check_output") + def test_returns_devices_list_from_system_profiler_call(self, check_output): + check_output.return_value = b""" + + + _items + + foo + bar + + + + vendor_id + hat + + + """ + + self.assertEqual(get_all_usb_devices_data(), [{"_items": ["foo", "bar"]}, {"vendor_id": "hat"}]) + + +@mock.patch("mbed_tools.devices._internal.darwin.system_profiler.get_all_usb_devices_data") +class TestGetEndUSBDevicesData(TestCase): + def test_identifies_flat_end_devices(self, get_all_usb_devices_data): + plist = b""" + + + _name + USB Receiver + vendor_id + 0xc53f + + + """ + get_all_usb_devices_data.return_value = plistlib.loads(plist) + + self.assertEqual(get_end_usb_devices_data(), [{"_name": "USB Receiver", "vendor_id": "0xc53f"}]) + + def test_identifies_nested_end_devices(self, get_all_usb_devices_data): + plist = b""" + + + _items + + + _name + USB Mouse + vendor_id + 1234 + + + + + _name + USB2.0 Hub + _items + + + _name + USB Flash + vendor_id + 5678 + + + + + """ + get_all_usb_devices_data.return_value = plistlib.loads(plist) + + result = get_end_usb_devices_data() + + self.assertIn({"_name": "USB Mouse", "vendor_id": "1234"}, result) + self.assertIn({"_name": "USB Flash", "vendor_id": "5678"}, result) + + def test_does_not_list_usb_hubs_or_buses(self, get_all_usb_devices_data): + plist = b""" + + + _name + USB31Bus + _items + + + _name + USB3.0 Hub + + + + + _name + USB20Bus + + + """ + get_all_usb_devices_data.return_value = plistlib.loads(plist) + + self.assertEqual(get_end_usb_devices_data(), []) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/linux/__init__.py b/tools/python/python_tests/mbed_tools/devices/_internal/linux/__init__.py new file mode 100644 index 0000000000..0368d0f1a0 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/linux/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/linux/test_linux_device_detector.py b/tools/python/python_tests/mbed_tools/devices/_internal/linux/test_linux_device_detector.py new file mode 100644 index 0000000000..e36fdddc48 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/linux/test_linux_device_detector.py @@ -0,0 +1,95 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Test Linux Device Detector.""" + +from collections import namedtuple +from unittest import TestCase, mock, skipIf +from mbed_tools.devices._internal.candidate_device import CandidateDevice + +try: + from mbed_tools.devices._internal.linux import device_detector + + import_succeeded = True +except ImportError: + import_succeeded = False + + +def mock_device_factory(**props): + return namedtuple("MockDevice", "properties")(props) + + +@skipIf(not import_succeeded, "Tests require package dependencies only used on Linux.") +class TestLinuxDeviceDetector(TestCase): + @mock.patch("mbed_tools.devices._internal.linux.device_detector.pyudev.Context") + @mock.patch("mbed_tools.devices._internal.linux.device_detector._find_fs_mounts_for_device") + @mock.patch("mbed_tools.devices._internal.linux.device_detector._find_serial_port_for_device") + def test_builds_list_of_candidates(self, mock_find_serial_port, mock_find_fs_mounts, mock_udev_context): + expected_serial = "2090290209" + expected_vid = "0x45" + expected_pid = "0x48" + expected_fs_mount = ["/media/user/DAPLINK"] + mock_find_serial_port.return_value = None + mock_find_fs_mounts.return_value = expected_fs_mount + devs = [ + mock_device_factory( + ID_SERIAL_SHORT=expected_serial, + ID_VENDOR_ID=expected_vid, + ID_MODEL_ID=expected_pid, + DEVNAME="/dev/sdabcde", + ) + ] + mock_udev_context().list_devices.return_value = devs + detector = device_detector.LinuxDeviceDetector() + candidates = detector.find_candidates() + self.assertEqual( + candidates, + [ + CandidateDevice( + serial_number=expected_serial, + vendor_id=expected_vid, + product_id=expected_pid, + mount_points=expected_fs_mount, + ) + ], + ) + + @mock.patch("mbed_tools.devices._internal.linux.device_detector.pyudev.Context") + @mock.patch("mbed_tools.devices._internal.linux.device_detector._find_fs_mounts_for_device") + def test_handles_filesystem_mountpoint_error_and_skips_device(self, mock_find_fs_mounts, mock_udev_context): + mock_find_fs_mounts.return_value = [] + devs = [ + mock_device_factory( + ID_SERIAL_SHORT="2090290209", ID_VENDOR_ID="0x45", ID_MODEL_ID="0x48", DEVNAME="/dev/sdabcde", + ) + ] + mock_udev_context().list_devices.return_value = devs + detector = device_detector.LinuxDeviceDetector() + candidates = detector.find_candidates() + self.assertEqual(candidates, []) + + @mock.patch("mbed_tools.devices._internal.linux.device_detector.pyudev.Context") + def test_finds_serial_port_with_matching_serial_id(self, mock_udev_context): + matching_serial = "a" + disk_device = mock_device_factory(ID_SERIAL_SHORT=matching_serial, DEVNAME="/dev/sdc") + serial_device_match = mock_device_factory(ID_SERIAL_SHORT=matching_serial, DEVNAME="/dev/ttyACM0") + serial_device_diff = mock_device_factory(ID_SERIAL_SHORT="b", DEVNAME="/dev/ttyUSB0") + mock_udev_context().list_devices.return_value = [serial_device_match, serial_device_diff] + serial_port = device_detector._find_serial_port_for_device(disk_device.properties["ID_SERIAL_SHORT"]) + self.assertEqual(serial_port, serial_device_match.properties["DEVNAME"]) + + @mock.patch("mbed_tools.devices._internal.linux.device_detector.pyudev.Context") + def test_returns_empty_none_when_no_matching_serial_id(self, mock_udev_context): + disk_device = mock_device_factory(ID_SERIAL_SHORT="i", DEVNAME="/dev/sdc") + serial_device = mock_device_factory(ID_SERIAL_SHORT="a", DEVNAME="/dev/ttyACM0") + mock_udev_context().list_devices.return_value = [serial_device] + serial_port = device_detector._find_serial_port_for_device(disk_device.properties["ID_SERIAL_SHORT"]) + self.assertEqual(serial_port, None) + + @mock.patch("mbed_tools.devices._internal.linux.device_detector.psutil") + def test_finds_fs_mountpoints_for_device_files(self, mock_psutil): + partition = namedtuple("Partition", "mountpoint,device")("/media/user/DAPLINK", "/dev/sdc") + mock_psutil.disk_partitions.return_value = [partition] + mounts = device_detector._find_fs_mounts_for_device("/dev/sdc") + self.assertEqual(str(mounts[0]), partition.mountpoint) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/test_candidate_device.py b/tools/python/python_tests/mbed_tools/devices/_internal/test_candidate_device.py new file mode 100644 index 0000000000..891d57e292 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/test_candidate_device.py @@ -0,0 +1,69 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import pathlib +from unittest import TestCase +from mbed_tools.devices._internal.candidate_device import CandidateDevice + + +def build_candidate_data(**overrides): + defaults = { + "product_id": "0x1234", + "vendor_id": "0x5678", + "mount_points": (pathlib.Path("./foo"),), + "serial_number": "qwer", + "serial_port": "COM1", + } + return {**defaults, **overrides} + + +class TestCandidateDevice(TestCase): + def test_produces_a_valid_candidate(self): + candidate_data = build_candidate_data() + candidate = CandidateDevice(**candidate_data) + + self.assertEqual(candidate.product_id, candidate_data["product_id"]) + self.assertEqual(candidate.vendor_id, candidate_data["vendor_id"]) + self.assertEqual(candidate.mount_points, candidate_data["mount_points"]) + self.assertEqual(candidate.serial_number, candidate_data["serial_number"]) + self.assertEqual(candidate.serial_port, candidate_data["serial_port"]) + + def test_raises_when_product_id_is_empty(self): + candidate_data = build_candidate_data(product_id="") + with self.assertRaisesRegex(ValueError, "product_id"): + CandidateDevice(**candidate_data) + + def test_raises_when_product_id_is_not_hex(self): + candidate_data = build_candidate_data(product_id="TEST") + with self.assertRaisesRegex(ValueError, "product_id"): + CandidateDevice(**candidate_data) + + def test_prefixes_product_id_hex_value(self): + candidate_data = build_candidate_data(product_id="ff01") + candidate = CandidateDevice(**candidate_data) + self.assertEqual(candidate.product_id, "0xff01") + + def test_raises_when_vendor_id_is_empty(self): + candidate_data = build_candidate_data(vendor_id="") + with self.assertRaisesRegex(ValueError, "vendor_id"): + CandidateDevice(**candidate_data) + + def test_raises_when_vendor_id_is_not_hex(self): + candidate_data = build_candidate_data(vendor_id="TEST") + with self.assertRaisesRegex(ValueError, "vendor_id"): + CandidateDevice(**candidate_data) + + def test_prefixes_vendor_id_hex_value(self): + candidate_data = build_candidate_data(vendor_id="cbad") + candidate = CandidateDevice(**candidate_data) + self.assertEqual(candidate.vendor_id, "0xcbad") + + def test_raises_when_mount_points_are_empty(self): + with self.assertRaisesRegex(ValueError, "mount_points"): + CandidateDevice(product_id="1234", vendor_id="1234", mount_points=[], serial_number="1234") + + def test_raises_when_serial_number_is_empty(self): + candidate_data = build_candidate_data(serial_number="") + with self.assertRaisesRegex(ValueError, "serial_number"): + CandidateDevice(**candidate_data) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/test_detect_candidate_devices.py b/tools/python/python_tests/mbed_tools/devices/_internal/test_detect_candidate_devices.py new file mode 100644 index 0000000000..a8af0e7602 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/test_detect_candidate_devices.py @@ -0,0 +1,50 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import pytest +from unittest import mock + +from python_tests.mbed_tools.devices.markers import windows_only, darwin_only, linux_only +from mbed_tools.devices._internal.base_detector import DeviceDetector +from mbed_tools.devices.exceptions import UnknownOSError +from mbed_tools.devices._internal.detect_candidate_devices import ( + detect_candidate_devices, + _get_detector_for_current_os, +) + + +class TestDetectCandidateDevices: + @mock.patch("mbed_tools.devices._internal.detect_candidate_devices._get_detector_for_current_os") + def test_returns_candidates_using_os_specific_detector(self, _get_detector_for_current_os): + detector = mock.Mock(spec_set=DeviceDetector) + _get_detector_for_current_os.return_value = detector + assert detect_candidate_devices() == detector.find_candidates.return_value + + +class TestGetDetectorForCurrentOS: + @windows_only + def test_windows_uses_correct_module(self): + from mbed_tools.devices._internal.windows.device_detector import WindowsDeviceDetector + + assert isinstance(_get_detector_for_current_os(), WindowsDeviceDetector) + + @darwin_only + def test_darwin_uses_correct_module(self): + from mbed_tools.devices._internal.darwin.device_detector import DarwinDeviceDetector + + assert isinstance(_get_detector_for_current_os(), DarwinDeviceDetector) + + @linux_only + def test_linux_uses_correct_module(self): + from mbed_tools.devices._internal.linux.device_detector import LinuxDeviceDetector + + assert isinstance(_get_detector_for_current_os(), LinuxDeviceDetector) + + @mock.patch("platform.system") + def test_raises_when_os_is_unknown(self, platform_system): + os_name = "SomethingNobodyUses" + platform_system.return_value = os_name + + with pytest.raises(UnknownOSError): + _get_detector_for_current_os() diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/test_file_parser.py b/tools/python/python_tests/mbed_tools/devices/_internal/test_file_parser.py new file mode 100644 index 0000000000..4872623b2d --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/test_file_parser.py @@ -0,0 +1,247 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import pathlib + +from unittest import mock + +import pytest + +from mbed_tools.devices._internal.file_parser import OnlineId, read_device_files + + +class TestReadDeviceFiles: + def test_finds_daplink_compatible_device_files(self, tmp_path): + details = pathlib.Path(tmp_path, "details.txt") + htm = pathlib.Path(tmp_path, "mbed.htm") + htm.write_text("code=2222") + details.write_text("Version: 2") + + info = read_device_files([tmp_path]) + + assert info.product_code is not None + assert info.interface_details is not None + + def test_finds_jlink_device_files(self, tmp_path): + segger_html = pathlib.Path(tmp_path, "Segger.html") + board_html = pathlib.Path(tmp_path, "Board.html") + segger_html.write_text(build_segger_html()) + board_html.write_text(build_board_html()) + + info = read_device_files([tmp_path]) + + assert info.online_id is not None + assert info.interface_details is not None + + def test_warns_if_no_device_files_found(self, caplog, tmp_path): + read_device_files([tmp_path]) + + assert str(tmp_path) in caplog.text + + def test_skips_hidden_files(self, caplog, tmp_path): + auth = "101000000000000000000002F7F35E602eeb0bb9b632205c51f6c357aeee7bc9" + file_contents = ( + '' + ) + pathlib.Path(tmp_path, "._MBED.HTM").write_text(file_contents) + pathlib.Path(tmp_path, "._DETAILS.TXT").write_text("Version: 2222") + + assert read_device_files([tmp_path]).product_code is None + assert not read_device_files([tmp_path]).interface_details + + def test_handles_os_error_with_warning(self, tmp_path, caplog, monkeypatch): + bad_htm = pathlib.Path(tmp_path, "MBED.HTM") + bad_htm.touch() + monkeypatch.setattr(pathlib.Path, "read_text", mock.Mock(side_effect=OSError)) + + read_device_files([tmp_path]) + assert str(bad_htm) in caplog.text + + +class TestExtractProductCodeFromHtm: + def test_reads_product_code_from_code_attribute(self, tmp_path): + code = "02400201B80ECE4A45F033F2" + file_contents = f'' + pathlib.Path(tmp_path, "MBED.HTM").write_text(file_contents) + + assert read_device_files([tmp_path]).product_code == code[:4] + + def test_reads_product_code_from_auth_attribute(self, tmp_path): + auth = "101000000000000000000002F7F35E602eeb0bb9b632205c51f6c357aeee7bc9" + file_contents = ( + '' + ) + pathlib.Path(tmp_path, "MBED.HTM").write_text(file_contents) + + assert read_device_files([tmp_path]).product_code == auth[:4] + + def test_none_if_no_product_code(self, tmp_path): + file_contents = '' + pathlib.Path(tmp_path, "MBED.HTM").write_text(file_contents) + + assert read_device_files([tmp_path]).product_code is None + + def test_extracts_first_product_code_found(self, tmp_path): + auth = "101000000000000000000002F7F35E602eeb0bb9b632205c51f6c357aeee7bc9" + file_contents_1 = ( + '' + ) + code = "02400201B80ECE4A45F033F2" + file_contents_2 = f'' + directory_1 = pathlib.Path(tmp_path, "test-1") + directory_1.mkdir() + directory_2 = pathlib.Path(tmp_path, "test-2") + directory_2.mkdir() + pathlib.Path(directory_1, "mbed.htm").write_text(file_contents_1) + pathlib.Path(directory_2, "mbed.htm").write_text(file_contents_2) + + result = read_device_files([directory_1, directory_2]) + + assert result.product_code == auth[:4] + + +class TestExtractOnlineIDFromHTM: + def test_reads_online_id_from_url(self, tmp_path): + url = "https://os.mbed.com/platforms/THIS-IS_a_SLUG_123/" + file_contents = f"window.location.replace({url});" + pathlib.Path(tmp_path, "MBED.HTM").write_text(file_contents) + + assert read_device_files([tmp_path]).online_id == OnlineId(target_type="platform", slug="THIS-IS_a_SLUG_123") + + def test_none_if_not_found(self, tmp_path): + file_contents = "window.location.replace(https://os.mbed.com/about);" + pathlib.Path(tmp_path, "MBED.HTM").write_text(file_contents) + + assert read_device_files([tmp_path]).online_id is None + + +class TestExtractsJlinkData: + def test_reads_board_slug(self, tmp_path): + board_html = pathlib.Path(tmp_path, "Board.html") + board_html.write_text(build_board_html("http://test.com/test/slug")) + + info = read_device_files([tmp_path]) + + assert info.online_id == OnlineId(target_type="jlink", slug="slug") + + def test_reads_board_slug_ignore_extension(self, tmp_path): + board_html = pathlib.Path(tmp_path, "Board.html") + board_html.write_text(build_board_html("http://test.com/slug.html")) + + info = read_device_files([tmp_path]) + + assert info.online_id == OnlineId(target_type="jlink", slug="slug") + + def test_id_none_if_no_board_slug(self, tmp_path): + board_html = pathlib.Path(tmp_path, "Board.html") + board_html.write_text(build_board_html("http://test.com")) + + info = read_device_files([tmp_path]) + + assert info.online_id is None + + def test_reads_segger_slug(self, tmp_path): + segger_html = pathlib.Path(tmp_path, "Segger.html") + segger_html.write_text(build_segger_html("variant")) + + info = read_device_files([tmp_path]) + + assert info.interface_details.get("Version") == "variant" + + def test_interface_empty_if_not_found(self, tmp_path): + board_html = pathlib.Path(tmp_path, "Board.html") + board_html.write_text("") + + info = read_device_files([tmp_path]) + + assert info.interface_details == {} + + +# Helpers to build test data +def build_short_details_txt(version="0226", build="Aug 24 2015 17:06:30", commit_sha="27a2367", local_mods="Yes"): + return ( + f"""Version: {version} +Build: {build} +Git Commit SHA: {commit_sha} +Git Local mods: {local_mods} +""", + {"Version": version, "Build": build, "Git Commit SHA": commit_sha, "Git Local mods": local_mods}, + ) + + +def build_long_details_txt( + interface_version="0226", + uid="0240000029164e45002f0012706e0006f301000097969900", + hif_id="97969900", + auto_reset="0", + auto_allow="0", + daplink_mode="Interface", + commit_sha="c76899838", + local_mods="0", + usb_ifaces="MSD, CDC, HID", + iface_crc="0x26764ebf", +): + return ( + f"""# DAPLink Firmware - see https://mbed.com/daplink +Unique ID: {uid} +HIF ID: {hif_id} +Auto Reset: {auto_reset} +Automation allowed: {auto_allow} +Daplink Mode: {daplink_mode} +Interface Version: {interface_version} +Git SHA: {commit_sha} +Local Mods: {local_mods} +USB Interfaces: {usb_ifaces} +Interface CRC: {iface_crc} +""", + { + "Unique ID": uid, + "HIF ID": hif_id, + "Auto Reset": auto_reset, + "Automation allowed": auto_allow, + "Daplink Mode": daplink_mode, + "Version": interface_version, + "Git SHA": commit_sha, + "Local Mods": local_mods, + "USB Interfaces": usb_ifaces, + "Interface CRC": iface_crc, + }, + ) + + +def build_segger_html(model="j-link-ob"): + return ( + """""" + """""" + ) + + +def build_board_html(target_url="http://www.nxp.com/FRDM-K64F"): + return f"""""" + + +class TestReadDetailsTxt: + @pytest.mark.parametrize( + "content, expected", + ( + build_short_details_txt(), + build_short_details_txt(version="0777", commit_sha="99789s", local_mods="No"), + build_long_details_txt(), + ("", {}), + ("\n", {}), + ("blablablablaandbla", {}), + ("blablabla\nblaandbla\nversion : 2\n\n", {"version": "2"}), + ), + ids=("short", "short2", "long", "empty", "newline", "nosep", "multiline"), + ) + def test_parses_details_txt(self, content, expected, tmp_path): + details_file_path = pathlib.Path(tmp_path, "DETAILS.txt") + details_file_path.write_text(content) + + result = read_device_files([tmp_path]).interface_details + assert result == expected diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/test_resolve_board.py b/tools/python/python_tests/mbed_tools/devices/_internal/test_resolve_board.py new file mode 100644 index 0000000000..05265c884e --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/test_resolve_board.py @@ -0,0 +1,134 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from unittest import mock + +import pytest + +from mbed_tools.targets.exceptions import UnknownBoard, MbedTargetsError + +from mbed_tools.devices._internal.file_parser import OnlineId, DeviceFileInfo +from mbed_tools.devices._internal.resolve_board import NoBoardForCandidate, resolve_board, ResolveBoardError + + +@pytest.fixture +def get_board_by_product_code_mock(): + with mock.patch("mbed_tools.devices._internal.resolve_board.get_board_by_product_code", autospec=True) as gbp: + yield gbp + + +@pytest.fixture +def get_board_by_jlink_slug_mock(): + with mock.patch("mbed_tools.devices._internal.resolve_board.get_board_by_jlink_slug", autospec=True) as gbp: + yield gbp + + +@pytest.fixture +def get_board_by_online_id_mock(): + with mock.patch("mbed_tools.devices._internal.resolve_board.get_board_by_online_id", autospec=True) as gbp: + yield gbp + + +class TestResolveBoardUsingProductCodeFromHTM: + def test_returns_resolved_target(self, get_board_by_product_code_mock): + dev_info = DeviceFileInfo("0123", None, None) + + subject = resolve_board(product_code=dev_info.product_code) + + assert subject == get_board_by_product_code_mock.return_value + get_board_by_product_code_mock.assert_called_once_with(dev_info.product_code) + + def test_raises_when_board_not_found(self, get_board_by_product_code_mock): + get_board_by_product_code_mock.side_effect = UnknownBoard + + with pytest.raises(NoBoardForCandidate): + resolve_board(product_code="0123") + + def test_raises_when_database_lookup_fails(self, get_board_by_product_code_mock, caplog): + get_board_by_product_code_mock.side_effect = MbedTargetsError + product_code = "0123" + + with pytest.raises(ResolveBoardError): + resolve_board(product_code=product_code) + + assert product_code in caplog.text + assert caplog.records[-1].levelname == "ERROR" + + +class TestResolveBoardUsingOnlineIdFromHTM: + def test_returns_resolved_board(self, get_board_by_online_id_mock): + online_id = OnlineId(target_type="hat", slug="boat") + + subject = resolve_board(online_id=online_id) + + assert subject == get_board_by_online_id_mock.return_value + get_board_by_online_id_mock.assert_called_once_with(target_type=online_id.target_type, slug=online_id.slug) + + def test_raises_when_board_not_found(self, get_board_by_online_id_mock): + get_board_by_online_id_mock.side_effect = UnknownBoard + + with pytest.raises(NoBoardForCandidate): + resolve_board(online_id=OnlineId(target_type="hat", slug="boat")) + + def test_raises_when_database_lookup_fails(self, get_board_by_online_id_mock, caplog): + get_board_by_online_id_mock.side_effect = MbedTargetsError + online_id = OnlineId(target_type="hat", slug="boat") + + with pytest.raises(ResolveBoardError): + resolve_board(online_id=online_id) + + assert repr(online_id) in caplog.text + assert caplog.records[-1].levelname == "ERROR" + + +class TestResolveBoardUsingSlugFromJlink: + def test_returns_resolved_board(self, get_board_by_jlink_slug_mock): + online_id = OnlineId("jlink", "test-board") + + subject = resolve_board(online_id=online_id) + + assert subject == get_board_by_jlink_slug_mock.return_value + get_board_by_jlink_slug_mock.assert_called_once_with(online_id.slug) + + def test_raises_when_board_not_found(self, get_board_by_jlink_slug_mock): + get_board_by_jlink_slug_mock.side_effect = UnknownBoard + online_id = OnlineId("jlink", "test-board") + + with pytest.raises(NoBoardForCandidate): + resolve_board(online_id=online_id) + + def test_raises_when_database_lookup_fails(self, get_board_by_jlink_slug_mock, caplog): + get_board_by_jlink_slug_mock.side_effect = MbedTargetsError + online_id = OnlineId("jlink", "test-board") + + with pytest.raises(ResolveBoardError): + resolve_board(online_id=online_id) + + assert repr(online_id) in caplog.text + assert caplog.records[-1].levelname == "ERROR" + + +class TestResolveBoardUsingProductCodeFromSerial: + def test_resolves_board_using_product_code_when_available(self, get_board_by_product_code_mock): + serial_number = "0A9KJFKD0993WJKUFS0KLJ329090" + subject = resolve_board(serial_number=serial_number) + + assert subject == get_board_by_product_code_mock.return_value + get_board_by_product_code_mock.assert_called_once_with(serial_number[:4]) + + def test_raises_when_board_not_found(self, get_board_by_product_code_mock): + get_board_by_product_code_mock.side_effect = UnknownBoard + + with pytest.raises(NoBoardForCandidate): + resolve_board(serial_number="0") + + def test_raises_when_database_lookup_fails(self, get_board_by_product_code_mock, caplog): + get_board_by_product_code_mock.side_effect = MbedTargetsError + serial_number = "0123456" + + with pytest.raises(ResolveBoardError): + resolve_board(serial_number=serial_number) + + assert serial_number[:4] in caplog.text + assert caplog.records[-1].levelname == "ERROR" diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/windows/__init__.py b/tools/python/python_tests/mbed_tools/devices/_internal/windows/__init__.py new file mode 100644 index 0000000000..b6786aa4ad --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/windows/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for Windows implementation.""" diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_component_descriptor_utils.py b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_component_descriptor_utils.py new file mode 100644 index 0000000000..f95ef5f164 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_component_descriptor_utils.py @@ -0,0 +1,45 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import unittest +from mbed_tools.devices._internal.windows.component_descriptor_utils import ( + is_undefined_data_object, + is_undefined_value, + data_object_to_dict, + UNKNOWN_VALUE, +) +from collections import namedtuple +import random + + +def generate_valid_values(): + return random.choice(["a test", 4646.454, 54, True]) + + +def generate_undefined_values(): + return random.choice([0, None, False, UNKNOWN_VALUE]) + + +class TestUtilities(unittest.TestCase): + def test_is_value_undefined(self): + self.assertTrue(is_undefined_value(generate_undefined_values())) + self.assertFalse(is_undefined_value(generate_valid_values())) + + def test_is_data_object_undefined(self): + field_number = 30 + DataObjectType = namedtuple("data_object_example", [f"field{i}" for i in range(0, field_number)]) + test1 = DataObjectType(*[generate_undefined_values() for i in range(0, field_number)]) + self.assertTrue(is_undefined_data_object(test1)) + test2 = DataObjectType(*[generate_valid_values() for i in range(0, field_number)]) + self.assertFalse(is_undefined_data_object(test2)) + + def test_to_dict(self): + field_number = 30 + DataObjectType = namedtuple("data_object_example", [f"field{i}" for i in range(0, field_number)]) + expected_dictionary = { + f"field{i}": random.choice([generate_valid_values(), generate_undefined_values()]) + for i in range(0, field_number) + } + test = DataObjectType(**expected_dictionary) + self.assertDictEqual(data_object_to_dict(test), expected_dictionary) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_disk_data_aggregation.py b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_disk_data_aggregation.py new file mode 100644 index 0000000000..91bfd5d13b --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_disk_data_aggregation.py @@ -0,0 +1,214 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import unittest +from python_tests.mbed_tools.devices.markers import windows_only + + +@windows_only +class TestDiskDataAggregrator(unittest.TestCase): + def test_data_aggregation(self): + from mbed_tools.devices._internal.windows.disk_drive import DiskDrive + from mbed_tools.devices._internal.windows.disk_partition import DiskPartition + from mbed_tools.devices._internal.windows.logical_disk import LogicalDisk + from mbed_tools.devices._internal.windows.volume_set import VolumeInformation, DriveType + from mbed_tools.devices._internal.windows.disk_aggregation import ( + AggregatedDiskData, + DiskDataAggregator, + WindowsUID, + ) + + disk1 = DiskDrive() + disk1.set_data_values( + dict( + Availability=None, + BytesPerSector=512, + Capabilities=(3, 4, 7), + CapabilityDescriptions=("Random Access", "Supports Writing", "Supports Removable Media"), + Caption="MBED VFS USB Device", + CompressionMethod=None, + ConfigManagerErrorCode=0, + ConfigManagerUserConfig=False, + CreationClassName="Win32_DiskDrive", + DefaultBlockSize=None, + Description="Disk drive", + DeviceID="\\\\.\\PHYSICALDRIVE1", + ErrorCleared=None, + ErrorDescription=None, + ErrorMethodology=None, + FirmwareRevision="0.1 ", + Index=1, + InstallDate=None, + InterfaceType="USB", + LastErrorCode=None, + Manufacturer="(Standard disk drives)", + MaxBlockSize=None, + MaxMediaSize=None, + MediaLoaded=True, + MediaType="Removable Media", + MinBlockSize=None, + Model="MBED VFS USB Device", + Name="\\\\.\\PHYSICALDRIVE1", + NeedsCleaning=None, + NumberOfMediaSupported=None, + Partitions=1, + PNPDeviceID="USBSTOR\\DISK&VEN_MBED&PROD_VFS&REV_0.1\\4454646", + PowerManagementCapabilities=None, + PowerManagementSupported=None, + SCSIBus=0, + SCSILogicalUnit=0, + SCSIPort=0, + SCSITargetId=0, + SectorsPerTrack=63, + SerialNumber="0240000034544e45001a00018aa900292011000097969900", + Signature=1, + Size="65802240", + Status="OK", + StatusInfo=None, + SystemCreationClassName="Win32_ComputerSystem", + SystemName="E112100", + TotalCylinders="8", + TotalHeads=255, + TotalSectors="128520", + TotalTracks="2040", + TracksPerCylinder=255, + ) + ) + partition1 = DiskPartition() + partition1.set_data_values( + dict( + AdditionalAvailability=None, + Availability=None, + PowerManagementCapabilities=None, + IdentifyingDescriptions=None, + MaxQuiesceTime=None, + OtherIdentifyingInfo=None, + StatusInfo=None, + PowerOnHours=None, + TotalPowerOnHours=None, + Access=None, + BlockSize="512", + Bootable=False, + BootPartition=False, + Caption="Disk #1, Partition #0", + ConfigManagerErrorCode=None, + ConfigManagerUserConfig=None, + CreationClassName="Win32_DiskPartition", + Description="16-bit FAT", + DeviceID="Disk #1, Partition #0", + DiskIndex=1, + ErrorCleared=None, + ErrorDescription=None, + ErrorMethodology=None, + HiddenSectors=None, + Index=0, + InstallDate=None, + LastErrorCode=None, + Name="Disk #1, Partition #0", + NumberOfBlocks="131200", + PNPDeviceID=None, + PowerManagementSupported=None, + PrimaryPartition=True, + Purpose=None, + RewritePartition=None, + Size="67174400", + StartingOffset="0", + Status=None, + SystemCreationClassName="Win32_ComputerSystem", + SystemName="E112100", + Type="16-bit FAT", + ) + ) + logical_disk1 = LogicalDisk() + logical_disk1.set_data_values( + dict( + Access=0, + Availability=None, + BlockSize=None, + Caption="F:", + ConfigManagerErrorCode=None, + ConfigManagerUserConfig=None, + CreationClassName="Win32_LogicalDisk", + Description="Removable Disk", + DeviceID="F:", + ErrorCleared=None, + ErrorDescription=None, + ErrorMethodology=None, + FreeSpace="67096576", + InstallDate=None, + LastErrorCode=None, + Name="F:", + NumberOfBlocks=None, + PNPDeviceID=None, + PowerManagementCapabilities=None, + PowerManagementSupported=None, + Purpose=None, + Size="67104768", + Status=None, + StatusInfo=None, + SystemCreationClassName="Win32_ComputerSystem", + SystemName="E112100", + ) + ) + disk_drives = {1: disk1} + disk_partitions = {"Disk #1, Partition #0": partition1} + logical_partition_relationships = {"F:": "Disk #1, Partition #0"} + volume_information = VolumeInformation( + Name="DAPLINK", + SerialNumber=654449012, + MaxComponentLengthOfAFileName=255, + SysFlags=131590, + FileSystem="FAT", + UniqueName="\\\\?\\Volume{d0613192-49b4-11ea-99e5-c85b76dfd333}\\", + DriveType=DriveType.DRIVE_REMOVABLE, + ) + + # TEST aggregation + expected_aggregated_object = AggregatedDiskData() + expected_aggregated_object.set_data_values( + dict( + uid=WindowsUID(uid="4454646", raw_uid=None, serial_number=None), + label="F:", + description="Removable Disk", + free_space="67096576", + size="67104768", + partition_name="Disk #1, Partition #0", + partition_type="16-bit FAT", + volume_information=volume_information, + caption="MBED VFS USB Device", + physical_disk_name="\\\\.\\PHYSICALDRIVE1", + model="MBED VFS USB Device", + interface_type="USB", + media_type="Removable Media", + manufacturer="(Standard disk drives)", + serial_number="0240000034544e45001a00018aa900292011000097969900", + status="OK", + pnp_device_id="USBSTOR\\DISK&VEN_MBED&PROD_VFS&REV_0.1\\4454646", + ) + ) + + self.assertTupleEqual( + DiskDataAggregator( + physical_disks=disk_drives, + partition_disks=disk_partitions, + logical_partition_relationships=logical_partition_relationships, + lookup_volume_information=lambda ld: volume_information, + ) + .aggregate(logical_disk1) + .to_tuple(), + expected_aggregated_object.to_tuple(), + ) + + def test_system_disk_data(self): + # On Windows, C: is the default primary drive and hence we should always get information about it. + from mbed_tools.devices._internal.windows.disk_aggregation import SystemDiskInformation + from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader + + disks = SystemDiskInformation(SystemDataLoader()) + c_data = disks.get_disk_information_by_label("c:") + self.assertFalse(c_data.is_undefined) + uid = c_data.uid + self.assertIsNotNone(uid) + labels = [d.label.lower() for d in disks.get_disk_information(uid)] + self.assertTrue("c:" in labels) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_disk_identifier.py b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_disk_identifier.py new file mode 100644 index 0000000000..c03143d00c --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_disk_identifier.py @@ -0,0 +1,83 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import unittest +from python_tests.mbed_tools.devices.markers import windows_only + + +@windows_only +class TestDiskUid(unittest.TestCase): + def test_disk_uid_parsing(self): + from mbed_tools.devices._internal.windows.disk_drive import Win32DiskIdParser, WindowsUID + + # Tests the parsing of real data. + # ST Link + pnpid1 = "USBSTOR\\DISK&VEN_MBED&PROD_MICROCONTROLLER&REV_1.0\\9&175DDF0B&0&066DFF555654725187095153&0" + serial_number1 = "066DFF555654725187095153" + self.assertEqual( + Win32DiskIdParser().parse(pnpid=pnpid1, serial_number=serial_number1), + WindowsUID( + uid="066dff555654725187095153", + raw_uid="9&175DDF0B&0&066DFF555654725187095153&0", + serial_number="066DFF555654725187095153", + ), + ) + pnpid2 = "USBSTOR\\DISK&VEN_MBED&PROD_MICROCONTROLLER&REV_1.0\\9&3849C7A8&0&0672FF574953867567051035&0" + serial_number2 = "0672FF574953867567051035" + self.assertEqual( + Win32DiskIdParser().parse(pnpid=pnpid2, serial_number=serial_number2), + WindowsUID( + uid="0672ff574953867567051035", + raw_uid="9&3849C7A8&0&0672FF574953867567051035&0", + serial_number="0672FF574953867567051035", + ), + ) + # System disk + pnpid3 = "SCSI\\DISK&VEN_SAMSUNG&PROD_MZNLN512HMJP-000\\4&143821B1&0&000100" + serial_number3 = "S2XANX0J211020" + self.assertEqual( + Win32DiskIdParser().parse(pnpid=pnpid3, serial_number=serial_number3), + WindowsUID(uid="4&143821b1&0&000100", raw_uid="4&143821B1&0&000100", serial_number="S2XANX0J211020"), + ) + # Daplink + pnpid4 = "USBSTOR\\DISK&VEN_MBED&PROD_VFS&REV_0.1\\0240000034544E45001A00018AA900292011000097969900&0" + serial_number4 = "0240000034544E45001A00018AA900292011000097969900" + self.assertEqual( + Win32DiskIdParser().parse(pnpid=pnpid4, serial_number=serial_number4), + WindowsUID( + uid="0240000034544e45001a00018aa900292011000097969900", + raw_uid="0240000034544E45001A00018AA900292011000097969900&0", + serial_number="0240000034544E45001A00018AA900292011000097969900", + ), + ) + # J Link + pnpid5 = "USBSTOR\\DISK&VEN_SEGGER&PROD_MSD_VOLUME&REV_1.00\\9&DBDECF6&0&000440112138&0" + serial_number5 = " 134657890" + self.assertEqual( + Win32DiskIdParser().parse(pnpid=pnpid5, serial_number=serial_number5), + WindowsUID( + uid="000440112138", + raw_uid="9&DBDECF6&0&000440112138&0", + serial_number=" 134657890", + ), + ) + + def test_uid_linking_between_usb_and_disk(self): + from mbed_tools.devices._internal.windows.usb_device_identifier import UsbIdentifier, WindowsUID + + disk_uid = WindowsUID( + uid="000440112138", + raw_uid="9&DBDECF6&0&000440112138&0", + serial_number=" 134657890", + ) + + usb_uid = UsbIdentifier( + UID=WindowsUID(uid="000440112138", raw_uid="000440112138", serial_number="8&2f125ec6&0"), + VID="1366", + PID="1015", + REV=None, + MI=None, + ) + self.assertEqual(disk_uid.presumed_serial_number, usb_uid.uid.presumed_serial_number) + self.assertEqual(usb_uid.uid.presumed_serial_number, disk_uid.presumed_serial_number) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_serial_port.py b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_serial_port.py new file mode 100644 index 0000000000..5be4618a13 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_serial_port.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from python_tests.mbed_tools.devices.markers import windows_only +from unittest import TestCase +import random + + +@windows_only +class TestSerialPort(TestCase): + def test_retrieve_port_name(self): + from mbed_tools.devices._internal.windows.serial_port import parse_caption + from mbed_tools.devices._internal.windows.component_descriptor import UNKNOWN_VALUE + + self.assertEqual(UNKNOWN_VALUE, parse_caption(UNKNOWN_VALUE)) + self.assertEqual("COM13", parse_caption("Serial Port for Barcode Scanner (COM13)")) + port_name = f"COM{random.choice(range(0, 1000))}" + self.assertEqual(port_name, parse_caption(f"mbed Serial Port ({port_name})")) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_system_data_loader.py b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_system_data_loader.py new file mode 100644 index 0000000000..7844d2dff1 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_system_data_loader.py @@ -0,0 +1,26 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import unittest +from unittest.mock import patch + +from python_tests.mbed_tools.devices.markers import windows_only + + +@windows_only +class TestSystemDataLoader(unittest.TestCase): + @patch("mbed_tools.devices._internal.windows.system_data_loader.load_all") + def test_system_data_load(self, load_all): + from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader, SYSTEM_DATA_TYPES + + def mock_system_element_fetcher(arg): + return (arg, list()) + + load_all.side_effect = mock_system_element_fetcher + + loader = SystemDataLoader() + for type in SYSTEM_DATA_TYPES: + self.assertIsNotNone(loader.get_system_data(type)) + self.assertTrue(isinstance(loader.get_system_data(type), list)) + load_all.assert_called() diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_usb_device_identifier.py b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_usb_device_identifier.py new file mode 100644 index 0000000000..65d3b186ab --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_usb_device_identifier.py @@ -0,0 +1,126 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from unittest import TestCase +from mbed_tools.devices._internal.windows.component_descriptor_utils import data_object_to_dict +from mbed_tools.devices._internal.windows.windows_identifier import WindowsUID +from python_tests.mbed_tools.devices._internal.windows.test_windows_identifier import generateUID +from python_tests.mbed_tools.devices.markers import windows_only + + +@windows_only +class TestUsbDeviceId(TestCase): + """Tests based on https://docs.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers.""" + + def test_single_interface_usb_device(self): + from mbed_tools.devices._internal.windows.usb_device_identifier import parse_device_id + + self.assertTrue(parse_device_id("").is_undefined) + self.assertTrue(parse_device_id(None).is_undefined) + self.assertTrue(parse_device_id("4&38EF038C&0&0").is_undefined) + self.assertFalse(parse_device_id("USB\\4&38EF038C&0&0").is_undefined) + self.assertEqual( + parse_device_id("USB\\4&38EF038C&0&0").uid, + WindowsUID(uid="4&38ef038c&0", raw_uid="4&38EF038C&0&0", serial_number=None), + ) + self.assertEqual(parse_device_id("USB\\ROOT_HUB30\\4&38EF038C&0&0").uid.raw_uid, "4&38EF038C&0&0") + self.assertEqual( + parse_device_id("USB\\VID_2109&PID_2812\\6&38E4CCB6&0&4").uid, + WindowsUID(uid="6&38e4ccb6&0", raw_uid="6&38E4CCB6&0&4", serial_number=None), + ) + self.assertEqual(parse_device_id("USB\\VID_2109&PID_2812\\6&38E4CCB6&0&4").PID, "2812") + self.assertEqual(parse_device_id("USB\\VID_2109&PID_2812\\6&38E4CCB6&0&4").VID, "2109") + self.assertEqual(parse_device_id("USB\\VID_2109&PID_2812&REV_1100\\6&38E4CCB6&0&4").REV, "1100") + + self.assertGreaterEqual( + data_object_to_dict(parse_device_id("USB\\VID_2109&PID_2812&REV_1100\\6&38E4CCB6&0&4")).items(), + { + "VID": "2109", + "PID": "2812", + "REV": "1100", + "UID": WindowsUID(uid="6&38e4ccb6&0", raw_uid="6&38E4CCB6&0&4", serial_number=None), + }.items(), + ) + self.assertEqual(parse_device_id("USB\\4&38EF038C&0&0").product_id, "") + self.assertEqual(parse_device_id("USB\\4&38EF038C&0&0").vendor_id, "") + + def test_multiple_interface_usb_device(self): + from mbed_tools.devices._internal.windows.usb_device_identifier import parse_device_id + + self.assertEqual( + parse_device_id("USB\\VID_0D28&PID_0204&MI_00\\0240000034544E45001A00018AA900292011000097969900").uid.uid, + "0240000034544e45001a00018aa900292011000097969900", + ) + self.assertEqual( + parse_device_id( + "USB\\VID_0D28&PID_0204&MI_00\\0240000034544E45001A00018AA900292011000097969900" + ).uid.raw_uid, + "0240000034544E45001A00018AA900292011000097969900", + ) + self.assertEqual( + parse_device_id("USB\\VID_0D28&PID_0204&MI_00\\0240000034544E45001A00018AA900292011000097969900").PID, + "0204", + ) + self.assertEqual( + parse_device_id("USB\\VID_0D28&PID_0204&MI_02\\0240000034544E45001A00018AA900292011000097969900").VID, + "0D28", + ) + self.assertEqual( + parse_device_id("USB\\VID_0D28&PID_0204&MI_02\\0240000034544E45001A00018AA900292011000097969900").MI, "02" + ) + + def test_equals(self): + from mbed_tools.devices._internal.windows.usb_device_identifier import UsbIdentifier, KEY_UID + + # Checks that two unset identifiers are equal + a = UsbIdentifier() + b = UsbIdentifier() + self.assertEqual(a, a) + self.assertEqual(a, b) + self.assertEqual(b, a) + # Checks that two different identifiers are not equal + a_dict = data_object_to_dict(a) + a_dict[KEY_UID] = generateUID() + b_dict = data_object_to_dict(a) + b_dict[KEY_UID] = generateUID() + a = UsbIdentifier(**a_dict) + b = UsbIdentifier(**b_dict) + self.assertEqual(a, a) + self.assertNotEqual(a.uid, b.uid) + self.assertNotEqual(a, b) + self.assertNotEqual(b, a) + # Checks that two identifiers with same fields are equal + b = UsbIdentifier(**a_dict) + self.assertEqual(a.uid, b.uid) + self.assertEqual(a, a) + self.assertEqual(a, b) + self.assertEqual(b, a) + # Checks that identifier and other type are not equal + self.assertFalse(a == 1) + + def test_hashing(self): + from mbed_tools.devices._internal.windows.usb_device_identifier import UsbIdentifier, KEY_UID + + # Generates two different USB identifiers + a = UsbIdentifier() + a_dict = data_object_to_dict(a) + a_dict[KEY_UID] = generateUID() + b_dict = data_object_to_dict(a) + b_dict[KEY_UID] = generateUID() + a = UsbIdentifier(**a_dict) + b = UsbIdentifier(**b_dict) + + self.assertNotEqual(hash(a), hash(b)) + self.assertNotEqual(a, b) + # Checks dictionary lookup + self.assertNotIn(a, dict()) + self.assertNotIn(a, {b: ""}) + self.assertIn(b, {b: ""}) + + # Creates c so that a and c have the same fields. + c = UsbIdentifier(**a_dict) + + # Checks dictionary lookup + self.assertIn(c, {c: ""}) + self.assertIn(a, {c: ""}) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_usb_hub_data_loader.py b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_usb_hub_data_loader.py new file mode 100644 index 0000000000..a9f858adee --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_usb_hub_data_loader.py @@ -0,0 +1,227 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import unittest +from python_tests.mbed_tools.devices.markers import windows_only +from mbed_tools.devices._internal.windows.windows_identifier import WindowsUID + +MOCKED_SERIAL_NUMBER_DATA = { + "USB\\VID_04CA&PID_7058\\5&31AC2C0B&0&8": "6&212cca94&0", + "USB\\VID_0D28&PID_0204&MI_00\\0240000034544E45001A00018AA900292011000097969900": None, + "USB\\VID_1FD2&PID_5003\\5&31AC2C0B&0&10": "6&1f9e0013&0", + "USB\\VID_0D28&PID_0204\\0240000034544E45001A00018AA900292011000097969900": None, + "USB\\VID_2109&PID_2812\\6&38E4CCB6&0&3": "7&3885ae75&0", + "USB\\VID_2109&PID_2812\\6&38E4CCB6&0&4": "7&25cbfdd0&0", + "USB\\ROOT_HUB30\\4&38EF038C&0&0": "5&31ac2c0b&0", + "USB\\VID_2109&PID_2812\\5&31AC2C0B&0&1": "6&38e4ccb6&0", + "USB\\VID_1366&PID_1015\\000440112138": "8&2f125ec6&0", + "USB\\VID_1366&PID_1015&MI_02\\8&2F125EC6&0&0002": None, + "USB\\VID_1366&PID_1015&MI_03\\8&2F125EC6&0&0003": "9&dbdecf6&0", + "USB\\VID_1366&PID_1015\\000440109371": "8&37e0b92a&0", + "USB\\VID_1366&PID_1015&MI_02\\8&37E0B92A&0&0002": None, + "USB\\VID_1366&PID_1015&MI_03\\8&37E0B92A&0&0003": "9&4aa5a31&0", + "USB\\VID_0483&PID_374B\\0670FF303931594E43184021": "8&2ae96f5b&0", + "USB\\VID_0483&PID_374B&MI_01\\8&2AE96F5B&0&0001": "9&4a48a72&0", +} + + +def generate_mocked_system_usb_device_information(): + from mbed_tools.devices._internal.windows.usb_hub_data_loader import ( + SystemUsbDeviceInformation, + UsbHub, + UsbIdentifier, + ) + from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader + + controllers = [ + UsbIdentifier( + UID=WindowsUID(uid="3&33fd14ca&0", raw_uid="3&33FD14CA&0&A0", serial_number=None), + VID="0483", + PID="ABDB", + REV="21", + MI=None, + ) + ] + + genuine_usb_hubs = [ + {"DeviceID": "USB\\VID_04CA&PID_7058\\5&31AC2C0B&0&8", "PNPDeviceID": "USB\\VID_04CA&PID_7058\\5&31AC2C0B&0&8"}, + { + "DeviceID": "USB\\VID_0D28&PID_0204&MI_00\\0240000034544E45001A00018AA900292011000097969900", + "PNPDeviceID": "USB\\VID_0D28&PID_0204&MI_00\\0240000034544E45001A00018AA900292011000097969900", + }, + { + "DeviceID": "USB\\VID_1FD2&PID_5003\\5&31AC2C0B&0&10", + "PNPDeviceID": "USB\\VID_1FD2&PID_5003\\5&31AC2C0B&0&10", + }, + { + "DeviceID": "USB\\VID_0D28&PID_0204\\0240000034544E45001A00018AA900292011000097969900", + "PNPDeviceID": "USB\\VID_0D28&PID_0204\\0240000034544E45001A00018AA900292011000097969900", + }, + {"DeviceID": "USB\\VID_2109&PID_2812\\6&38E4CCB6&0&3", "PNPDeviceID": "USB\\VID_2109&PID_2812\\6&38E4CCB6&0&3"}, + {"DeviceID": "USB\\VID_2109&PID_2812\\6&38E4CCB6&0&4", "PNPDeviceID": "USB\\VID_2109&PID_2812\\6&38E4CCB6&0&4"}, + {"DeviceID": "USB\\ROOT_HUB30\\4&38EF038C&0&0", "PNPDeviceID": "USB\\ROOT_HUB30\\4&38EF038C&0&0"}, + {"DeviceID": "USB\\VID_2109&PID_2812\\5&31AC2C0B&0&1", "PNPDeviceID": "USB\\VID_2109&PID_2812\\5&31AC2C0B&0&1"}, + {"DeviceID": "USB\\VID_1366&PID_1015\\000440112138", "PNPDeviceID": "USB\\VID_1366&PID_1015\\000440112138"}, + { + "DeviceID": "USB\\VID_1366&PID_1015&MI_02\\8&2F125EC6&0&0002", + "PNPDeviceID": "USB\\VID_1366&PID_1015&MI_02\\8&2F125EC6&0&0002", + }, + { + "DeviceID": "USB\\VID_1366&PID_1015&MI_03\\8&2F125EC6&0&0003", + "PNPDeviceID": "USB\\VID_1366&PID_1015&MI_03\\8&2F125EC6&0&0003", + }, + {"DeviceID": "USB\\VID_1366&PID_1015\\000440109371", "PNPDeviceID": "USB\\VID_1366&PID_1015\\000440109371"}, + { + "DeviceID": "USB\\VID_1366&PID_1015&MI_02\\8&37E0B92A&0&0002", + "PNPDeviceID": "USB\\VID_1366&PID_1015&MI_02\\8&37E0B92A&0&0002", + }, + { + "DeviceID": "USB\\VID_1366&PID_1015&MI_03\\8&37E0B92A&0&0003", + "PNPDeviceID": "USB\\VID_1366&PID_1015&MI_03\\8&37E0B92A&0&0003", + }, + { + "DeviceID": "USB\\VID_0483&PID_374B\\0670FF303931594E43184021", + "PNPDeviceID": "USB\\VID_0483&PID_374B\\0670FF303931594E43184021", + }, + { + "DeviceID": "USB\\VID_0483&PID_374B&MI_01\\8&2AE96F5B&0&0001", + "PNPDeviceID": "USB\\VID_0483&PID_374B&MI_01\\8&2AE96F5B&0&0001", + }, + ] + usb_hubs = genuine_usb_hubs + [ + dict( + DeviceID=f"USB\\VID_{uid.vendor_id}&PID_{uid.product_id}&MI_01\\{uid.uid.raw_uid}", + PNPDeviceID=f"USB\\VID_{uid.vendor_id}&PID_{uid.product_id}&MI_01\\{uid.uid.raw_uid}", + ) + for uid in controllers + ] + + class MockedDataLoader(SystemDataLoader): + def _load(self): + pass + + class MockedSystemUsbDeviceInformation(SystemUsbDeviceInformation): + def __init__(self): + super().__init__(MockedDataLoader()) + + def _list_usb_controller_ids(self): + return controllers + + def _iterate_over_hubs(self): + hubs = [UsbHub() for _ in range(0, len(usb_hubs))] + for i, hub in enumerate(usb_hubs): + hubs[i].set_data_values(hub) + for h in hubs: + yield h + + def _determine_potential_serial_number(self, usb_device): + return MOCKED_SERIAL_NUMBER_DATA.get(usb_device.pnp_id) + + return MockedSystemUsbDeviceInformation() + + +@windows_only +class TestUsbHub(unittest.TestCase): + def test_system_usb_ids_list(self): + from mbed_tools.devices._internal.windows.usb_hub_data_loader import UsbIdentifier + + mock = generate_mocked_system_usb_device_information() + expected_values = { + UsbIdentifier( + UID=WindowsUID( + uid="0240000034544e45001a00018aa900292011000097969900", + raw_uid="0240000034544E45001A00018AA900292011000097969900", + serial_number=None, + ), + VID="0D28", + PID="0204", + REV=None, + MI="00", + ), + UsbIdentifier( + UID=WindowsUID(uid="5&31ac2c0b&0", raw_uid="5&31AC2C0B&0&1", serial_number="6&38e4ccb6&0"), + VID="2109", + PID="2812", + REV=None, + MI=None, + ), + UsbIdentifier( + UID=WindowsUID(uid="000440112138", raw_uid="000440112138", serial_number="8&2f125ec6&0"), + VID="1366", + PID="1015", + REV=None, + MI=None, + ), + UsbIdentifier( + UID=WindowsUID(uid="4&38ef038c&0", raw_uid="4&38EF038C&0&0", serial_number="5&31ac2c0b&0"), + VID=None, + PID=None, + REV=None, + MI=None, + ), + UsbIdentifier( + UID=WindowsUID(uid="5&31ac2c0b&0", raw_uid="5&31AC2C0B&0&10", serial_number="6&1f9e0013&0"), + VID="1FD2", + PID="5003", + REV=None, + MI=None, + ), + UsbIdentifier( + UID=WindowsUID(uid="000440109371", raw_uid="000440109371", serial_number="8&37e0b92a&0"), + VID="1366", + PID="1015", + REV=None, + MI=None, + ), + UsbIdentifier( + UID=WindowsUID(uid="5&31ac2c0b&0", raw_uid="5&31AC2C0B&0&8", serial_number="6&212cca94&0"), + VID="04CA", + PID="7058", + REV=None, + MI=None, + ), + UsbIdentifier( + UID=WindowsUID( + uid="0670ff303931594e43184021", raw_uid="0670FF303931594E43184021", serial_number="8&2ae96f5b&0" + ), + VID="0483", + PID="374B", + REV=None, + MI=None, + ), + UsbIdentifier( + UID=WindowsUID(uid="6&38e4ccb6&0", raw_uid="6&38E4CCB6&0&3", serial_number="7&3885ae75&0"), + VID="2109", + PID="2812", + REV=None, + MI=None, + ), + } + self.assertSetEqual(mock.usb_device_ids(), expected_values) + + def test_system_usb_interfaces(self): + mock = generate_mocked_system_usb_device_information() + cache = mock.usb_devices + for id in mock.usb_device_ids(): + self.assertIn(id, cache) + + def test_get_usb_interfaces(self): + from mbed_tools.devices._internal.windows.usb_hub_data_loader import UsbIdentifier + + known_usb = UsbIdentifier( + UID=WindowsUID( + uid="0670ff303931594e43184021", raw_uid="0670FF303931594E43184021", serial_number="8&2ae96f5b&0" + ), + VID="0483", + PID="374B", + REV=None, + MI=None, + ) + expected_related_interfaces = [ + "USB\\VID_0483&PID_374B\\0670FF303931594E43184021", + "USB\\VID_0483&PID_374B&MI_01\\8&2AE96F5B&0&0001", + ] + mock = generate_mocked_system_usb_device_information() + self.assertIsNotNone(mock.get_usb_devices(known_usb)) + self.assertListEqual([h.component_id for h in mock.get_usb_devices(known_usb)], expected_related_interfaces) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_windows_component.py b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_windows_component.py new file mode 100644 index 0000000000..cd5281492b --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_windows_component.py @@ -0,0 +1,81 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from typing import NamedTuple, Any +from unittest import TestCase + +from mbed_tools.devices._internal.windows.component_descriptor_utils import is_undefined_data_object, is_undefined_value +from python_tests.mbed_tools.devices._internal.windows.test_component_descriptor_utils import ( + generate_undefined_values, + generate_valid_values, +) +from python_tests.mbed_tools.devices.markers import windows_only + + +class ComponentDefinition(NamedTuple): + field1: str + field2: str + field3: str + field4: Any + field5: bool + field6: int + + +def get_test_class(): + from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor + + class AComponentForTest(ComponentDescriptor): + def __init__(self) -> None: + """Initialiser.""" + super().__init__(ComponentDefinition, win32_class_name="Win32_ComputerSystem") + + @property + def component_id(self) -> str: + """Returns the device id field.""" + return self.get("field1") + + return AComponentForTest + + +@windows_only +class TestComponentDescriptor(TestCase): + def test_init(self): + self.assertIsNotNone(get_test_class()()) + self.assertTrue(get_test_class()().__class__, "AComponentForTest") + + def test_parameters(self): + self.assertListEqual([name for name in ComponentDefinition._fields], get_test_class()().field_names) + + def test_set_values(self): + + a_component = get_test_class()() + self.assertTrue(is_undefined_data_object(a_component.to_tuple())) + valid_values = {k: generate_valid_values() for k in a_component.field_names} + a_component.set_data_values(valid_values) + self.assertFalse(is_undefined_data_object(a_component.to_tuple())) + self.assertTupleEqual(a_component.to_tuple(), tuple(valid_values.values())) + + def test_is_undefined(self): + a_component = get_test_class()() + self.assertTrue(a_component.is_undefined) + self.assertTrue(is_undefined_value(a_component.component_id)) + a_component_with_undefined_values = get_test_class()() + undefined_values = {k: generate_undefined_values() for k in a_component.field_names} + a_component_with_undefined_values.set_data_values(undefined_values) + self.assertTrue(a_component_with_undefined_values.is_undefined) + a_defined_component = get_test_class()() + valid_values = {k: generate_valid_values() for k in a_component.field_names} + a_defined_component.set_data_values(valid_values) + self.assertIsNotNone(a_defined_component.component_id) + self.assertFalse(a_defined_component.is_undefined) + + def test_iterator(self): + from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptorWrapper + + generator = ComponentDescriptorWrapper(get_test_class()).element_generator() + self.assertIsNotNone(generator) + + for element in generator: + # The generator should be defined but none of the element should be defined as fields should not exist + self.assertTrue(element.is_undefined) diff --git a/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_windows_identifier.py b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_windows_identifier.py new file mode 100644 index 0000000000..ecf174955c --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/_internal/windows/test_windows_identifier.py @@ -0,0 +1,183 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from python_tests.mbed_tools.devices.markers import windows_only +import unittest +from mbed_tools.devices._internal.windows.windows_identifier import WindowsUID, is_device_instance_id +import random +import uuid + + +def generateUID() -> WindowsUID: + return WindowsUID( + uid=str(uuid.uuid4()), raw_uid=f"/{uuid.uuid4()}&001", serial_number=f"SN{str(uuid.uuid4()).replace('-','')}" + ) + + +@windows_only +class TestWindowsUID(unittest.TestCase): + def test_is_instance_id(self): + # Testing that the values are likely to be instance IDs generated by the OS + self.assertTrue(is_device_instance_id(None)) + self.assertTrue(is_device_instance_id("8&2F125EC6&0&0003")) + self.assertTrue(is_device_instance_id("8&2f125ec6&0")) + self.assertTrue(is_device_instance_id("8&2F125EC6&0&0002")) + self.assertFalse(is_device_instance_id("")) + self.assertFalse(is_device_instance_id("000440112138")) + + def test_uid_equality(self): + uid1 = WindowsUID(uid="uid1", raw_uid="/uid1&001", serial_number=str(random.randint(1, 100))) + # Equal testing with self and other types. + self.assertNotEqual(uid1, None) + self.assertIsNotNone(uid1) + self.assertNotEqual(uid1, "") + self.assertFalse(uid1 == "") + self.assertFalse(uid1 == dict()) + self.assertEqual(uid1, uid1) + self.assertTrue(uid1 == uid1) + + # Does not equal to completely different objects + uid2 = WindowsUID(uid="uid2", raw_uid="/uid1&002", serial_number=None) + self.assertNotEqual(uid1, uid2) + + # Equals other objects with same uid + uid3 = WindowsUID(uid="uid1", raw_uid="/uid1&003", serial_number=str(random.randint(1, 100))) + self.assertEqual(uid1, uid3) + self.assertTrue(uid1 == uid3) + + # Equals other objects with similar uid (subset) + uid5 = WindowsUID(uid="uid1&0114", raw_uid="/uid1&0114", serial_number=None) + self.assertEqual(uid3, uid5) + self.assertEqual(uid1, uid5) + + # Equals other objects with same serial number + uid4 = WindowsUID(uid="uid4", raw_uid="/uid4&004", serial_number=uid1.serial_number) + self.assertEqual(uid1, uid4) + + # Equals other objects with serial number same to uid + uid6 = WindowsUID(uid="uid6454", raw_uid="/uid6454&006", serial_number="uid1") + self.assertEqual(uid1, uid6) + # Tests with real data examples: disk UIDs and equivalent USB hosts UIDs. + # Daplink: + uid7 = WindowsUID( + uid="0240000034544e45001a00018aa900292011000097969900&0", + raw_uid="0240000034544E45001A00018AA900292011000097969900&0", + serial_number="0240000034544e45001a00018aa900292011000097969900", + ) + uid8 = WindowsUID( + uid="0240000034544e45001a00018aa900292011000097969900", + raw_uid="0240000034544E45001A00018AA900292011000097969900", + serial_number=None, + ) + self.assertEqual(uid7, uid8) + # JLink: + uid9 = WindowsUID( + uid="000440112138", raw_uid="9&DBDECF6&0&000440112138&0", serial_number=" 134657890" + ) + uid10 = WindowsUID(uid="000440112138", raw_uid="000440112138", serial_number="8&2f125ec6&0") + self.assertEqual(uid9, uid10) + # STLink + uid11 = WindowsUID( + uid="0672ff574953867567051035", + raw_uid="9&3849C7A8&0&0672FF574953867567051035&0", + serial_number="0672FF574953867567051035", + ) + uid12 = WindowsUID( + uid="0672ff574953867567051035", raw_uid="0672FF574953867567051035", serial_number="8&254f12cf&0" + ) + self.assertEqual(uid11, uid12) + + def test_serial_number(self): + # Tests trying to determine the most plausible serial number from a set of values. + uid1 = generateUID() + self.assertEqual(uid1.presumed_serial_number, uid1.uid) + self.assertNotEqual(uid1.presumed_serial_number, uid1.serial_number) + uid2 = WindowsUID(uid="uid12&223", raw_uid="djfds;fj", serial_number=None) + self.assertEqual(uid2.presumed_serial_number, uid2.uid) + self.assertNotEqual(uid2.presumed_serial_number, uid2.serial_number) + self.assertFalse(uid2.contains_genuine_serial_number()) + uid3 = WindowsUID(uid="uid12&223", raw_uid="djfds;fj", serial_number="12345679") + self.assertNotEqual(uid3.presumed_serial_number, uid3.uid) + self.assertEqual(uid3.presumed_serial_number, uid3.serial_number) + self.assertTrue(uid3.contains_genuine_serial_number()) + + def test_instanceid(self): + # Tests trying to determine the most plausible instance IDs from a set of values. + uid1 = generateUID() + self.assertEqual(uid1.instance_id, uid1.serial_number) + uid2 = WindowsUID(uid="uid12&223", raw_uid="djfds;fj", serial_number=None) + self.assertEqual(uid2.instance_id, uid2.uid) + uid3 = WindowsUID(uid="uid12&223", raw_uid="djfds;fj", serial_number="12345679") + self.assertEqual(uid3.instance_id, uid3.uid) + uid4 = WindowsUID(uid="12345687", raw_uid="djfds;fj", serial_number="12&3456&79") + self.assertEqual(uid4.instance_id, uid4.serial_number) + + def test_uid_hashing(self): + uid1 = generateUID() + uid2 = generateUID() + # Usual checks for different UIDs + # Checks that if hashes are different then elements are not equal + self.assertNotEqual(hash(uid1), hash(uid2)) + self.assertNotEqual(uid1, uid2) + + # Checks lookup in set + self.assertIn(uid1, (uid1, uid2)) + self.assertIn(uid2, {uid1, uid2}) + # Checks lookup in dictionary + self.assertIn(uid1, {uid1: "1", uid2: "2"}) + self.assertNotIn(uid1, dict()) + self.assertNotIn(uid1, {uid2: "1"}) + self.assertEqual({uid1: "1", uid2: "2"}.get(uid1), "1") + self.assertIsNone({uid2: "2"}.get(uid1)) + + # Checks the situation where two UIDs are equal/corresponding to the same device but their fields are different. + uid3 = WindowsUID(uid="uid12&223", raw_uid="djfds;fj", serial_number=None) + uid4 = WindowsUID(uid="123412", raw_uid="djfds;fjfsf&0&0000", serial_number="uid12&223") + # UIDs being equal => hashes are equal. + self.assertEqual(uid3, uid4) + self.assertEqual(uid4, uid3) + self.assertEqual(hash(uid3), hash(uid4)) + self.assertEqual(hash(uid4), hash(uid3)) + # Checks lookup in set + self.assertIn(uid3, (uid1, uid4)) + self.assertIn(uid4, (uid1, uid3)) + + def test_related_uid_lookup(self): + # Tests dictionary lookup using real data. + # UIDs are corresponding to Disk UIDs and related USB HUB UIDs. + # Daplink + uid11 = WindowsUID( + uid="0240000034544e45001a00018aa900292011000097969900&0", + raw_uid="0240000034544E45001A00018AA900292011000097969900&0", + serial_number="0240000034544e45001a00018aa900292011000097969900", + ) + uid12 = WindowsUID( + uid="0240000034544e45001a00018aa900292011000097969900", + raw_uid="0240000034544E45001A00018AA900292011000097969900", + serial_number=None, + ) + self.assertIn(uid12.presumed_serial_number, {uid11.presumed_serial_number: ""}) + # JLink + uid21 = WindowsUID( + uid="000440112138", raw_uid="9&DBDECF6&0&000440112138&0", serial_number=" 134657890" + ) + uid22 = WindowsUID(uid="000440112138", raw_uid="000440112138", serial_number="8&2f125ec6&0") + self.assertIn(uid21.presumed_serial_number, {uid22.presumed_serial_number: ""}) + # STLink + uid31 = WindowsUID( + uid="0672ff574953867567051035", + raw_uid="9&3849C7A8&0&0672FF574953867567051035&0", + serial_number="0672FF574953867567051035", + ) + uid32 = WindowsUID( + uid="0672ff574953867567051035", raw_uid="0672FF574953867567051035", serial_number="8&254f12cf&0" + ) + self.assertIn(uid31.presumed_serial_number, {uid32.presumed_serial_number: ""}) + + def test_ordering(self): + uid1 = WindowsUID(uid="123456789", raw_uid="/uid1&002", serial_number=None) + uid2 = WindowsUID(uid="0&64FAFGG", raw_uid="/0&64FAFGG&002", serial_number="345240562") + self.assertGreater(uid2, uid1) + self.assertLess(uid1, uid2) + self.assertListEqual(sorted([uid2, uid1]), [uid1, uid2]) diff --git a/tools/python/python_tests/mbed_tools/devices/factories.py b/tools/python/python_tests/mbed_tools/devices/factories.py new file mode 100644 index 0000000000..3f85e93945 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/factories.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import factory +import pathlib + +from mbed_tools.devices._internal.candidate_device import CandidateDevice + + +class CandidateDeviceFactory(factory.Factory): + class Meta: + model = CandidateDevice + + product_id = factory.Faker("hexify") + vendor_id = factory.Faker("hexify") + mount_points = [pathlib.Path(".")] + serial_number = factory.Faker("hexify", text=("^" * 20)) # 20 characters serial number + serial_port = None diff --git a/tools/python/python_tests/mbed_tools/devices/markers.py b/tools/python/python_tests/mbed_tools/devices/markers.py new file mode 100644 index 0000000000..94abce4029 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/markers.py @@ -0,0 +1,12 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Shared pytest functionality.""" +import platform +import unittest + + +windows_only = unittest.skipIf(platform.system() != "Windows", reason="Windows required") +darwin_only = unittest.skipIf(platform.system() != "Darwin", reason="Darwin required") +linux_only = unittest.skipIf(platform.system() != "Linux", reason="Linux required") diff --git a/tools/python/python_tests/mbed_tools/devices/test_mbed_devices.py b/tools/python/python_tests/mbed_tools/devices/test_mbed_devices.py new file mode 100644 index 0000000000..40dad93219 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/devices/test_mbed_devices.py @@ -0,0 +1,195 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import pathlib +import re + +from unittest import mock + +import pytest + +from mbed_tools.targets import Board + +from python_tests.mbed_tools.devices.factories import CandidateDeviceFactory +from mbed_tools.devices.device import Device +from mbed_tools.devices._internal.exceptions import NoBoardForCandidate, ResolveBoardError + +from mbed_tools.devices.devices import ( + get_connected_devices, + find_connected_device, + find_all_connected_devices, +) +from mbed_tools.devices.exceptions import DeviceLookupFailed, NoDevicesFound + + +@mock.patch("mbed_tools.devices.devices.detect_candidate_devices") +@mock.patch("mbed_tools.devices.device.resolve_board") +class TestGetConnectedDevices: + @mock.patch("mbed_tools.devices.device.read_device_files") + def test_builds_devices_from_candidates(self, read_device_files, resolve_board, detect_candidate_devices): + candidate = CandidateDeviceFactory() + detect_candidate_devices.return_value = [candidate] + iface_details = {"Version": "0222"} + read_device_files.return_value = mock.Mock(interface_details=iface_details) + + connected_devices = get_connected_devices() + assert connected_devices.identified_devices == [ + Device( + serial_port=candidate.serial_port, + serial_number=candidate.serial_number, + mount_points=candidate.mount_points, + mbed_board=resolve_board.return_value, + mbed_enabled=True, + interface_version=iface_details["Version"], + ) + ] + assert not connected_devices.unidentified_devices + + @mock.patch.object(Board, "from_offline_board_entry") + def test_skips_candidates_without_a_board(self, board, resolve_board, detect_candidate_devices): + candidate = CandidateDeviceFactory() + resolve_board.side_effect = NoBoardForCandidate + detect_candidate_devices.return_value = [candidate] + board.return_value = None + + connected_devices = get_connected_devices() + assert connected_devices.identified_devices == [] + assert connected_devices.unidentified_devices == [ + Device( + serial_port=candidate.serial_port, + serial_number=candidate.serial_number, + mount_points=candidate.mount_points, + mbed_board=None, + ) + ] + + @mock.patch("mbed_tools.devices.device.read_device_files") + def test_raises_when_resolve_board_fails(self, read_device_files, resolve_board, detect_candidate_devices): + candidate = CandidateDeviceFactory() + resolve_board.side_effect = ResolveBoardError + detect_candidate_devices.return_value = [candidate] + + with pytest.raises(DeviceLookupFailed, match="candidate"): + get_connected_devices() + + +@mock.patch("mbed_tools.devices.devices.find_all_connected_devices") +class TestFindConnectedDevice: + def test_finds_device_with_matching_name(self, mock_find_connected_devices): + target_name = "K64F" + mock_find_connected_devices.return_value = [ + mock.Mock(mbed_board=mock.Mock(board_type=target_name, spec=True), serial_number="123", spec=True) + ] + + dev = find_connected_device(target_name) + + assert target_name == dev.mbed_board.board_type + + def test_finds_device_with_matching_name_identifier(self, mock_find_connected_devices): + target_name = "K64F" + target_identifier = 1 + mock_find_connected_devices.return_value = [ + mock.Mock(mbed_board=mock.Mock(board_type=target_name, spec=True), serial_number="123", spec=True), + mock.Mock(mbed_board=mock.Mock(board_type=target_name, spec=True), serial_number="456", spec=True), + ] + + dev = find_connected_device(target_name, target_identifier) + + assert dev.serial_number == "456" + + def test_raises_when_multiple_matching_name_no_identifier(self, mock_find_connected_devices): + target_name = "K64F" + mock_find_connected_devices.return_value = [ + mock.Mock( + serial_port="tty.0", + mount_points=[pathlib.Path("/board")], + mbed_board=mock.Mock(board_type=target_name, spec=True), + serial_number="456", + spec=True, + ), + mock.Mock( + serial_port="tty.1", + mount_points=[pathlib.Path("/board2")], + mbed_board=mock.Mock(board_type=target_name, spec=True), + serial_number="123", + spec=True, + ), + ] + + with pytest.raises(DeviceLookupFailed, match="Multiple"): + find_connected_device("K64F", None) + + def test_raises_when_identifier_out_of_bounds(self, mock_find_connected_devices): + target_name = "K64F" + mock_find_connected_devices.return_value = [ + mock.Mock( + serial_port="tty.0", + mount_points=[pathlib.Path("/board")], + mbed_board=mock.Mock(board_type=target_name, spec=True), + serial_number="456", + spec=True, + ), + mock.Mock( + serial_port="tty.1", + mount_points=[pathlib.Path("/board2")], + mbed_board=mock.Mock(board_type=target_name, spec=True), + serial_number="123", + spec=True, + ), + ] + + with pytest.raises(DeviceLookupFailed, match="valid"): + find_connected_device("K64F", 2) + + +@mock.patch("mbed_tools.devices.devices.get_connected_devices") +class TestFindAllConnectedDevices: + def test_finds_all_devices_with_matching_name(self, mock_get_connected_devices): + target_name = "K64F" + mock_get_connected_devices.return_value = mock.Mock( + identified_devices=[ + mock.Mock(mbed_board=mock.Mock(board_type=target_name, spec=True), serial_number="456", spec=True), + mock.Mock(mbed_board=mock.Mock(board_type=target_name, spec=True), serial_number="123", spec=True), + mock.Mock(mbed_board=mock.Mock(board_type="DISCO", spec=True), serial_number="345", spec=True), + ], + spec=True, + ) + + devices = find_all_connected_devices(target_name) + + assert len(devices) == 2 + assert devices[0].serial_number == "123" + assert devices[1].serial_number == "456" + + def test_raises_when_no_mbed_enabled_devices_found(self, mock_get_connected_devices): + mock_get_connected_devices.return_value = mock.Mock(identified_devices=[], spec=True) + + with pytest.raises(NoDevicesFound): + find_all_connected_devices("K64F") + + def test_raises_when_device_matching_target_name_not_found(self, mock_get_connected_devices): + target_name = "K64F" + connected_target_name = "DISCO" + connected_target_serial_port = "tty.BLEH" + connected_target_mount_point = [pathlib.Path("/dap")] + mock_get_connected_devices.return_value = mock.Mock( + identified_devices=[ + mock.Mock( + serial_port=connected_target_serial_port, + mount_points=connected_target_mount_point, + mbed_board=mock.Mock(board_type=connected_target_name, spec=True), + spec=True, + ) + ], + spec=True, + ) + + with pytest.raises( + DeviceLookupFailed, + match=( + f".*(target: {re.escape(connected_target_name)}).*(port: {re.escape(connected_target_serial_port)}).*" + f"(mount point.*: {re.escape(str(connected_target_mount_point))})" + ), + ): + find_all_connected_devices(target_name) diff --git a/tools/python/python_tests/mbed_tools/lib/test_json_helpers.py b/tools/python/python_tests/mbed_tools/lib/test_json_helpers.py new file mode 100644 index 0000000000..d5b1e5b6d7 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/lib/test_json_helpers.py @@ -0,0 +1,17 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import json + +import pytest + +from mbed_tools.lib.json_helpers import decode_json_file + + +def test_invalid_json(tmp_path): + lib_json_path = tmp_path / "mbed_lib.json" + lib_json_path.write_text("name") + + with pytest.raises(json.JSONDecodeError): + decode_json_file(lib_json_path) diff --git a/tools/python/python_tests/mbed_tools/lib/test_logging.py b/tools/python/python_tests/mbed_tools/lib/test_logging.py new file mode 100644 index 0000000000..b55d223712 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/lib/test_logging.py @@ -0,0 +1,166 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import logging +from unittest import TestCase, mock + +from mbed_tools.lib.exceptions import ToolsError +from mbed_tools.lib.logging import _exception_message, MbedToolsHandler, log_exception, set_log_level, LOGGING_FORMAT + + +class SubclassedToolsError(ToolsError): + """An exception subclassing ToolsError.""" + + +class TestExceptionMessage(TestCase): + def test_critical_log_level_with_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.CRITICAL, True) + self.assertTrue("'-v'" in message) + self.assertTrue("'--traceback'" not in message) + self.assertTrue("unlikely string" in message) + + def test_critical_log_level_without_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.CRITICAL, False) + self.assertTrue("'-v'" in message) + self.assertTrue("'--traceback'" not in message) + self.assertTrue("unlikely string" in message) + + def test_error_log_level_with_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.ERROR, True) + self.assertTrue("'-v'" in message) + self.assertTrue("'--traceback'" not in message) + self.assertTrue("unlikely string" in message) + + def test_error_log_level_without_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.ERROR, False) + self.assertTrue("'-v'" in message) + self.assertTrue("'--traceback'" not in message) + self.assertTrue("unlikely string" in message) + + def test_warning_log_level_with_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.WARNING, True) + self.assertTrue("'-vv'" in message) + self.assertTrue("'--traceback'" not in message) + self.assertTrue("unlikely string" in message) + + def test_warning_log_level_without_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.WARNING, False) + self.assertTrue("'-vv'" in message) + self.assertTrue("'--traceback'" not in message) + self.assertTrue("unlikely string" in message) + + def test_info_log_level_with_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.INFO, True) + self.assertTrue("'-vvv'" in message) + self.assertTrue("'--traceback'" not in message) + self.assertTrue("unlikely string" in message) + + def test_info_log_level_without_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.INFO, False) + self.assertTrue("'-vvv'" in message) + self.assertTrue("'--traceback'" not in message) + self.assertTrue("unlikely string" in message) + + def test_debug_log_level_with_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.DEBUG, True) + self.assertTrue("-v" not in message) + self.assertTrue("'--traceback'" not in message) + self.assertTrue("unlikely string" in message) + + def test_debug_log_level_without_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.DEBUG, False) + self.assertTrue("-v" not in message) + self.assertTrue("'--traceback'" in message) + self.assertTrue("unlikely string" in message) + + def test_log_level_not_set_with_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.NOTSET, True) + self.assertTrue("'-v'" in message) + self.assertTrue("'--traceback'" not in message) + self.assertTrue("unlikely string" in message) + + def test_log_level_not_set_without_traceback(self): + message = _exception_message(ToolsError("unlikely string"), logging.NOTSET, False) + self.assertTrue("'-v'" in message) + self.assertTrue("'--traceback'" not in message) + self.assertTrue("unlikely string" in message) + + +class TestMbedToolsHandler(TestCase): + exception_string: str = "A Message" + expected_log_message: str = "A Message\n\nMore information may be available by using the command line option '-vv'." + + def test_no_exception_raised(self): + mock_logger = mock.Mock(spec_set=logging.Logger) + with MbedToolsHandler(mock_logger, traceback=False): + pass + self.assertFalse(mock_logger.error.called, "Error should not be logger when an exception is not raised.") + + def test_tools_error_with_traceback(self): + mock_logger = mock.Mock(spec_set=logging.Logger) + with MbedToolsHandler(mock_logger, traceback=True): + raise ToolsError(self.exception_string) + mock_logger.error.assert_called_once_with(self.expected_log_message, exc_info=True) + + def test_tools_error_without_traceback(self): + mock_logger = mock.Mock(spec_set=logging.Logger) + with MbedToolsHandler(mock_logger, traceback=False): + raise ToolsError(self.exception_string) + mock_logger.error.assert_called_once_with(self.expected_log_message, exc_info=False) + + def test_subclassed_tools_error_with_traceback(self): + mock_logger = mock.Mock(spec_set=logging.Logger) + with MbedToolsHandler(mock_logger, traceback=True): + raise SubclassedToolsError(self.exception_string) + mock_logger.error.assert_called_once_with(self.expected_log_message, exc_info=True) + + def test_subclassed_tools_error_without_traceback(self): + mock_logger = mock.Mock(spec_set=logging.Logger) + with MbedToolsHandler(mock_logger, traceback=False): + raise SubclassedToolsError(self.exception_string) + mock_logger.error.assert_called_once_with(self.expected_log_message, exc_info=False) + + def test_other_exceptions(self): + mock_logger = mock.Mock(spec_set=logging.Logger) + with self.assertRaises(ValueError): + with MbedToolsHandler(mock_logger, traceback=False): + raise ValueError(self.exception_string) + self.assertFalse(mock_logger.error.called, "Error should not be logger when a tools error is not raised.") + + +class TestLogException(TestCase): + def test_log_error(self): + mock_logger = mock.Mock(spec_set=logging.Logger) + mock_exception = mock.Mock(spec_set=Exception) + + log_exception(mock_logger, mock_exception) + + mock_logger.error.assert_called_once_with(mock_exception, exc_info=False) + + def test_log_error_with_traceback(self): + mock_logger = mock.Mock(spec_set=logging.Logger) + mock_exception = mock.Mock(spec_set=Exception) + + log_exception(mock_logger, mock_exception, True) + + mock_logger.error.assert_called_once_with(mock_exception, exc_info=True) + + +@mock.patch("mbed_tools.lib.logging.logging", return_value=mock.Mock(spec_set=logging)) +class TestSetLogLevel(TestCase): + def test_debug(self, mocked_logging): + set_log_level(verbose_count=3) + mocked_logging.basicConfig.assert_called_once_with(level=mocked_logging.DEBUG, format=LOGGING_FORMAT) + + def test_info(self, mocked_logging): + set_log_level(verbose_count=2) + mocked_logging.basicConfig.assert_called_once_with(level=mocked_logging.INFO, format=LOGGING_FORMAT) + + def test_warning(self, mocked_logging): + set_log_level(verbose_count=1) + mocked_logging.basicConfig.assert_called_once_with(level=mocked_logging.WARNING, format=LOGGING_FORMAT) + + def test_error(self, mocked_logging): + set_log_level(verbose_count=0) + mocked_logging.basicConfig.assert_called_once_with(level=mocked_logging.ERROR, format=LOGGING_FORMAT) diff --git a/tools/python/python_tests/mbed_tools/lib/test_python_helpers.py b/tools/python/python_tests/mbed_tools/lib/test_python_helpers.py new file mode 100644 index 0000000000..d676e47f4f --- /dev/null +++ b/tools/python/python_tests/mbed_tools/lib/test_python_helpers.py @@ -0,0 +1,22 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import pytest + +from mbed_tools.lib.python_helpers import flatten_nested + + +@pytest.mark.parametrize( + "input_list, expected_result", + ( + ([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]), + ([1, [2], 3, 4, 5], [1, 2, 3, 4, 5]), + ([[1, 2, 3], [4, 5]], [1, 2, 3, 4, 5]), + ([(1, 2, 3), (4, 5)], [1, 2, 3, 4, 5]), + ([[1, [[2, [3]]]], [4, 5]], [1, 2, 3, 4, 5]), + ([["alan", [["bob", ["sally"]]]], ["jim", "jenny"]], ["alan", "bob", "sally", "jim", "jenny"]), + ), +) +def test_flatten_nested_list(input_list, expected_result): + assert flatten_nested(input_list) == expected_result diff --git a/tools/python/python_tests/mbed_tools/project/__init__.py b/tools/python/python_tests/mbed_tools/project/__init__.py new file mode 100644 index 0000000000..0368d0f1a0 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/project/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tools/python/python_tests/mbed_tools/project/_internal/__init__.py b/tools/python/python_tests/mbed_tools/project/_internal/__init__.py new file mode 100644 index 0000000000..0368d0f1a0 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/project/_internal/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tools/python/python_tests/mbed_tools/project/_internal/test_git_utils.py b/tools/python/python_tests/mbed_tools/project/_internal/test_git_utils.py new file mode 100644 index 0000000000..c188d47aa5 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/project/_internal/test_git_utils.py @@ -0,0 +1,151 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from pathlib import Path +from unittest import mock + +import pytest + +from mbed_tools.project.exceptions import VersionControlError +from mbed_tools.project._internal import git_utils + + +@pytest.fixture +def mock_repo(): + with mock.patch("mbed_tools.project._internal.git_utils.git.Repo") as repo: + yield repo + + +@pytest.fixture +def mock_progress(): + with mock.patch("mbed_tools.project._internal.git_utils.ProgressReporter") as progress: + yield progress + + +class TestClone: + def test_returns_repo(self, mock_progress, mock_repo, tmp_path): + url = "https://blah" + path = Path(tmp_path, "dst") + repo = git_utils.clone(url, path) + + assert repo is not None + mock_repo.clone_from.assert_called_once_with(url=url, to_path=str(path), progress=mock_progress(), depth=1) + + def test_returns_repo_for_ref(self, mock_progress, mock_repo, tmp_path): + url = "https://example.com/org/repo.git" + ref = "development" + path = Path(tmp_path, "repo") + repo = git_utils.clone(url, path, ref) + + assert repo is not None + mock_repo.clone_from.assert_called_once_with( + url=url, to_path=str(path), progress=mock_progress(), depth=1, branch=ref + ) + + def test_raises_when_fails_due_to_bad_url(self, tmp_path): + with pytest.raises(VersionControlError, match="from url 'bad' failed"): + git_utils.clone("bad", Path(tmp_path, "dst")) + + def test_raises_when_fails_due_to_bad_url_with_ref(self, mock_progress, mock_repo, tmp_path): + url = "https://example.com/org/repo.git" + ref = "development" + path = Path(tmp_path, "repo") + + mock_repo.clone_from.side_effect = git_utils.git.exc.GitCommandError("git clone", 255) + + with pytest.raises(VersionControlError, match=f"Cloning git repository from url '{url}' failed."): + git_utils.clone(url, path, ref) + + def test_raises_when_fails_due_to_existing_nonempty_dst_dir(self, mock_repo, tmp_path): + dst_dir = Path(tmp_path, "dst") + dst_dir.mkdir() + (dst_dir / "some_file.txt").touch() + + with pytest.raises(VersionControlError, match="exists and is not an empty directory"): + git_utils.clone("https://blah", dst_dir) + + def test_can_clone_to_empty_existing_dst_dir(self, mock_repo, tmp_path, mock_progress): + dst_dir = Path(tmp_path, "dst") + dst_dir.mkdir() + url = "https://repo" + + repo = git_utils.clone(url, dst_dir) + + assert repo is not None + mock_repo.clone_from.assert_called_once_with(url=url, to_path=str(dst_dir), progress=mock_progress(), depth=1) + + +class TestInit: + def test_returns_initialised_repo(self, mock_repo): + repo = git_utils.init(Path()) + + assert repo is not None + mock_repo.init.assert_called_once_with(str(Path())) + + def test_raises_when_init_fails(self, mock_repo): + mock_repo.init.side_effect = git_utils.git.exc.GitCommandError("git init", 255) + + with pytest.raises(VersionControlError): + git_utils.init(Path()) + + +class TestGetRepo: + def test_returns_repo_object(self, mock_repo): + repo = git_utils.get_repo(Path()) + + assert isinstance(repo, mock_repo().__class__) + + def test_raises_version_control_error_when_no_git_repo_found(self, mock_repo): + mock_repo.side_effect = git_utils.git.exc.InvalidGitRepositoryError + + with pytest.raises(VersionControlError): + git_utils.get_repo(Path()) + + +class TestCheckout: + def test_git_lib_called_with_correct_command(self, mock_repo): + git_utils.checkout(mock_repo, "master") + + mock_repo.git.checkout.assert_called_once_with("master") + + def test_git_lib_called_with_correct_command_with_force(self, mock_repo): + git_utils.checkout(mock_repo, "master", force=True) + + mock_repo.git.checkout.assert_called_once_with("master", "--force") + + def test_raises_version_control_error_when_git_checkout_fails(self, mock_repo): + mock_repo.git.checkout.side_effect = git_utils.git.exc.GitCommandError("git checkout", 255) + + with pytest.raises(VersionControlError): + git_utils.checkout(mock_repo, "bad") + + +class TestFetch: + def test_does_a_fetch(self, mock_repo): + ref = "b23a8eb1c3f80292c8eb40689106759fae83a4c6" + git_utils.fetch(mock_repo, ref) + + mock_repo.git.fetch.assert_called_once_with("origin", ref) + + def test_raises_when_fetch_fails(self, mock_repo): + ref = "v2.7.9" + mock_repo.git.fetch.side_effect = git_utils.git.exc.GitCommandError("git fetch", 255) + + with pytest.raises(VersionControlError): + git_utils.fetch(mock_repo, ref) + + +class TestGetDefaultBranch: + def test_returns_default_branch_name(self, mock_repo): + mock_repo().git.symbolic_ref.return_value = "refs/remotes/origin/main" + + branch_name = git_utils.get_default_branch(mock_repo()) + + assert branch_name == "main" + + def test_raises_version_control_error_when_git_command_fails(self, mock_repo): + mock_repo().git.symbolic_ref.side_effect = git_utils.git.exc.GitCommandError("git symbolic_ref", 255) + + with pytest.raises(VersionControlError): + git_utils.get_default_branch(mock_repo()) diff --git a/tools/python/python_tests/mbed_tools/project/_internal/test_libraries.py b/tools/python/python_tests/mbed_tools/project/_internal/test_libraries.py new file mode 100644 index 0000000000..eade42b9cc --- /dev/null +++ b/tools/python/python_tests/mbed_tools/project/_internal/test_libraries.py @@ -0,0 +1,201 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import pathlib + +import pytest + +from unittest import mock + +from mbed_tools.project._internal.libraries import MbedLibReference, LibraryReferences +from mbed_tools.project.exceptions import VersionControlError +from python_tests.mbed_tools.project.factories import make_mbed_lib_reference + + +@pytest.fixture +def mock_clone(): + with mock.patch("mbed_tools.project._internal.git_utils.clone") as clone: + yield clone + + +@pytest.fixture +def mock_checkout(): + with mock.patch("mbed_tools.project._internal.git_utils.checkout") as checkout: + yield checkout + + +@pytest.fixture +def mock_fetch(): + with mock.patch("mbed_tools.project._internal.git_utils.fetch") as fetch: + yield fetch + + +@pytest.fixture +def mock_get_repo(): + with mock.patch("mbed_tools.project._internal.git_utils.get_repo") as get_repo: + yield get_repo + + +@pytest.fixture +def mock_get_default_branch(): + with mock.patch("mbed_tools.project._internal.git_utils.get_default_branch") as get_default_branch: + yield get_default_branch + + +@pytest.fixture +def mock_repo(): + with mock.patch("mbed_tools.project._internal.git_utils.git.Repo") as repo: + yield repo + + +class TestLibraryReferences: + def test_hydrates_top_level_library_references(self, mock_clone, tmp_path): + fs_root = pathlib.Path(tmp_path, "foo") + lib = make_mbed_lib_reference(fs_root, ref_url="https://git") + mock_clone.side_effect = lambda url, dst_dir, *args: dst_dir.mkdir() + + lib_refs = LibraryReferences(fs_root, ignore_paths=["mbed-os"]) + lib_refs.fetch() + + mock_clone.assert_called_once_with(lib.get_git_reference().repo_url, lib.source_code_path) + assert lib.is_resolved() + + def test_hydrates_recursive_dependencies(self, mock_clone, tmp_path): + fs_root = pathlib.Path(tmp_path, "foo") + lib = make_mbed_lib_reference(fs_root, ref_url="https://git") + # Create a lib reference without touching the fs at this point, we want to mock the effects of a recursive + # reference lookup and we need to assert the reference was resolved. + lib2 = MbedLibReference( + reference_file=(lib.source_code_path / "lib2.lib"), source_code_path=(lib.source_code_path / "lib2") + ) + # Here we mock the effects of a recursive reference lookup. We create a new lib reference as a side effect of + # the first call to the mock. Then we create the src dir, thus resolving the lib, on the second call. + mock_clone.side_effect = lambda url, dst_dir, *args: ( + make_mbed_lib_reference(pathlib.Path(dst_dir), name=lib2.reference_file.name, ref_url="https://valid2"), + lib2.source_code_path.mkdir(), + ) + + lib_refs = LibraryReferences(fs_root, ignore_paths=["mbed-os"]) + lib_refs.fetch() + + assert lib.is_resolved() + assert lib2.is_resolved() + + def test_does_perform_checkout_of_default_repo_branch_if_no_git_ref_exists( + self, mock_get_repo, mock_checkout, mock_fetch, mock_get_default_branch, mock_clone, tmp_path + ): + fs_root = pathlib.Path(tmp_path, "foo") + make_mbed_lib_reference(fs_root, ref_url="https://git", resolved=True) + + lib_refs = LibraryReferences(fs_root, ignore_paths=["mbed-os"]) + lib_refs.checkout(force=False) + + mock_fetch.assert_called_once_with(mock_get_repo(), mock_get_default_branch()) + mock_checkout.assert_called_once_with(mock_get_repo(), "FETCH_HEAD", force=False) + + def test_performs_checkout_if_git_ref_exists(self, mock_get_repo, mock_checkout, mock_fetch, mock_clone, tmp_path): + fs_root = pathlib.Path(tmp_path, "foo") + lib = make_mbed_lib_reference(fs_root, ref_url="https://git#lajdhalk234", resolved=True) + + lib_refs = LibraryReferences(fs_root, ignore_paths=["mbed-os"]) + lib_refs.checkout(force=False) + + mock_fetch.assert_called_once_with(mock_get_repo(), lib.get_git_reference().ref) + mock_checkout.assert_called_once_with(mock_get_repo.return_value, "FETCH_HEAD", force=False) + + def test_fetch_does_not_perform_checkout_if_no_git_ref_exists( + self, mock_get_repo, mock_checkout, mock_fetch, mock_clone, tmp_path + ): + fs_root = pathlib.Path(tmp_path, "foo") + make_mbed_lib_reference(fs_root, ref_url="https://git") + mock_clone.side_effect = lambda url, dst_dir, *args: dst_dir.mkdir() + + lib_refs = LibraryReferences(fs_root, ignore_paths=["mbed-os"]) + lib_refs.fetch() + + mock_fetch.assert_not_called() + mock_checkout.assert_not_called() + + def test_fetch_performs_checkout_if_ref_is_hash( + self, mock_get_repo, mock_clone, mock_fetch, mock_checkout, tmp_path + ): + num_times_called = 0 + + def clone_side_effect(url, dst_dir, *args): + nonlocal num_times_called + if num_times_called == 0: + num_times_called += 1 + raise VersionControlError("Failed to clone") + elif num_times_called == 1: + num_times_called += 1 + dst_dir.mkdir() + else: + assert False + + fs_root = pathlib.Path(tmp_path, "foo") + lib = make_mbed_lib_reference(fs_root, ref_url="https://git#398bc1a63370") + mock_clone.side_effect = clone_side_effect + + lib_refs = LibraryReferences(fs_root, ignore_paths=["mbed-os"]) + lib_refs.fetch() + + mock_clone.assert_called_with(lib.get_git_reference().repo_url, lib.source_code_path) + mock_fetch.assert_called_once_with(None, lib.get_git_reference().ref) + mock_checkout.assert_called_once_with(None, "FETCH_HEAD") + + def test_raises_when_no_such_ref(self, mock_repo, mock_clone, mock_fetch, mock_checkout, tmp_path): + num_times_called = 0 + + def clone_side_effect(url, dst_dir, *args): + nonlocal num_times_called + if num_times_called == 0: + num_times_called += 1 + raise VersionControlError("Failed to clone") + elif num_times_called == 1: + num_times_called += 1 + dst_dir.mkdir() + else: + assert False + + fs_root = pathlib.Path(tmp_path, "foo") + make_mbed_lib_reference(fs_root, ref_url="https://git#lajdhalk234") + + mock_clone.side_effect = clone_side_effect + mock_fetch.side_effect = None + mock_checkout.side_effect = VersionControlError("Failed to checkout") + + with pytest.raises(VersionControlError, match="Failed to checkout"): + lib_refs = LibraryReferences(fs_root, ignore_paths=["mbed-os"]) + lib_refs.fetch() + + def test_doesnt_fetch_for_branch_or_tag(self, mock_clone, mock_fetch, mock_checkout, tmp_path): + fs_root = pathlib.Path(tmp_path, "foo") + make_mbed_lib_reference(fs_root, ref_url="https://git#lajdhalk234") + + mock_clone.side_effect = lambda url, dst_dir, *args: dst_dir.mkdir() + + lib_refs = LibraryReferences(fs_root, ignore_paths=["mbed-os"]) + lib_refs.fetch() + + mock_fetch.assert_not_called() + mock_checkout.assert_not_called() + + def test_does_not_resolve_references_in_ignore_paths(self, mock_get_repo, mock_checkout, mock_clone, tmp_path): + fs_root = pathlib.Path(tmp_path, "mbed-os") + make_mbed_lib_reference(fs_root, ref_url="https://git#lajdhalk234") + + lib_refs = LibraryReferences(fs_root, ignore_paths=["mbed-os"]) + lib_refs.fetch() + + mock_clone.assert_not_called() + + def test_fetches_only_requested_ref(self, mock_repo, tmp_path): + fs_root = pathlib.Path(tmp_path, "foo") + fake_ref = "28eeee2b4c169739192600b92e7970dbbcabd8d0" + make_mbed_lib_reference(fs_root, ref_url=f"https://git#{fake_ref}", resolved=True) + + lib_refs = LibraryReferences(fs_root, ignore_paths=["mbed-os"]) + lib_refs.checkout(force=False) + + mock_repo().git.fetch.assert_called_once_with("origin", fake_ref) diff --git a/tools/python/python_tests/mbed_tools/project/_internal/test_progress.py b/tools/python/python_tests/mbed_tools/project/_internal/test_progress.py new file mode 100644 index 0000000000..d1bf32ba97 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/project/_internal/test_progress.py @@ -0,0 +1,43 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from unittest import TestCase, mock + +from mbed_tools.project._internal.progress import ProgressReporter, ProgressBar + + +class TestProgressBar(TestCase): + @mock.patch("mbed_tools.project._internal.progress.ProgressBar.update") + def test_updates_progress_bar_with_correct_block_size(self, mock_bar_update): + bar = ProgressBar(total=100) + bar.update_progress(1, 1) + + mock_bar_update.assert_called_once_with(1) + + def test_sets_total_attribute_to_value_of_total_size(self): + bar = ProgressBar() + + self.assertIsNone(bar.total) + + bar.update_progress(1, 2, total_size=33) + + self.assertEqual(bar.total, 33) + + +@mock.patch("mbed_tools.project._internal.progress.ProgressBar", autospec=True) +class TestProgressReporter(TestCase): + def test_creates_progress_bar_on_begin_opcode(self, mock_progress_bar): + reporter = ProgressReporter() + reporter._cur_line = "begin" + reporter.update(reporter.BEGIN, 1) + + mock_progress_bar.assert_called_once() + + def test_closes_progress_bar_on_end_opcode(self, mock_progress_bar): + reporter = ProgressReporter() + reporter.bar = mock_progress_bar() + reporter.update(reporter.END, 1) + + reporter.bar.close.assert_called_once() + reporter.bar.update.assert_not_called() diff --git a/tools/python/python_tests/mbed_tools/project/_internal/test_project_data.py b/tools/python/python_tests/mbed_tools/project/_internal/test_project_data.py new file mode 100644 index 0000000000..f3b05e30c0 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/project/_internal/test_project_data.py @@ -0,0 +1,105 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for project_data.py.""" +import pathlib +import pytest + +from unittest import mock + +from mbed_tools.project._internal.project_data import ( + MbedProgramFiles, + MbedOS, + MAIN_CPP_FILE_NAME, +) +from python_tests.mbed_tools.project.factories import make_mbed_lib_reference, make_mbed_program_files, make_mbed_os_files + + +class TestMbedProgramFiles: + def test_from_new_raises_if_program_files_already_exist(self, tmp_path): + root = pathlib.Path(tmp_path, "foo") + make_mbed_program_files(root) + + with pytest.raises(ValueError): + MbedProgramFiles.from_new(root) + + def test_from_new_returns_valid_program_file_set(self, tmp_path): + root = pathlib.Path(tmp_path, "foo") + root.mkdir() + + program = MbedProgramFiles.from_new(root) + + assert program.app_config_file.exists() + assert program.mbed_os_ref.exists() + assert program.cmakelists_file.exists() + + def test_from_new_calls_render_template_for_gitignore_and_main(self, tmp_path): + with mock.patch( + "mbed_tools.project._internal.project_data.render_cmakelists_template" + ) as render_cmakelists_template, mock.patch( + "mbed_tools.project._internal.project_data.render_main_cpp_template" + ) as render_main_cpp_template, mock.patch( + "mbed_tools.project._internal.project_data.render_gitignore_template" + ) as render_gitignore_template: + root = pathlib.Path(tmp_path, "foo") + root.mkdir() + program_files = MbedProgramFiles.from_new(root) + render_cmakelists_template.assert_called_once_with(program_files.cmakelists_file, "foo") + render_main_cpp_template.assert_called_once_with(root / MAIN_CPP_FILE_NAME) + render_gitignore_template.assert_called_once_with(root / ".gitignore") + + def test_from_existing_finds_existing_program_data(self, tmp_path): + root = pathlib.Path(tmp_path, "foo") + make_mbed_program_files(root) + + program = MbedProgramFiles.from_existing(root, pathlib.Path("K64F", "develop", "GCC_ARM")) + + assert program.app_config_file.exists() + assert program.mbed_os_ref.exists() + assert program.cmakelists_file.exists() + + +class TestMbedLibReference: + def test_is_resolved_returns_true_if_source_code_dir_exists(self, tmp_path): + root = pathlib.Path(tmp_path, "foo") + lib = make_mbed_lib_reference(root, resolved=True) + + assert lib.is_resolved() + + def test_is_resolved_returns_false_if_source_code_dir_doesnt_exist(self, tmp_path): + root = pathlib.Path(tmp_path, "foo") + lib = make_mbed_lib_reference(root) + + assert not lib.is_resolved() + + def test_get_git_reference_returns_lib_file_contents(self, tmp_path): + root = pathlib.Path(tmp_path, "foo") + url = "https://github.com/mylibrepo" + ref = "latest" + references = [f"{url}#{ref}", f"{url}/#{ref}"] + + for full_ref in references: + lib = make_mbed_lib_reference(root, ref_url=full_ref) + + reference = lib.get_git_reference() + + assert reference.repo_url == url + assert reference.ref == ref + + +class TestMbedOS: + def test_from_existing_finds_existing_mbed_os_data(self, tmp_path): + root_path = pathlib.Path(tmp_path, "my-version-of-mbed-os") + make_mbed_os_files(root_path) + + mbed_os = MbedOS.from_existing(root_path) + + assert mbed_os.targets_json_file == root_path / "targets" / "targets.json" + + def test_raises_if_files_missing(self, tmp_path): + root_path = pathlib.Path(tmp_path, "my-version-of-mbed-os") + root_path.mkdir() + + with pytest.raises(ValueError): + MbedOS.from_existing(root_path) diff --git a/tools/python/python_tests/mbed_tools/project/_internal/test_render_templates.py b/tools/python/python_tests/mbed_tools/project/_internal/test_render_templates.py new file mode 100644 index 0000000000..3c4a74e4e8 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/project/_internal/test_render_templates.py @@ -0,0 +1,44 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from pathlib import Path +from unittest import mock + +from mbed_tools.project._internal.render_templates import ( + render_cmakelists_template, + render_main_cpp_template, + render_gitignore_template, +) + + +@mock.patch("mbed_tools.project._internal.render_templates.datetime") +class TestRenderTemplates: + def test_renders_cmakelists_template(self, mock_datetime, tmp_path): + the_year = 3999 + mock_datetime.datetime.now.return_value.year = the_year + program_name = "mytestprogram" + file_path = Path(tmp_path, "mytestpath") + + render_cmakelists_template(file_path, program_name) + output = file_path.read_text() + + assert str(the_year) in output + assert program_name in output + + def test_renders_main_cpp_template(self, mock_datetime, tmp_path): + the_year = 3999 + mock_datetime.datetime.now.return_value.year = the_year + file_path = Path(tmp_path, "mytestpath") + + render_main_cpp_template(file_path) + + assert str(the_year) in file_path.read_text() + + def test_renders_gitignore_template(self, _, tmp_path): + file_path = Path(tmp_path, "mytestpath") + + render_gitignore_template(file_path) + + assert "cmake_build" in file_path.read_text() + assert ".mbedbuild" in file_path.read_text() diff --git a/tools/python/python_tests/mbed_tools/project/factories.py b/tools/python/python_tests/mbed_tools/project/factories.py new file mode 100644 index 0000000000..f10b9343e6 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/project/factories.py @@ -0,0 +1,45 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from mbed_tools.project._internal.libraries import MbedLibReference +from mbed_tools.project._internal.project_data import ( + CMAKELISTS_FILE_NAME, + APP_CONFIG_FILE_NAME, + MBED_OS_REFERENCE_FILE_NAME, +) + + +def make_mbed_program_files(root, config_file_name=APP_CONFIG_FILE_NAME): + if not root.exists(): + root.mkdir() + + (root / MBED_OS_REFERENCE_FILE_NAME).touch() + (root / config_file_name).touch() + (root / CMAKELISTS_FILE_NAME).touch() + + +def make_mbed_lib_reference(root, name="mylib.lib", resolved=False, ref_url=None): + ref_file = root / name + source_dir = ref_file.with_suffix("") + if not root.exists(): + root.mkdir() + + ref_file.touch() + + if resolved: + source_dir.mkdir() + + if ref_url is not None: + ref_file.write_text(ref_url) + + return MbedLibReference(reference_file=ref_file, source_code_path=source_dir) + + +def make_mbed_os_files(root): + if not root.exists(): + root.mkdir() + + targets_dir = root / "targets" + targets_dir.mkdir() + (targets_dir / "targets.json").touch() diff --git a/tools/python/python_tests/mbed_tools/project/test_mbed_program.py b/tools/python/python_tests/mbed_tools/project/test_mbed_program.py new file mode 100644 index 0000000000..50eb17cbf4 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/project/test_mbed_program.py @@ -0,0 +1,149 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import os +import pathlib +import pytest + + +from mbed_tools.project import MbedProgram +from mbed_tools.project.exceptions import ExistingProgram, ProgramNotFound, MbedOSNotFound +from mbed_tools.project.mbed_program import _find_program_root, parse_url +from mbed_tools.project._internal.project_data import MbedProgramFiles +from python_tests.mbed_tools.project.factories import make_mbed_program_files, make_mbed_os_files + + +DEFAULT_BUILD_SUBDIR = pathlib.Path("K64F", "develop", "GCC_ARM") + + +def from_new_set_target_toolchain(program_root): + program = MbedProgram.from_new(program_root) + parent_build_dir = program.files.cmake_build_dir + program.files.cmake_build_dir = parent_build_dir / DEFAULT_BUILD_SUBDIR + return program + + +class TestInitialiseProgram: + def test_from_new_local_dir_raises_if_path_is_existing_program(self, tmp_path): + program_root = pathlib.Path(tmp_path, "programfoo") + program_root.mkdir() + (program_root / "mbed-os.lib").touch() + + with pytest.raises(ExistingProgram): + MbedProgram.from_new(program_root) + + def test_from_new_local_dir_generates_valid_program_creating_directory(self, tmp_path): + fs_root = pathlib.Path(tmp_path, "foo") + fs_root.mkdir() + program_root = fs_root / "programfoo" + + program = from_new_set_target_toolchain(program_root) + + assert program.files == MbedProgramFiles.from_existing(program_root, DEFAULT_BUILD_SUBDIR) + + def test_from_new_local_dir_generates_valid_program_creating_directory_in_cwd(self, tmp_path): + old_cwd = os.getcwd() + try: + fs_root = pathlib.Path(tmp_path, "foo") + fs_root.mkdir() + os.chdir(fs_root) + program_root = pathlib.Path("programfoo") + + program = from_new_set_target_toolchain(program_root) + + assert program.files == MbedProgramFiles.from_existing(program_root, DEFAULT_BUILD_SUBDIR) + finally: + os.chdir(old_cwd) + + def test_from_new_local_dir_generates_valid_program_existing_directory(self, tmp_path): + fs_root = pathlib.Path(tmp_path, "foo") + fs_root.mkdir() + program_root = fs_root / "programfoo" + program_root.mkdir() + + program = from_new_set_target_toolchain(program_root) + + assert program.files == MbedProgramFiles.from_existing(program_root, DEFAULT_BUILD_SUBDIR) + + def test_from_existing_raises_if_path_is_not_a_program(self, tmp_path): + fs_root = pathlib.Path(tmp_path, "foo") + fs_root.mkdir() + program_root = fs_root / "programfoo" + + with pytest.raises(ProgramNotFound): + MbedProgram.from_existing(program_root, DEFAULT_BUILD_SUBDIR) + + def test_from_existing_raises_if_no_mbed_os_dir_found_and_check_mbed_os_is_true(self, tmp_path): + fs_root = pathlib.Path(tmp_path, "foo") + make_mbed_program_files(fs_root) + + with pytest.raises(MbedOSNotFound): + MbedProgram.from_existing(fs_root, DEFAULT_BUILD_SUBDIR, check_mbed_os=True) + + def test_from_existing_returns_valid_program(self, tmp_path): + fs_root = pathlib.Path(tmp_path, "foo") + make_mbed_program_files(fs_root) + make_mbed_os_files(fs_root / "mbed-os") + + program = MbedProgram.from_existing(fs_root, DEFAULT_BUILD_SUBDIR) + + assert program.files.app_config_file.exists() + assert program.mbed_os.root.exists() + + def test_from_existing_with_mbed_os_path_returns_valid_program(self, tmp_path): + fs_root = pathlib.Path(tmp_path, "foo") + mbed_os_path = fs_root / "extern/mbed-os" + mbed_os_path.mkdir(parents=True) + make_mbed_program_files(fs_root) + make_mbed_os_files(mbed_os_path) + + program = MbedProgram.from_existing(fs_root, DEFAULT_BUILD_SUBDIR, mbed_os_path) + + assert program.files.app_config_file.exists() + assert program.mbed_os.root.exists() + + +class TestParseURL: + def test_creates_url_and_dst_dir_from_name(self): + name = "mbed-os-example-blows-up-board" + data = parse_url(name) + + assert data["url"] == f"https://github.com/armmbed/{name}" + assert data["dst_path"] == name + + def test_creates_valid_dst_dir_from_url(self): + url = "https://superversioncontrol/superorg/mbed-os-example-numskull" + data = parse_url(url) + + assert data["url"] == url + assert data["dst_path"] == "mbed-os-example-numskull" + + def test_creates_valid_dst_dir_from_ssh_url(self): + url = "git@superversioncontrol:superorg/mbed-os-example-numskull" + data = parse_url(url) + assert data["url"] == url + assert data["dst_path"] == "mbed-os-example-numskull" + + +class TestFindProgramRoot: + def test_finds_program_higher_in_dir_tree(self, tmp_path): + program_root = pathlib.Path(tmp_path, "foo") + pwd = program_root / "subprojfoo" / "libbar" + make_mbed_program_files(program_root) + pwd.mkdir(parents=True) + + assert _find_program_root(pwd) == program_root.resolve() + + def test_finds_program_at_current_path(self, tmp_path): + program_root = pathlib.Path(tmp_path, "foo") + make_mbed_program_files(program_root) + + assert _find_program_root(program_root) == program_root.resolve() + + def test_raises_if_no_program_found(self, tmp_path): + program_root = pathlib.Path(tmp_path, "foo") + program_root.mkdir() + + with pytest.raises(ProgramNotFound): + _find_program_root(program_root) diff --git a/tools/python/python_tests/mbed_tools/project/test_mbed_project.py b/tools/python/python_tests/mbed_tools/project/test_mbed_project.py new file mode 100644 index 0000000000..ae84e05c5a --- /dev/null +++ b/tools/python/python_tests/mbed_tools/project/test_mbed_project.py @@ -0,0 +1,85 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import pathlib + +import pytest + +from unittest import mock + +from mbed_tools.project import initialise_project, import_project, deploy_project, get_known_libs + + +@pytest.fixture +def mock_libs(): + with mock.patch("mbed_tools.project.project.LibraryReferences") as libs: + yield libs + + +@pytest.fixture +def mock_program(): + with mock.patch("mbed_tools.project.project.MbedProgram") as prog: + yield prog + + +@pytest.fixture +def mock_git(): + with mock.patch("mbed_tools.project.project.git_utils") as gutils: + yield gutils + + +class TestInitialiseProject: + def test_fetches_mbed_os_when_create_only_is_false(self, mock_libs, mock_program): + path = pathlib.Path() + initialise_project(path, create_only=False) + + mock_program.from_new.assert_called_once_with(path) + mock_libs().fetch.assert_called_once() + + def test_skips_mbed_os_when_create_only_is_true(self, mock_libs, mock_program): + path = pathlib.Path() + initialise_project(path, create_only=True) + + mock_program.from_new.assert_called_once_with(path) + mock_libs().fetch.assert_not_called() + + +class TestImportProject: + def test_clones_from_remote(self, mock_git): + url = "https://git.com/gitorg/repo" + import_project(url, recursive=False) + + mock_git.clone.assert_called_once_with(url, pathlib.Path(url.rsplit("/", maxsplit=1)[-1])) + + def test_resolves_libs_when_recursive_is_true(self, mock_git, mock_libs): + url = "https://git.com/gitorg/repo" + import_project(url, recursive=True) + + mock_git.clone.assert_called_once_with(url, pathlib.Path(url.rsplit("/", maxsplit=1)[-1])) + mock_libs().fetch.assert_called_once() + + +class TestDeployProject: + def test_checks_out_libraries(self, mock_libs): + path = pathlib.Path("somewhere") + deploy_project(path, force=False) + + mock_libs().checkout.assert_called_once_with(force=False) + + def test_resolves_libs_if_unresolved_detected(self, mock_libs): + mock_libs().iter_unresolved.return_value = [1] + path = pathlib.Path("somewhere") + deploy_project(path) + + mock_libs().fetch.assert_called_once() + + +class TestPrintLibs: + def test_list_libraries_gets_known_lib_list(self, mock_libs): + path = pathlib.Path("somewhere") + mock_libs().iter_resolved.return_value = ["", ""] + + libs = get_known_libs(path) + + assert libs == ["", ""] diff --git a/tools/python/python_tests/mbed_tools/regression/test_configure.py b/tools/python/python_tests/mbed_tools/regression/test_configure.py new file mode 100644 index 0000000000..eaa01ad625 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/regression/test_configure.py @@ -0,0 +1,59 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# + +import tempfile +import pathlib +import json + +from unittest import TestCase + +from click.testing import CliRunner + +from mbed_tools.cli.configure import configure + +target_json = json.dumps( + { + "TARGET": { + "core": None, + "trustzone": False, + "default_toolchain": "ARM", + "supported_toolchains": None, + "extra_labels": [], + "supported_form_factors": [], + "components": [], + "is_disk_virtual": False, + "macros": [], + "device_has": [], + "features": [], + "detect_code": [], + "c_lib": "std", + "bootloader_supported": False, + "static_memory_defines": True, + "printf_lib": "minimal-printf", + "supported_c_libs": {"arm": ["std"], "gcc_arm": ["std", "small"], "iar": ["std"]}, + "supported_application_profiles": ["full"], + } + } +) + +mbed_app_json = json.dumps( + {"target_overrides": {"*": {"target.c_lib": "small", "target.printf_lib": "minimal-printf"}}} +) + + +class TestConfigureRegression(TestCase): + def test_generate_config_called_with_correct_arguments(self): + with tempfile.TemporaryDirectory() as tmpDir: + tmpDirPath = pathlib.Path(tmpDir) + pathlib.Path(tmpDirPath / "mbed-os.lib").write_text("https://github.com/ARMmbed/mbed-os") + pathlib.Path(tmpDirPath / "mbed_app.json").write_text(mbed_app_json) + pathlib.Path(tmpDirPath / "mbed-os").mkdir() + pathlib.Path(tmpDirPath / "mbed-os" / "targets").mkdir() + pathlib.Path(tmpDirPath / "mbed-os" / "targets" / "targets.json").write_text(target_json) + + result = CliRunner().invoke( + configure, ["-m", "Target", "-t", "gcc_arm", "-p", tmpDir], catch_exceptions=False + ) + self.assertIn("mbed_config.cmake has been generated and written to", result.output) diff --git a/tools/python/python_tests/mbed_tools/sterm/test_terminal.py b/tools/python/python_tests/mbed_tools/sterm/test_terminal.py new file mode 100644 index 0000000000..1c7be667e4 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/sterm/test_terminal.py @@ -0,0 +1,225 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from unittest import mock + +import sys + +import pytest + +from mbed_tools.sterm import terminal + + +@pytest.fixture +def mock_serial(): + with mock.patch("mbed_tools.sterm.terminal.Serial") as serial: + yield serial + + +@pytest.fixture +def mock_console(): + with mock.patch("serial.tools.miniterm.Console") as console: + yield console + + +@pytest.fixture +def mock_sterm(): + with mock.patch("mbed_tools.sterm.terminal.SerialTerminal") as sterm: + yield sterm + + +def test_initialises_serial_port(mock_sterm, mock_serial): + port = "tty.1111" + baud = 9600 + + terminal.run(port, baud) + + mock_serial.assert_called_once_with(port=port, baudrate=str(baud)) + + +def test_initialises_sterm(mock_sterm, mock_serial): + port = "tty.1111" + baud = "9600" + + terminal.run(port, baud) + + mock_sterm.assert_called_once_with(mock_serial(), echo=True) + + +def test_starts_sterm_thread(mock_sterm, mock_serial): + terminal.run("tty.122", 9600) + + mock_sterm().start.assert_called_once() + + +def test_joins_tx_and_rx_threads(mock_sterm, mock_serial): + terminal.run("tty.122", 9600) + + mock_sterm().join.assert_any_call(True) + + +def test_joins_tx_thread_after_keyboard_interrupt(mock_sterm, mock_serial): + mock_sterm().join.side_effect = (KeyboardInterrupt(), None) + + terminal.run("tty.122", 9600) + + mock_sterm().join.assert_called_with() + + +def test_closes_sterm(mock_sterm, mock_serial): + terminal.run("tty.122", 9600) + + mock_sterm().close.assert_called_once() + + +def test_closes_sterm_after_exception(mock_sterm, mock_serial): + mock_sterm().join.side_effect = (Exception(), None) + with pytest.raises(Exception): + terminal.run("tty.122", 9600) + + mock_sterm().close.assert_called_once() + + +def test_closes_sterm_after_keyboard_interrupt(mock_sterm, mock_serial): + mock_sterm().join.side_effect = (KeyboardInterrupt(), None) + terminal.run("tty.122", 9600) + + mock_sterm().close.assert_called_once() + + +def test_sets_terminal_special_chars(mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial()) + + assert term.exit_character == terminal.CTRL_C + assert term.menu_character == terminal.CTRL_T + assert term.reset_character == terminal.CTRL_B + assert term.help_character == terminal.CTRL_H + + +def test_sets_terminal_rx_and_tx_encoding_to_utf8(mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial()) + + assert term.input_encoding == "UTF-8" + assert term.output_encoding == "UTF-8" + + +def test_stops_terminal_when_ctrl_c_received(mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial()) + term.alive = True + mock_console().getkey.return_value = terminal.CTRL_C + + term.writer() + + assert term.alive is False + + +def test_stops_terminal_on_keyboard_interrupt(mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial()) + term.alive = True + mock_console().getkey.side_effect = KeyboardInterrupt() + + term.writer() + + assert term.alive is False + + +@pytest.mark.parametrize("menu_key", terminal.VALID_MENU_KEYS) +def test_handles_valid_menu_key(menu_key, mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial()) + term.handle_menu_key = mock.Mock() + term.alive = True + mock_console().getkey.side_effect = (terminal.CTRL_T, menu_key, terminal.CTRL_C) + + term.writer() + + term.handle_menu_key.assert_called_once_with(menu_key) + + +INVALID_MENU_KEYS = tuple(set(chr(i) for i in range(0, 127)) - set(terminal.VALID_MENU_KEYS) - set([terminal.CTRL_H])) + + +@pytest.mark.parametrize("menu_key", INVALID_MENU_KEYS) +def test_ignores_invalid_menu_key(menu_key, mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial()) + term.handle_menu_key = mock.Mock() + term.alive = True + mock_console().getkey.side_effect = (terminal.CTRL_T, menu_key) + + with pytest.raises(StopIteration): + term.writer() + + term.handle_menu_key.assert_not_called() + + +def test_reset_sends_serial_break(mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial()) + + term.reset() + + mock_serial().sendBreak.assert_called_once() + + +def test_ctrl_b_sends_reset_to_serial_port(mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial()) + term.alive = True + mock_console().getkey.side_effect = (terminal.CTRL_B,) + + with pytest.raises(StopIteration): + term.writer() + + mock_serial().sendBreak.assert_called_once() + + +def test_ctrl_h_prints_help_text(mock_serial, mock_console): + sys.stderr.write = mock.Mock() + term = terminal.SerialTerminal(mock_serial()) + term.alive = True + mock_console().getkey.side_effect = (terminal.CTRL_H,) + + with pytest.raises(StopIteration): + term.writer() + + sys.stderr.write.assert_called_once_with(term.get_help_text()) + + +def test_help_text_is_correct(mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial()) + + assert term.get_help_text() == terminal.HELP_TEXT + + +def test_writes_normal_char_to_serial_output(mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial()) + term.alive = True + normal_char = "h" + mock_console().getkey.side_effect = (normal_char,) + + with pytest.raises(StopIteration): + term.writer() + + mock_serial().write.assert_called_once_with(term.tx_encoder.encode(normal_char)) + + +def test_echo_to_console_is_default_disabled(mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial()) + term.alive = True + normal_char = "h" + mock_console().getkey.side_effect = (normal_char,) + + with pytest.raises(StopIteration): + term.writer() + + mock_console().write.assert_not_called() + + +def test_echo_to_console_can_be_enabled(mock_serial, mock_console): + term = terminal.SerialTerminal(mock_serial(), echo=True) + term.alive = True + normal_char = "h" + mock_console().getkey.side_effect = (normal_char,) + + with pytest.raises(StopIteration): + term.writer() + + mock_console().write.assert_called_once_with(normal_char) diff --git a/tools/python/python_tests/mbed_tools/targets/__init__.py b/tools/python/python_tests/mbed_tools/targets/__init__.py new file mode 100644 index 0000000000..ed3894a7dc --- /dev/null +++ b/tools/python/python_tests/mbed_tools/targets/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for the mbed-targets modules.""" diff --git a/tools/python/python_tests/mbed_tools/targets/_internal/targets_json_parsers/test_accumulating_attribute_parser.py b/tools/python/python_tests/mbed_tools/targets/_internal/targets_json_parsers/test_accumulating_attribute_parser.py new file mode 100644 index 0000000000..d52d73bdf4 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/targets/_internal/targets_json_parsers/test_accumulating_attribute_parser.py @@ -0,0 +1,187 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for parsing the attributes for targets in targets.json that accumulate.""" +from unittest import TestCase, mock + +from mbed_tools.targets._internal.targets_json_parsers.accumulating_attribute_parser import ( + ALL_ACCUMULATING_ATTRIBUTES, + get_accumulating_attributes_for_target, + _targets_accumulate_hierarchy, + _determine_accumulated_attributes, + _remove_attribute_element, + _element_matches, +) + + +class ListAllAccumulatingAttributes(TestCase): + def test_expected_list(self): + expected_attributes = ( + "extra_labels", + "macros", + "device_has", + "features", + "components", + "extra_labels_add", + "extra_labels_remove", + "macros_add", + "macros_remove", + "device_has_add", + "device_has_remove", + "features_add", + "features_remove", + "components_add", + "components_remove", + ) + self.assertEqual(ALL_ACCUMULATING_ATTRIBUTES, expected_attributes) + + +class TestGetAccumulatingAttributes(TestCase): + @mock.patch( + "mbed_tools.targets._internal.targets_json_parsers." + "accumulating_attribute_parser._targets_accumulate_hierarchy" + ) + @mock.patch( + "mbed_tools.targets._internal.targets_json_parsers." + "accumulating_attribute_parser._determine_accumulated_attributes" + ) + def test_correctly_calls(self, _determine_accumulated_attributes, _targets_accumulate_hierarchy): + target_name = "Target_Name" + all_targets_data = {target_name: {"attribute_1": ["something"]}} + result = get_accumulating_attributes_for_target(all_targets_data, target_name) + + _targets_accumulate_hierarchy.assert_called_once_with(all_targets_data, target_name) + _determine_accumulated_attributes.assert_called_once_with(_targets_accumulate_hierarchy.return_value) + self.assertEqual(result, _determine_accumulated_attributes.return_value) + + +class TestParseHierarchy(TestCase): + def test_accumulate_hierarchy_single_inheritance(self): + all_targets_data = { + "D": {"attribute_1": ["some things"]}, + "C": {"inherits": ["D"], "attribute_2": "something else"}, + "B": {}, + "A": {"inherits": ["C"], "attribute_3": ["even more things"]}, + } + result = _targets_accumulate_hierarchy(all_targets_data, "A") + + self.assertEqual(result, [all_targets_data["A"], all_targets_data["C"], all_targets_data["D"]]) + + def test_accumulate_hierarchy_multiple_inheritance(self): + all_targets_data = { + "F": {"attribute_1": "some thing"}, + "E": {"attribute_2": "some other thing"}, + "D": {"inherits": ["F"]}, + "C": {"inherits": ["E"]}, + "B": {"inherits": ["C", "D"]}, + "A": {"inherits": ["B"]}, + } + result = _targets_accumulate_hierarchy(all_targets_data, "A") + + self.assertEqual( + result, + [ + all_targets_data["A"], + all_targets_data["B"], + all_targets_data["C"], + all_targets_data["D"], + all_targets_data["E"], + all_targets_data["F"], + ], + ) + + +class TestAccumulatingAttributes(TestCase): + def test_determine_accumulated_attributes_basic_add(self): + accumulation_order = [ + {"attribute_1": "something"}, + {f"{ALL_ACCUMULATING_ATTRIBUTES[0]}_add": ["2", "3"]}, + {ALL_ACCUMULATING_ATTRIBUTES[0]: ["1"]}, + ] + expected_attributes = {ALL_ACCUMULATING_ATTRIBUTES[0]: ["1", "2", "3"]} + result = _determine_accumulated_attributes(accumulation_order) + self.assertEqual(result, expected_attributes) + + def test_determine_accumulated_attributes_basic_remove(self): + accumulation_order = [ + {"attribute_1": "something"}, + {f"{ALL_ACCUMULATING_ATTRIBUTES[0]}_remove": ["2", "3"]}, + {ALL_ACCUMULATING_ATTRIBUTES[0]: ["1", "2", "3"]}, + ] + expected_attributes = {ALL_ACCUMULATING_ATTRIBUTES[0]: ["1"]} + result = _determine_accumulated_attributes(accumulation_order) + self.assertEqual(result, expected_attributes) + + def test_combination_multiple_attributes(self): + accumulation_order = [ + {f"{ALL_ACCUMULATING_ATTRIBUTES[0]}_add": ["2", "3"]}, + {f"{ALL_ACCUMULATING_ATTRIBUTES[1]}_remove": ["B", "C"]}, + {ALL_ACCUMULATING_ATTRIBUTES[0]: ["1"]}, + {ALL_ACCUMULATING_ATTRIBUTES[1]: ["A", "B", "C"]}, + ] + expected_attributes = { + ALL_ACCUMULATING_ATTRIBUTES[0]: ["1", "2", "3"], + ALL_ACCUMULATING_ATTRIBUTES[1]: ["A"], + } + result = _determine_accumulated_attributes(accumulation_order) + self.assertEqual(result, expected_attributes) + + def test_combination_later_check_no_unwanted_overrides(self): + accumulation_order = [ + {f"{ALL_ACCUMULATING_ATTRIBUTES[0]}_add": ["2", "3"]}, + {f"{ALL_ACCUMULATING_ATTRIBUTES[1]}_remove": ["B", "C"]}, + {ALL_ACCUMULATING_ATTRIBUTES[0]: ["1"]}, + {ALL_ACCUMULATING_ATTRIBUTES[1]: ["A", "B", "C"]}, + {ALL_ACCUMULATING_ATTRIBUTES[1]: []}, + ] + expected_attributes = { + ALL_ACCUMULATING_ATTRIBUTES[0]: ["1", "2", "3"], + ALL_ACCUMULATING_ATTRIBUTES[1]: ["A"], + } + result = _determine_accumulated_attributes(accumulation_order) + self.assertEqual(result, expected_attributes) + + +class TestElementMatches(TestCase): + def test_element_matches_exactly(self): + element_to_remove = "SOMETHING" + element_to_check = "SOMETHING" + + self.assertTrue(_element_matches(element_to_remove, element_to_check)) + + def test_element_no_match(self): + element_to_remove = "SOMETHING" + element_to_check = "SOMETHING_ELSE" + + self.assertFalse(_element_matches(element_to_remove, element_to_check)) + + def test_element_matches_with_number_arg(self): + element_to_remove = "SOMETHING" + element_to_check = "SOMETHING=5" + + self.assertTrue(_element_matches(element_to_remove, element_to_check)) + + def test_element_no_match_with_number_arg(self): + element_to_remove = "SOMETHING" + element_to_check = "SOMETHING_DIFFERENT=5" + + self.assertFalse(_element_matches(element_to_remove, element_to_check)) + + +class TestRemoveAttributeElement(TestCase): + def test_remove_element_without_numbers(self): + current_attribute_state = {"attribute_1": ["ONE", "TWO=2", "THREE"]} + elements_to_remove = ["ONE", "THREE"] + expected_result = {"attribute_1": ["TWO=2"]} + result = _remove_attribute_element(current_attribute_state, "attribute_1", elements_to_remove) + + self.assertEqual(result, expected_result) + + def test_remove_element_with_numbers(self): + current_attribute_state = {"attribute_1": ["ONE", "TWO=2", "THREE"]} + elements_to_remove = ["TWO"] + expected_result = {"attribute_1": ["ONE", "THREE"]} + result = _remove_attribute_element(current_attribute_state, "attribute_1", elements_to_remove) + + self.assertEqual(result, expected_result) diff --git a/tools/python/python_tests/mbed_tools/targets/_internal/targets_json_parsers/test_overriding_attribute_parser.py b/tools/python/python_tests/mbed_tools/targets/_internal/targets_json_parsers/test_overriding_attribute_parser.py new file mode 100644 index 0000000000..8fc09ab15b --- /dev/null +++ b/tools/python/python_tests/mbed_tools/targets/_internal/targets_json_parsers/test_overriding_attribute_parser.py @@ -0,0 +1,157 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for parsing the attributes for targets in targets.json that override.""" +from unittest import TestCase, mock + +from mbed_tools.targets._internal.targets_json_parsers.overriding_attribute_parser import ( + get_overriding_attributes_for_target, + get_labels_for_target, + _targets_override_hierarchy, + _determine_overridden_attributes, + _remove_unwanted_attributes, + MERGING_ATTRIBUTES, +) +from mbed_tools.targets._internal.targets_json_parsers.accumulating_attribute_parser import ALL_ACCUMULATING_ATTRIBUTES + + +class TestGetOverridingAttributes(TestCase): + @mock.patch( + "mbed_tools.targets._internal.targets_json_parsers." "overriding_attribute_parser._targets_override_hierarchy" + ) + @mock.patch( + "mbed_tools.targets._internal.targets_json_parsers." + "overriding_attribute_parser._determine_overridden_attributes" + ) + def test_correctly_calls(self, _determine_overridden_attributes, _targets_override_hierarchy): + target_name = "Target_Name" + all_targets_data = {target_name: {"attribute_1": ["something"]}} + result = get_overriding_attributes_for_target(all_targets_data, target_name) + + _targets_override_hierarchy.assert_called_once_with(all_targets_data, target_name) + _determine_overridden_attributes.assert_called_once_with(_targets_override_hierarchy.return_value) + self.assertEqual(result, _determine_overridden_attributes.return_value) + + +class TestParseHierarchy(TestCase): + def test_overwrite_hierarchy_single_inheritance(self): + all_targets_data = { + "D": {"attribute_1": ["some things"]}, + "C": {"inherits": ["D"], "attribute_2": "something else"}, + "B": {}, + "A": {"inherits": ["C"], "attribute_3": ["even more things"]}, + } + result = _targets_override_hierarchy(all_targets_data, "A") + + self.assertEqual(result, [all_targets_data["A"], all_targets_data["C"], all_targets_data["D"]]) + + def test_overwrite_hierarchy_multiple_inheritance(self): + all_targets_data = { + "F": {"attribute_1": "some thing"}, + "E": {"attribute_2": "some other thing"}, + "D": {"inherits": ["F"]}, + "C": {"inherits": ["E"]}, + "B": {"inherits": ["C", "D"]}, + "A": {"inherits": ["B"]}, + } + result = _targets_override_hierarchy(all_targets_data, "A") + + self.assertEqual( + result, + [ + all_targets_data["A"], + all_targets_data["B"], + all_targets_data["C"], + all_targets_data["E"], + all_targets_data["D"], + all_targets_data["F"], + ], + ) + + +class TestOverridingAttributes(TestCase): + def test_determine_overwritten_attributes(self): + override_order = [ + {"attribute_1": "1"}, + {"attribute_1": "I should be overridden", "attribute_2": "2"}, + {"attribute_3": "3"}, + ] + expected_attributes = {"attribute_1": "1", "attribute_2": "2", "attribute_3": "3"} + + result = _determine_overridden_attributes(override_order) + self.assertEqual(result, expected_attributes) + + def test_remove_accumulating_attributes(self): + override_order = [ + {ALL_ACCUMULATING_ATTRIBUTES[0]: "1"}, + {"attribute": "Normal override attribute"}, + {ALL_ACCUMULATING_ATTRIBUTES[1]: "3"}, + ] + expected_attributes = {"attribute": "Normal override attribute"} + + result = _determine_overridden_attributes(override_order) + self.assertEqual(result, expected_attributes) + + def test_merging_attributes(self): + override_order = [ + {MERGING_ATTRIBUTES[0]: {"FOO": "I should also remain"}}, + {MERGING_ATTRIBUTES[0]: {"FOO": "I should not remain"}}, + {MERGING_ATTRIBUTES[0]: {"BAR": "I should remain"}}, + ] + expected_attributes = {MERGING_ATTRIBUTES[0]: {"BAR": "I should remain", "FOO": "I should also remain"}} + + result = _determine_overridden_attributes(override_order) + self.assertEqual(result, expected_attributes) + + +class TestGetLabelsForTarget(TestCase): + def test_linear_inheritance(self): + all_targets_data = { + "C": {"attribute_1": ["some things"]}, + "B": {"inherits": ["C"], "attribute_2": "something else"}, + "A": {"inherits": ["B"], "attribute_3": ["even more things"]}, + } + target_name = "A" + expected_result = {"A", "B", "C"} + result = get_labels_for_target(all_targets_data, target_name) + + self.assertSetEqual(result, expected_result) + + def test_multiple_inheritance(self): + all_targets_data = { + "E": {"attribute_1": "some thing"}, + "D": {"attribute_2": "some other thing"}, + "C": {"inherits": ["E"]}, + "B": {"inherits": ["C", "D"]}, + "A": {"inherits": ["B"]}, + } + target_name = "A" + expected_result = {"A", "B", "C", "D", "E"} + result = get_labels_for_target(all_targets_data, target_name) + + self.assertSetEqual(result, expected_result) + + def test_no_inheritance(self): + all_targets_data = { + "A": {"attribute_3": ["some things"]}, + } + target_name = "A" + expected_result = {"A"} + result = get_labels_for_target(all_targets_data, target_name) + + self.assertSetEqual(result, expected_result) + + +class TestRemoveUnwantedAttributes(TestCase): + def test_removes_accumulating_public_and_inherits(self): + target_attributes = { + ALL_ACCUMULATING_ATTRIBUTES[0]: "1", + "public": False, + "inherits": "SomeOtherBoard", + "attribute": "I should remain", + } + expected_result = {"attribute": "I should remain"} + + result = _remove_unwanted_attributes(target_attributes) + self.assertEqual(result, expected_result) diff --git a/tools/python/python_tests/mbed_tools/targets/_internal/test_board_database.py b/tools/python/python_tests/mbed_tools/targets/_internal/test_board_database.py new file mode 100644 index 0000000000..7f04151bc9 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/targets/_internal/test_board_database.py @@ -0,0 +1,148 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for `mbed_tools.targets._internal.board_database`.""" + +from unittest import mock + +import logging +import pytest + +import mbed_tools.targets._internal.board_database as board_database +from mbed_tools.targets.env import env + + +class TestGetOnlineBoardData: + """Tests for the method `board_database.get_online_board_data`.""" + + def test_401(self, caplog, requests_mock): + """Given a 401 error code, BoardAPIError is raised.""" + caplog.set_level(logging.DEBUG) + requests_mock.get(board_database._BOARD_API, status_code=401, text="Who are you?") + with pytest.raises(board_database.BoardAPIError): + board_database.get_online_board_data() + assert any( + x for x in caplog.records if x.levelno == logging.WARNING and "MBED_API_AUTH_TOKEN" in x.msg + ), "Auth token should be mentioned" + assert any( + x for x in caplog.records if x.levelno == logging.DEBUG and "Who are you?" in x.msg + ), "Message content should be in the debug message" + + def test_404(self, caplog, requests_mock): + """Given a 404 error code, TargetAPIError is raised.""" + caplog.set_level(logging.DEBUG) + requests_mock.get(board_database._BOARD_API, status_code=404, text="Not Found") + with pytest.raises(board_database.BoardAPIError): + board_database.get_online_board_data() + assert any( + x for x in caplog.records if x.levelno == logging.WARNING and "404" in x.msg + ), "HTTP status code should be mentioned" + assert any( + x for x in caplog.records if x.levelno == logging.DEBUG and "Not Found" in x.msg + ), "Message content should be in the debug message" + + def test_200_invalid_json(self, caplog, requests_mock): + """Given a valid response but invalid json, JSONDecodeError is raised.""" + caplog.set_level(logging.DEBUG) + requests_mock.get(board_database._BOARD_API, text="some text") + with pytest.raises(board_database.ResponseJSONError): + board_database.get_online_board_data() + assert any( + x for x in caplog.records if x.levelno == logging.WARNING and "Invalid JSON" in x.msg + ), "Invalid JSON should be mentioned" + assert any( + x for x in caplog.records if x.levelno == logging.DEBUG and "some text" in x.msg + ), "Message content should be in the debug message" + + def test_200_no_data_field(self, caplog, requests_mock): + """Given a valid response but no data field, ResponseJSONError is raised.""" + caplog.set_level(logging.DEBUG) + requests_mock.get(board_database._BOARD_API, json={"notdata": [], "stillnotdata": {}}) + with pytest.raises(board_database.ResponseJSONError): + board_database.get_online_board_data() + assert any( + x for x in caplog.records if x.levelno == logging.WARNING and "missing the 'data' field" in x.msg + ), "Data field should be mentioned" + assert any( + x for x in caplog.records if x.levelno == logging.DEBUG and "notdata, stillnotdata" in x.msg + ), "JSON keys from message should be in the debug message" + + def test_200_value_data(self, requests_mock): + """Given a valid response, target data is set from the returned json.""" + requests_mock.get(board_database._BOARD_API, json={"data": 42}) + board_data = board_database.get_online_board_data() + assert 42 == board_data, "Target data should match the contents of the target API data" + + @mock.patch("mbed_tools.targets._internal.board_database.requests") + @mock.patch("mbed_tools.targets._internal.board_database.env", spec_set=env) + def test_auth_header_set_with_token(self, env, requests): + """Given an authorization token env variable, get is called with authorization header.""" + env.MBED_API_AUTH_TOKEN = "token" + header = {"Authorization": "Bearer token"} + board_database._get_request() + requests.get.assert_called_once_with(board_database._BOARD_API, headers=header) + + @mock.patch("mbed_tools.targets._internal.board_database.requests") + def test_no_auth_header_set_with_empty_token_var(self, requests): + """Given no authorization token env variable, get is called with no header.""" + board_database._get_request() + requests.get.assert_called_once_with(board_database._BOARD_API, headers=None) + + @mock.patch("mbed_tools.targets._internal.board_database.requests.get") + def test_logs_no_warning_on_success(self, get, caplog): + board_database._get_request() + assert not caplog.records + + @mock.patch("mbed_tools.targets._internal.board_database.requests.get") + def test_raises_tools_error_on_connection_error(self, get, caplog): + get.side_effect = board_database.requests.exceptions.ConnectionError + with pytest.raises(board_database.BoardAPIError): + board_database._get_request() + assert "Unable to connect" in caplog.text + assert len(caplog.records) == 1 + + @mock.patch("mbed_tools.targets._internal.board_database.requests.get") + def test_logs_error_on_requests_ssl_error(self, get, caplog): + get.side_effect = board_database.requests.exceptions.SSLError + with pytest.raises(board_database.BoardAPIError): + board_database._get_request() + assert "verify an SSL" in caplog.text + + @mock.patch("mbed_tools.targets._internal.board_database.requests.get") + def test_logs_error_on_requests_proxy_error(self, get, caplog): + get.side_effect = board_database.requests.exceptions.ProxyError + with pytest.raises(board_database.BoardAPIError): + board_database._get_request() + assert "connect to proxy" in caplog.text + + @mock.patch.dict("os.environ", {"http_proxy": "http://proxy:8080", "https_proxy": "https://proxy:8080"}) + def test_requests_uses_proxy_variables(self, requests_mock): + requests_mock.get(board_database._BOARD_API) + board_database._get_request() + assert requests_mock.last_request.proxies == {"http": "http://proxy:8080", "https": "https://proxy:8080"} + + +class TestGetOfflineTargetData: + """Tests for the method get_offline_target_data.""" + + def test_local_target_database_file_found(self): + """Test local database is found and loaded.""" + data = board_database.get_offline_board_data() + assert len(data), "Some data should be returned from local database file." + + @mock.patch("mbed_tools.targets._internal.board_database.get_board_database_path") + def test_raises_on_invalid_json(self, mocked_get_file): + """Test raises an error when the file contains invalid JSON.""" + invalid_json = "None" + path_mock = mock.Mock() + path_mock.read_text.return_value = invalid_json + mocked_get_file.return_value = path_mock + with pytest.raises(board_database.ResponseJSONError): + board_database.get_offline_board_data() + + +class TestGetLocalTargetDatabaseFile: + def test_returns_path_to_targets(self): + path = board_database.get_board_database_path() + assert path.exists(), "Path to boards should exist in the package data folder." diff --git a/tools/python/python_tests/mbed_tools/targets/_internal/test_target_attributes.py b/tools/python/python_tests/mbed_tools/targets/_internal/test_target_attributes.py new file mode 100644 index 0000000000..ba66a4cf0c --- /dev/null +++ b/tools/python/python_tests/mbed_tools/targets/_internal/test_target_attributes.py @@ -0,0 +1,118 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for `mbed_tools.targets.target_attributes`.""" +from unittest import TestCase, mock + +from mbed_tools.targets._internal.target_attributes import ( + TargetNotFoundError, + get_target_attributes, + _extract_target_attributes, + _extract_core_labels, + _apply_config_overrides, +) + + +class TestExtractTargetAttributes(TestCase): + def test_no_target_found(self): + all_targets_data = { + "Target_1": "some attributes", + "Target_2": "some more attributes", + } + with self.assertRaises(TargetNotFoundError): + _extract_target_attributes(all_targets_data, "Unlisted_Target") + + def test_target_found(self): + target_attributes = {"attribute1": "something"} + + all_targets_data = { + "Target_1": target_attributes, + "Target_2": "some more attributes", + } + # When not explicitly included public is assumed to be True + self.assertEqual(_extract_target_attributes(all_targets_data, "Target_1"), target_attributes) + + def test_target_public(self): + all_targets_data = { + "Target_1": {"attribute1": "something", "public": True}, + "Target_2": "some more attributes", + } + # The public attribute affects visibility but is removed from result + self.assertEqual(_extract_target_attributes(all_targets_data, "Target_1"), {"attribute1": "something"}) + + def test_target_private(self): + all_targets_data = { + "Target_1": {"attribute1": "something", "public": False}, + "Target_2": "some more attributes", + } + with self.assertRaises(TargetNotFoundError): + _extract_target_attributes(all_targets_data, "Target_1"), + + +class TestGetTargetAttributes(TestCase): + @mock.patch("mbed_tools.targets._internal.target_attributes._extract_target_attributes") + @mock.patch("mbed_tools.targets._internal.target_attributes.get_labels_for_target") + @mock.patch("mbed_tools.targets._internal.target_attributes._extract_core_labels") + def test_gets_attributes_for_target(self, extract_core_labels, get_labels_for_target, extract_target_attributes): + targets_json_data = {"attrs": "vals"} + target_name = "My_Target" + build_attributes = {"attribute": "value"} + extract_target_attributes.return_value = build_attributes + + result = get_target_attributes(targets_json_data, target_name) + + extract_target_attributes.assert_called_once_with(targets_json_data, target_name) + get_labels_for_target.assert_called_once_with(targets_json_data, target_name) + extract_core_labels.assert_called_once_with(build_attributes.get("core", None)) + self.assertEqual(result, extract_target_attributes.return_value) + + +class TestExtractCoreLabels(TestCase): + @mock.patch("mbed_tools.targets._internal.target_attributes.decode_json_file") + def test_extract_core(self, read_json_file): + core_labels = ["FOO", "BAR"] + metadata = {"CORE_LABELS": {"core_name": core_labels}} + read_json_file.return_value = metadata + target_core = "core_name" + + result = _extract_core_labels(target_core) + + self.assertEqual(result, set(core_labels)) + + def test_no_core(self): + result = _extract_core_labels(None) + self.assertEqual(result, set()) + + @mock.patch("mbed_tools.targets._internal.target_attributes.decode_json_file") + def test_no_labels(self, read_json_file): + metadata = {"CORE_LABELS": {"not_the_same_core": []}} + read_json_file.return_value = metadata + + result = _extract_core_labels("core_name") + + self.assertEqual(result, set()) + + +class TestApplyConfigOverrides(TestCase): + def test_applies_overrides(self): + config = {"foo": {"help": "Do a foo", "value": 0}} + overrides = {"foo": 9} + expected_result = {"foo": {"help": "Do a foo", "value": 9}} + + self.assertEqual(expected_result, _apply_config_overrides(config, overrides)) + + def test_applies_no_overrides(self): + config = {"foo": {"help": "Do a foo", "value": 0}} + overrides = {} + + self.assertEqual(config, _apply_config_overrides(config, overrides)) + + def test_warns_when_attempting_to_override_undefined_config_parameter(self): + config = {"foo": {"help": "Do a foo", "value": 0}} + overrides = {"bar": 9} + + with self.assertLogs(level="WARNING") as logger: + _apply_config_overrides(config, overrides) + + self.assertIn("bar=9", logger.output[0]) diff --git a/tools/python/python_tests/mbed_tools/targets/factories.py b/tools/python/python_tests/mbed_tools/targets/factories.py new file mode 100644 index 0000000000..2f9441e09c --- /dev/null +++ b/tools/python/python_tests/mbed_tools/targets/factories.py @@ -0,0 +1,30 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from mbed_tools.targets import Board + + +def make_board( + board_type="BoardType", + board_name="BoardName", + mbed_os_support=None, + mbed_enabled=None, + product_code="9999", + slug="BoardSlug", + target_type="TargetType", +): + return Board( + board_type=board_type, + product_code=product_code, + board_name=board_name, + target_type=target_type, + slug=slug, + mbed_os_support=mbed_os_support if mbed_os_support else (), + mbed_enabled=mbed_enabled if mbed_enabled else (), + build_variant=(), + ) + + +def make_dummy_internal_board_data(): + return [dict(attributes=dict(board_type=str(i), board_name=str(i), product_code=str(i))) for i in range(10)] diff --git a/tools/python/python_tests/mbed_tools/targets/test_board.py b/tools/python/python_tests/mbed_tools/targets/test_board.py new file mode 100644 index 0000000000..9d67f88bab --- /dev/null +++ b/tools/python/python_tests/mbed_tools/targets/test_board.py @@ -0,0 +1,106 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for `mbed_tools.targets.mbed_target`.""" + +from unittest import TestCase + +# Import from top level as this is the expected interface for users +from mbed_tools.targets import Board + + +class TestBoard(TestCase): + """Tests for the class `Board`.""" + + def test_offline_database_entry(self): + """Given an entry from the offline database, a Board is generated with the correct information.""" + board = Board.from_offline_board_entry( + { + "mbed_os_support": ["Mbed OS 5.15"], + "mbed_enabled": ["Basic"], + "board_type": "B_1", + "board_name": "Board 1", + "product_code": "P1", + "target_type": "platform", + "slug": "Le Slug", + } + ) + + self.assertEqual("B_1", board.board_type) + self.assertEqual("Board 1", board.board_name) + self.assertEqual(("Mbed OS 5.15",), board.mbed_os_support) + self.assertEqual(("Basic",), board.mbed_enabled) + self.assertEqual("P1", board.product_code) + self.assertEqual("platform", board.target_type) + self.assertEqual("Le Slug", board.slug) + self.assertEqual((), board.build_variant) + + def test_empty_database_entry(self): + """Given no data, a Board is created with no information.""" + board = Board.from_online_board_entry({}) + + self.assertEqual("", board.board_type) + self.assertEqual("", board.board_name) + self.assertEqual((), board.mbed_os_support) + self.assertEqual((), board.mbed_enabled) + self.assertEqual("", board.product_code) + self.assertEqual("", board.target_type) + self.assertEqual("", board.slug) + + def test_online_database_entry(self): + online_data = { + "type": "target", + "id": "1", + "attributes": { + "features": { + "mbed_enabled": ["Advanced"], + "mbed_os_support": [ + "Mbed OS 5.10", + "Mbed OS 5.11", + "Mbed OS 5.12", + "Mbed OS 5.13", + "Mbed OS 5.14", + "Mbed OS 5.15", + "Mbed OS 5.8", + "Mbed OS 5.9", + ], + "antenna": ["Connector", "Onboard"], + "certification": [ + "Anatel (Brazil)", + "AS/NZS (Australia and New Zealand)", + "CE (Europe)", + "FCC/CFR (USA)", + "IC RSS (Canada)", + "ICASA (South Africa)", + "KCC (South Korea)", + "MIC (Japan)", + "NCC (Taiwan)", + "RoHS (Europe)", + ], + "communication": ["Bluetooth & BLE"], + "interface_firmware": ["DAPLink", "J-Link"], + "target_core": ["Cortex-M4"], + "mbed_studio_support": ["Build and run"], + }, + "board_type": "k64f", + "flash_size": 512, + "name": "u-blox NINA-B1", + "product_code": "0455", + "ram_size": 64, + "target_type": "module", + "hidden": False, + "device_name": "nRF52832_xxAA", + "slug": "u-blox-nina-b1", + }, + } + board = Board.from_online_board_entry(online_data) + + self.assertEqual(online_data["attributes"]["board_type"].upper(), board.board_type) + self.assertEqual(online_data["attributes"]["name"], board.board_name) + self.assertEqual(tuple(online_data["attributes"]["features"]["mbed_os_support"]), board.mbed_os_support) + self.assertEqual(tuple(online_data["attributes"]["features"]["mbed_enabled"]), board.mbed_enabled) + self.assertEqual(online_data["attributes"]["product_code"], board.product_code) + self.assertEqual(online_data["attributes"]["target_type"], board.target_type) + self.assertEqual(online_data["attributes"]["slug"], board.slug) + self.assertEqual(tuple(), board.build_variant) diff --git a/tools/python/python_tests/mbed_tools/targets/test_boards.py b/tools/python/python_tests/mbed_tools/targets/test_boards.py new file mode 100644 index 0000000000..4983992bab --- /dev/null +++ b/tools/python/python_tests/mbed_tools/targets/test_boards.py @@ -0,0 +1,109 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for `mbed_tools.targets.boards`.""" + +import json +from dataclasses import asdict +from unittest import mock, TestCase + +from mbed_tools.targets import Board +from mbed_tools.targets.boards import Boards +from mbed_tools.targets.exceptions import UnknownBoard +from python_tests.mbed_tools.targets.factories import make_dummy_internal_board_data + + +@mock.patch("mbed_tools.targets._internal.board_database.get_online_board_data") +class TestBoards(TestCase): + """Tests for the class `Boards`.""" + + def test_iteration_is_repeatable(self, mocked_get_board_data): + """Test Boards is an iterable and not an exhaustible iterator.""" + fake_board_data = make_dummy_internal_board_data() + mocked_get_board_data.return_value = fake_board_data + + boards = Boards.from_online_database() + tgts_a = [b for b in boards] + tgts_b = [b for b in boards] + + self.assertEqual(tgts_a, tgts_b, "The lists are equal as boards was not exhausted on the first pass.") + + def test_board_found_in_boards_membership_test(self, mocked_get_board_data): + """Tests the __contains__ method was implemented correctly.""" + board_data = make_dummy_internal_board_data() + mocked_get_board_data.return_value = board_data + + boards = Boards.from_online_database() + board, *_ = boards + + self.assertIn(board, boards) + + def test_membership_test_returns_false_for_non_board(self, mocked_get_board_data): + """Tests the __contains__ method was implemented correctly.""" + board_data = make_dummy_internal_board_data() + mocked_get_board_data.return_value = board_data + + boards = Boards.from_online_database() + + self.assertFalse("a" in boards) + + def test_len_boards(self, mocked_get_board_data): + """Test len(Boards()) matches len(board_database).""" + board_data = make_dummy_internal_board_data() + mocked_get_board_data.return_value = board_data + + self.assertEqual(len(Boards.from_online_database()), len(board_data)) + + def test_get_board_success(self, mocked_get_board_data): + """Check a Board can be looked up by arbitrary parameters.""" + fake_board_data = [ + {"attributes": {"product_code": "0300"}}, + {"attributes": {"product_code": "0200"}}, + {"attributes": {"product_code": "0100"}}, + ] + mocked_get_board_data.return_value = fake_board_data + + boards = Boards.from_online_database() + board = boards.get_board(lambda b: b.product_code == "0100") + + self.assertEqual(board.product_code, "0100", "Board's product code should match the given product code.") + + def test_get_board_failure(self, mocked_get_board_data): + """Check Boards handles queries without a match.""" + mocked_get_board_data.return_value = [] + + boards = Boards.from_online_database() + + with self.assertRaises(UnknownBoard): + boards.get_board(lambda b: b.product_code == "unknown") + + @mock.patch("mbed_tools.targets._internal.board_database.get_offline_board_data") + def test_json_dump_from_raw_and_filtered_data(self, mocked_get_offline_board_data, mocked_get_online_board_data): + raw_board_data = [ + {"attributes": {"product_code": "0200", "board": "test"}}, + {"attributes": {"product_code": "0100", "board": "test2"}}, + ] + mocked_get_online_board_data.return_value = raw_board_data + + boards = [Board.from_online_board_entry(b) for b in raw_board_data] + filtered_board_data = [asdict(board) for board in boards] + mocked_get_offline_board_data.return_value = filtered_board_data + + # Boards.from_online_database handles "raw" board entries from the online db + boards = Boards.from_online_database() + json_str_from_raw = boards.json_dump() + t1_raw, t2_raw = boards + + # Boards.from_offline_database expects the data to have been "filtered" through the Boards interface + offline_boards = Boards.from_offline_database() + json_str_from_filtered = offline_boards.json_dump() + t1_filt, t2_filt = offline_boards + + self.assertEqual( + json_str_from_raw, + json.dumps([asdict(t1_raw), asdict(t2_raw)], indent=4), + "JSON string should match serialised board __dict__.", + ) + + self.assertEqual(json_str_from_filtered, json.dumps([t1_filt.__dict__, t2_filt.__dict__], indent=4)) diff --git a/tools/python/python_tests/mbed_tools/targets/test_config.py b/tools/python/python_tests/mbed_tools/targets/test_config.py new file mode 100644 index 0000000000..bad38a60b7 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/targets/test_config.py @@ -0,0 +1,23 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +import os +from unittest import TestCase, mock + +from mbed_tools.targets.env import env + + +class TestMbedApiAuthToken(TestCase): + @mock.patch.dict(os.environ, {"MBED_API_AUTH_TOKEN": "sometoken"}) + def test_returns_api_token_set_in_env(self): + self.assertEqual(env.MBED_API_AUTH_TOKEN, "sometoken") + + +class TestDatabaseMode(TestCase): + @mock.patch.dict(os.environ, {"MBED_DATABASE_MODE": "ONLINE"}) + def test_returns_database_mode_set_in_env(self): + self.assertEqual(env.MBED_DATABASE_MODE, "ONLINE") + + def test_returns_default_database_mode_if_not_set_in_env(self): + self.assertEqual(env.MBED_DATABASE_MODE, "AUTO") diff --git a/tools/python/python_tests/mbed_tools/targets/test_get_board.py b/tools/python/python_tests/mbed_tools/targets/test_get_board.py new file mode 100644 index 0000000000..cc7d9aca17 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/targets/test_get_board.py @@ -0,0 +1,154 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for `mbed_tools.targets.get_board`.""" +import pytest + +from unittest import mock + +from mbed_tools.targets._internal.exceptions import BoardAPIError + +# Import from top level as this is the expected interface for users +from mbed_tools.targets import get_board_by_online_id, get_board_by_product_code, get_board_by_jlink_slug +from mbed_tools.targets.get_board import ( + _DatabaseMode, + _get_database_mode, + get_board, +) +from mbed_tools.targets.env import env +from mbed_tools.targets.exceptions import UnknownBoard, UnsupportedMode +from python_tests.mbed_tools.targets.factories import make_board + + +@pytest.fixture +def mock_get_board(): + with mock.patch("mbed_tools.targets.get_board.get_board", autospec=True) as gbp: + yield gbp + + +@pytest.fixture +def mock_env(): + with mock.patch("mbed_tools.targets.get_board.env", spec_set=env) as gbp: + yield gbp + + +@pytest.fixture +def mocked_boards(): + with mock.patch("mbed_tools.targets.get_board.Boards", autospec=True) as gbp: + yield gbp + + +class TestGetBoard: + def test_online_mode(self, mock_env, mocked_boards): + mock_env.MBED_DATABASE_MODE = "ONLINE" + fn = mock.Mock() + + subject = get_board(fn) + + assert subject == mocked_boards.from_online_database().get_board.return_value + mocked_boards.from_online_database().get_board.assert_called_once_with(fn) + + def test_offline_mode(self, mock_env, mocked_boards): + mock_env.MBED_DATABASE_MODE = "OFFLINE" + fn = mock.Mock() + + subject = get_board(fn) + + assert subject == mocked_boards.from_offline_database().get_board.return_value + mocked_boards.from_offline_database().get_board.assert_called_once_with(fn) + + def test_auto_mode_calls_offline_boards_first(self, mock_env, mocked_boards): + mock_env.MBED_DATABASE_MODE = "AUTO" + fn = mock.Mock() + + subject = get_board(fn) + + assert subject == mocked_boards.from_offline_database().get_board.return_value + mocked_boards.from_online_database().get_board.assert_not_called() + mocked_boards.from_offline_database().get_board.assert_called_once_with(fn) + + def test_auto_mode_falls_back_to_online_database_when_board_not_found(self, mock_env, mocked_boards): + mock_env.MBED_DATABASE_MODE = "AUTO" + mocked_boards.from_offline_database().get_board.side_effect = UnknownBoard + fn = mock.Mock() + + subject = get_board(fn) + + assert subject == mocked_boards.from_online_database().get_board.return_value + mocked_boards.from_offline_database().get_board.assert_called_once_with(fn) + mocked_boards.from_online_database().get_board.assert_called_once_with(fn) + + def test_auto_mode_raises_when_board_not_found_offline_with_no_network(self, mock_env, mocked_boards): + mock_env.MBED_DATABASE_MODE = "AUTO" + mocked_boards.from_offline_database().get_board.side_effect = UnknownBoard + mocked_boards.from_online_database().get_board.side_effect = BoardAPIError + fn = mock.Mock() + + with pytest.raises(UnknownBoard): + get_board(fn) + mocked_boards.from_offline_database().get_board.assert_called_once_with(fn) + mocked_boards.from_online_database().get_board.assert_called_once_with(fn) + + +class TestGetBoardByProductCode: + def test_matches_boards_by_product_code(self, mock_get_board): + product_code = "swag" + + assert get_board_by_product_code(product_code) == mock_get_board.return_value + + # Test callable matches correct boards + fn = mock_get_board.call_args[0][0] + + matching_board = make_board(product_code=product_code) + not_matching_board = make_board(product_code="whatever") + + assert fn(matching_board) + assert not fn(not_matching_board) + + +class TestGetBoardByOnlineId: + def test_matches_boards_by_online_id(self, mock_get_board): + target_type = "platform" + + assert get_board_by_online_id(slug="slug", target_type=target_type) == mock_get_board.return_value + + # Test callable matches correct boards + fn = mock_get_board.call_args[0][0] + + matching_board_1 = make_board(target_type=target_type, slug="slug") + matching_board_2 = make_board(target_type=target_type, slug="SlUg") + not_matching_board = make_board(target_type=target_type, slug="whatever") + + assert fn(matching_board_1) + assert fn(matching_board_2) + assert not fn(not_matching_board) + + +class TestGetBoardByJlinkSlug: + def test_matches_boards_by_online_id(self, mock_get_board): + assert get_board_by_jlink_slug(slug="slug") == mock_get_board.return_value + + # Test callable matches correct boards + fn = mock_get_board.call_args[0][0] + + matching_board_1 = make_board(slug="slug") + matching_board_2 = make_board(board_type="slug") + matching_board_3 = make_board(board_name="slug") + not_matching_board = make_board() + + assert fn(matching_board_1) + assert fn(matching_board_2) + assert fn(matching_board_3) + assert not fn(not_matching_board) + + +class TestGetDatabaseMode: + def test_returns_configured_database_mode(self, mock_env): + mock_env.MBED_DATABASE_MODE = "OFFLINE" + assert _get_database_mode() == _DatabaseMode.OFFLINE + + def test_raises_when_configuration_is_not_supported(self, mock_env): + mock_env.MBED_DATABASE_MODE = "NOT_VALID" + with pytest.raises(UnsupportedMode): + _get_database_mode() diff --git a/tools/python/python_tests/mbed_tools/targets/test_get_target.py b/tools/python/python_tests/mbed_tools/targets/test_get_target.py new file mode 100644 index 0000000000..9510f650f8 --- /dev/null +++ b/tools/python/python_tests/mbed_tools/targets/test_get_target.py @@ -0,0 +1,48 @@ +# +# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +from unittest import TestCase, mock +from mbed_tools.targets.get_target import get_target_by_board_type, get_target_by_name +from mbed_tools.targets.exceptions import TargetError +from mbed_tools.targets._internal.target_attributes import TargetAttributesError + + +class TestGetTarget(TestCase): + @mock.patch("mbed_tools.targets.get_target.target_attributes.get_target_attributes") + def test_get_by_name(self, mock_target_attrs): + target_name = "Target" + targets_json_file_path = "targets.json" + + result = get_target_by_name(target_name, targets_json_file_path) + + self.assertEqual(result, mock_target_attrs.return_value) + mock_target_attrs.assert_called_once_with(targets_json_file_path, target_name) + + @mock.patch("mbed_tools.targets.get_target.target_attributes.get_target_attributes") + def test_get_by_name_raises_target_error_when_target_json_not_found(self, mock_target_attrs): + target_name = "Target" + targets_json_file_path = "not-targets.json" + mock_target_attrs.side_effect = FileNotFoundError + + with self.assertRaises(TargetError): + get_target_by_name(target_name, targets_json_file_path) + + @mock.patch("mbed_tools.targets.get_target.target_attributes.get_target_attributes") + def test_get_by_name_raises_target_error_when_target_attr_collection_fails(self, mock_target_attrs): + target_name = "Target" + targets_json_file_path = "targets.json" + mock_target_attrs.side_effect = TargetAttributesError + + with self.assertRaises(TargetError): + get_target_by_name(target_name, targets_json_file_path) + + @mock.patch("mbed_tools.targets.get_target.get_target_by_name") + def test_get_by_board_type(self, mock_get_target_by_name): + board_type = "Board" + path_to_mbed_program = "somewhere" + + result = get_target_by_board_type(board_type, path_to_mbed_program) + + self.assertEqual(result, mock_get_target_by_name.return_value) + mock_get_target_by_name.assert_called_once_with(board_type, path_to_mbed_program) diff --git a/tools/python/python_tests/memap/__init__.py b/tools/python/python_tests/memap/__init__.py new file mode 100644 index 0000000000..848835c6c7 --- /dev/null +++ b/tools/python/python_tests/memap/__init__.py @@ -0,0 +1,17 @@ +""" +mbed SDK +Copyright (c) 2017 ARM Limited +SPDX-License-Identifier: Apache-2.0 + +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. +""" \ No newline at end of file diff --git a/tools/test/memap/arm.map b/tools/python/python_tests/memap/arm.map similarity index 100% rename from tools/test/memap/arm.map rename to tools/python/python_tests/memap/arm.map diff --git a/tools/test/memap/gcc.map b/tools/python/python_tests/memap/gcc.map similarity index 100% rename from tools/test/memap/gcc.map rename to tools/python/python_tests/memap/gcc.map diff --git a/tools/test/memap/iar.map b/tools/python/python_tests/memap/iar.map similarity index 100% rename from tools/test/memap/iar.map rename to tools/python/python_tests/memap/iar.map diff --git a/tools/test/memap/memap_test.py b/tools/python/python_tests/memap/memap_test.py similarity index 97% rename from tools/test/memap/memap_test.py rename to tools/python/python_tests/memap/memap_test.py index 77259a721b..2570d0a419 100644 --- a/tools/test/memap/memap_test.py +++ b/tools/python/python_tests/memap/memap_test.py @@ -1,6 +1,7 @@ """ mbed SDK Copyright (c) 2017 ARM Limited +SPDX-License-Identifier: Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,8 +21,8 @@ import json import pytest -import tools.memap -from tools.memap import MemapParser +from memap import memap +from memap.memap import MemapParser from copy import deepcopy """ @@ -150,14 +151,14 @@ def generate_test_helper(memap_parser, format, depth, sep, file_output=None): """ old_modules = deepcopy(memap_parser.modules) - tools.memap.sep = sep + memap.sep = sep memap_parser.generate_output(format, depth, file_output=file_output) assert memap_parser.modules == old_modules,\ "generate_output modified the 'modules' property" for file_name in memap_parser.short_modules: - assert(len(file_name.split(tools.memap.sep)) <= depth) + assert(len(file_name.split(memap.sep)) <= depth) @pytest.mark.parametrize("depth", [1, 2, 20]) diff --git a/tools/test/memap/parse_test.py b/tools/python/python_tests/memap/parse_test.py similarity index 98% rename from tools/test/memap/parse_test.py rename to tools/python/python_tests/memap/parse_test.py index e3bb7ed5ec..656f51c466 100644 --- a/tools/test/memap/parse_test.py +++ b/tools/python/python_tests/memap/parse_test.py @@ -24,7 +24,7 @@ import json import pytest -from tools.memap import MemapParser, _ArmccParser +from memap.memap import MemapParser, _ArmccParser from copy import deepcopy diff --git a/tools/python/python_tests/requirements.apt.txt b/tools/python/python_tests/requirements.apt.txt new file mode 100644 index 0000000000..745b3968b2 --- /dev/null +++ b/tools/python/python_tests/requirements.apt.txt @@ -0,0 +1,7 @@ +python3-pytest +python3-factory-boy +python3-requests-mock +python3-mock +python3-coverage +python3-bs4 +python3-lxml \ No newline at end of file diff --git a/tools/python/python_tests/requirements.txt b/tools/python/python_tests/requirements.txt new file mode 100644 index 0000000000..f736ed797d --- /dev/null +++ b/tools/python/python_tests/requirements.txt @@ -0,0 +1,16 @@ +## NOTE: This file must be kept in sync with requirements.apt.txt in the same folder. +## That file gives the equivalent package names of these packages in the apt package manager +## for Ubuntu & Debian + +# These are the requirements for running the Python package tests. +# They are in addition to the requirements.txt under mbedos/tools/. +pytest +factory_boy +requests-mock +mock +coverage + +# Even though beautifulsoup4 and lxml are only used by the mac version +# of mbed ls tools, they're needed on all platforms for its unit test. +beautifulsoup4 +lxml \ No newline at end of file diff --git a/tools/python/python_tests/scancode_evaluate/__init__.py b/tools/python/python_tests/scancode_evaluate/__init__.py new file mode 100644 index 0000000000..2bae17afc8 --- /dev/null +++ b/tools/python/python_tests/scancode_evaluate/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright (c) 2020-2023 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# \ No newline at end of file diff --git a/tools/test/ci/scancode_evaluate_test.py b/tools/python/python_tests/scancode_evaluate/scancode_evaluate_test.py similarity index 97% rename from tools/test/ci/scancode_evaluate_test.py rename to tools/python/python_tests/scancode_evaluate/scancode_evaluate_test.py index dc5b8fbe93..8448cd16d4 100644 --- a/tools/test/ci/scancode_evaluate_test.py +++ b/tools/python/python_tests/scancode_evaluate/scancode_evaluate_test.py @@ -5,8 +5,7 @@ import importlib import os import pytest - -license_check = importlib.import_module("scancode-evaluate").license_check +from scancode_evaluate.scancode_evaluate import license_check STUBS_PATH = os.path.join( os.path.abspath(os.path.join(os.path.dirname(__file__))), "scancode_test" diff --git a/tools/test/ci/scancode_test/scancode_test_1.json b/tools/python/python_tests/scancode_evaluate/scancode_test/scancode_test_1.json similarity index 100% rename from tools/test/ci/scancode_test/scancode_test_1.json rename to tools/python/python_tests/scancode_evaluate/scancode_test/scancode_test_1.json diff --git a/tools/test/ci/scancode_test/scancode_test_2.json b/tools/python/python_tests/scancode_evaluate/scancode_test/scancode_test_2.json similarity index 97% rename from tools/test/ci/scancode_test/scancode_test_2.json rename to tools/python/python_tests/scancode_evaluate/scancode_test/scancode_test_2.json index 6ed59481ab..588ac44508 100644 --- a/tools/test/ci/scancode_test/scancode_test_2.json +++ b/tools/python/python_tests/scancode_evaluate/scancode_test/scancode_test_2.json @@ -31,7 +31,7 @@ ], "files":[ { - "path":"tools/test/ci/scancode_test/test.h", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test.h", "type":"file", "licenses":[ { @@ -141,7 +141,7 @@ ] }, { - "path":"tools/test/ci/scancode_test/test2.h", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test2.h", "type":"file", "licenses":[ { @@ -251,7 +251,7 @@ ] }, { - "path":"tools/test/ci/scancode_test/test3.h", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test3.h", "type":"file", "licenses":[ { @@ -361,7 +361,7 @@ ] }, { - "path":"tools/test/ci/scancode_test/test4.h", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test4.h", "type":"file", "licenses":[ { @@ -471,7 +471,7 @@ ] }, { - "path":"tools/test/ci/scancode_test/test5.c", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test5.c", "type":"file", "licenses":[ { @@ -548,7 +548,7 @@ ] }, { - "path":"tools/test/ci/scancode_test/test6.c", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test6.c", "type":"file", "licenses":[ { @@ -625,7 +625,7 @@ ] }, { - "path":"tools/test/ci/scancode_test/test7.c", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test7.c", "type":"file", "licenses":[ { @@ -702,7 +702,7 @@ ] }, { - "path":"tools/test/ci/scancode_test/test8.c", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test8.c", "type":"file", "licenses":[ { diff --git a/tools/test/ci/scancode_test/scancode_test_3.json b/tools/python/python_tests/scancode_evaluate/scancode_test/scancode_test_3.json similarity index 94% rename from tools/test/ci/scancode_test/scancode_test_3.json rename to tools/python/python_tests/scancode_evaluate/scancode_test/scancode_test_3.json index 01dcbe228a..5c06930840 100644 --- a/tools/test/ci/scancode_test/scancode_test_3.json +++ b/tools/python/python_tests/scancode_evaluate/scancode_test/scancode_test_3.json @@ -28,7 +28,7 @@ ], "files":[ { - "path":"tools/test/ci/scancode_test/test.h", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test.h", "type":"file", "licenses":[ @@ -41,7 +41,7 @@ ] }, { - "path":"tools/test/ci/scancode_test/test3.h", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test3.h", "type":"file", "licenses":[ { @@ -85,7 +85,7 @@ ] }, { - "path":"tools/test/ci/scancode_test/test4.h", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test4.h", "type":"file", "licenses":[ { @@ -129,7 +129,7 @@ ] }, { - "path":"tools/test/ci/scancode_test/test5.h", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test5.h", "type":"file", "licenses":[ { @@ -173,7 +173,7 @@ ] }, { - "path":"tools/test/ci/scancode_test/test6.h", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test6.h", "type":"file", "licenses":[ { diff --git a/tools/test/ci/scancode_test/scancode_test_4.json b/tools/python/python_tests/scancode_evaluate/scancode_test/scancode_test_4.json similarity index 98% rename from tools/test/ci/scancode_test/scancode_test_4.json rename to tools/python/python_tests/scancode_evaluate/scancode_test/scancode_test_4.json index e17537b2a3..948669aed1 100644 --- a/tools/test/ci/scancode_test/scancode_test_4.json +++ b/tools/python/python_tests/scancode_evaluate/scancode_test/scancode_test_4.json @@ -24,7 +24,7 @@ ], "files":[ { - "path":"tools/test/ci/scancode_test/test3.h", + "path":"tools/python/python_tests/scancode_evaluate/scancode_test/test3.h", "type":"file", "licenses":[ { diff --git a/tools/python/run_python_tests.sh b/tools/python/run_python_tests.sh new file mode 100755 index 0000000000..b398106635 --- /dev/null +++ b/tools/python/run_python_tests.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +# +# Copyright (c) 2020-2023 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# + +# Script which executes Python tests for each Python package and generates coverage information. +# This is executed by the GitHub Actions CI build but also can be run locally. +# Make sure to install python_tests/requirements.txt before running the tests! + +set -e + +PYTHON=python3 + +# Comma separated list of directories to exclude from coverage +COVERAGE_EXCLUDES='--omit=python_tests/*' + +echo ">> Running pytest for mbed_tools package" +$PYTHON -m coverage run "$COVERAGE_EXCLUDES" -m pytest python_tests/mbed_tools + +# For everything after the first command, we pass the "-a" argument to coverage +# so that it appends to the existing coverage database +echo ">> Running pytest for memap package" +$PYTHON -m coverage run "$COVERAGE_EXCLUDES" -a -m pytest python_tests/memap + +echo ">> Running pytest for scancode_evaluate package" +$PYTHON -m coverage run "$COVERAGE_EXCLUDES" -a -m pytest python_tests/scancode_evaluate + +# Note: For some reason, they decided not to add a "_test" suffix on the +# test case filenames for some packages. So, the "-p *.py" argument is needed to +# make it look for any files as tests, not just ones ending in _test.py. + +echo ">> Running unittests for mbed_greentea package" +$PYTHON -m coverage run "$COVERAGE_EXCLUDES" -a -m unittest discover -p '*.py' python_tests.mbed_greentea + +echo ">> Running unittests for mbed_host_tests package" +$PYTHON -m coverage run "$COVERAGE_EXCLUDES" -a -m unittest discover -p '*.py' python_tests.mbed_host_tests + +echo ">> Running unittests for mbed_lstools package" +$PYTHON -m coverage run "$COVERAGE_EXCLUDES" -a -m unittest discover -p '*.py' python_tests.mbed_lstools + +echo ">> Running unittests for mbed_os_tools package" +$PYTHON -m coverage run "$COVERAGE_EXCLUDES" -a -m unittest discover -p '*.py' python_tests.mbed_os_tools + +echo ">> Generating coverage report under htmlcov/" +$PYTHON -m coverage html diff --git a/tools/python/scancode_evaluate/__init__.py b/tools/python/scancode_evaluate/__init__.py new file mode 100644 index 0000000000..2bae17afc8 --- /dev/null +++ b/tools/python/scancode_evaluate/__init__.py @@ -0,0 +1,4 @@ +# +# Copyright (c) 2020-2023 Arm Limited and Contributors. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# \ No newline at end of file diff --git a/tools/test/ci/scancode-evaluate.py b/tools/python/scancode_evaluate/scancode_evaluate.py similarity index 85% rename from tools/test/ci/scancode-evaluate.py rename to tools/python/scancode_evaluate/scancode_evaluate.py index e650fc9d0c..3cf24457f9 100644 --- a/tools/test/ci/scancode-evaluate.py +++ b/tools/python/scancode_evaluate/scancode_evaluate.py @@ -19,7 +19,7 @@ limitations import argparse import json import logging -import os.path +import pathlib import re import sys from enum import Enum @@ -28,8 +28,11 @@ MISSING_LICENSE_TEXT = "Missing license header" MISSING_PERMISSIVE_LICENSE_TEXT = "Non-permissive license" MISSING_SPDX_TEXT = "Missing SPDX license identifier" -userlog = logging.getLogger("scancode-evaluate") +userlog = logging.getLogger("scancode_evaluate") +# find the mbed-os root dir by going up three levels from this script +this_script_dir = pathlib.Path(__file__).parent +mbed_os_root = this_script_dir.parent.parent.parent class ReturnCode(Enum): """Return codes.""" @@ -43,16 +46,14 @@ def init_logger(): userlog.setLevel(logging.INFO) userlog.addHandler( logging.FileHandler( - os.path.join(os.getcwd(), 'scancode-evaluate.log'), mode='w' + pathlib.Path.cwd() / 'scancode_evaluate.log', mode='w' ) ) -def path_leaf(path): - """Return the leaf of a path.""" - head, tail = os.path.split(path) - # Ensure the correct file name is returned if the file ends with a slash - return tail or os.path.basename(head) +def format_path_for_display(path: pathlib.Path) -> str: + """Format a returned file path for display in the log""" + return str(pathlib.Path(*path.parts[1:])) def has_permissive_text_in_scancode_output(scancode_output_data_file_licenses): @@ -82,8 +83,11 @@ def has_binary_license(scanned_file_content): def get_file_text(scancode_output_data_file): - """Returns file text for scancode output file""" - file_path = os.path.abspath(scancode_output_data_file['path']) + """ + Returns file text for scancode output file. + File path is expected to be relative to mbed-os root. + """ + file_path = mbed_os_root / scancode_output_data_file['path'] try: with open(file_path, 'r') as read_file: return read_file.read() @@ -150,11 +154,13 @@ def license_check(scancode_output_path): if license_offenders: userlog.warning("Found files with missing license details, please review and fix") for offender in license_offenders: - userlog.warning("File: %s reason: %s" % (path_leaf(offender['path']), offender['fail_reason'])) + userlog.warning("File: %s reason: %s" % + (format_path_for_display(pathlib.Path(offender['path'])), offender['fail_reason'])) if spdx_offenders: userlog.warning("Found files with missing SPDX identifier, please review and fix") for offender in spdx_offenders: - userlog.warning("File: %s reason: %s" % (path_leaf(offender['path']), offender['fail_reason'])) + userlog.warning("File: %s reason: %s" % + (format_path_for_display(pathlib.Path(offender['path'])), offender['fail_reason'])) return len(license_offenders) @@ -171,7 +177,7 @@ def parse_args(): if __name__ == "__main__": init_logger() args = parse_args() - if os.path.isfile(args.scancode_output_path): + if pathlib.Path(args.scancode_output_path).is_file(): sys.exit( ReturnCode.SUCCESS.value if license_check(args.scancode_output_path) == 0 diff --git a/tools/requirements-ci-build.txt b/tools/requirements-ci-build.txt index 5920cb158c..821692691b 100644 --- a/tools/requirements-ci-build.txt +++ b/tools/requirements-ci-build.txt @@ -1,9 +1,3 @@ -# This file contains the requirements needed to run CI builds for Mbed OS. -# It installs flashing support through the mbed tools packages and also the mbedhtrun test runner. -mbed-host-tests -mbed-greentea -mbed-ls - # For USB Device host tests hidapi>=0.7.99 pyusb>=1.2.0 diff --git a/tools/requirements.apt.txt b/tools/requirements.apt.txt new file mode 100644 index 0000000000..3bdaccef56 --- /dev/null +++ b/tools/requirements.apt.txt @@ -0,0 +1,20 @@ +python3-intelhex +python3-prettytable +python3-future +python3-jinja2 +python3-click +python3-git +python3-tqdm +python3-tabulate +python3-requests +python3-psutil +python3-pyudev +python3-typing-extensions +python3-serial +python3-dotenv +python3-appdirs +python3-fasteners +python3-lockfile +python3-junit.xml +python3-cryptography +python3-cbor diff --git a/tools/requirements.txt b/tools/requirements.txt index b20b043ecb..95f5fca6d4 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,10 +1,33 @@ +## NOTE: This file must be kept in sync with requirements.apt.txt in the same folder. +## That file gives the equivalent package names of these packages in the apt package manager +## for Ubuntu & Debian + PrettyTable<=1.0.1; python_version < '3.6' -prettytable>=2.0,<3.0; python_version >= '3.6' +prettytable>=2.0,<4.0; python_version >= '3.6' future>=0.18.0,<1.0 jinja2>=2.11.3 intelhex>=2.3.0,<3.0.0 -mbed-tools -mbed-os-tools +dotenv +Click>=7.1,<9 +GitPython +tqdm +tabulate +requests>=2.20 +pywin32; platform_system=='Windows' +psutil; platform_system=='Linux' +pyudev; platform_system=='Linux' +typing-extensions +pyserial +fasteners +appdirs>=1.4,<2.0 +junit-xml>=1.0,<2.0 +lockfile +six>=1.0,<2.0 +colorama>=0.3,<0.5 + +# beautifulsoup only needed for USB device detection on Mac +beautifulsoup4; sys_platform == 'darwin' +lxml; sys_platform == 'darwin' # needed for signing secure images cryptography diff --git a/tools/test/pylint.py b/tools/test/pylint.py index 06630d210a..74031257fd 100644 --- a/tools/test/pylint.py +++ b/tools/test/pylint.py @@ -51,7 +51,7 @@ def execute_pylint(filename): status = process.poll() return status, stout, sterr -FILES = ["memap.py", "test/pylint.py"] +FILES = ["python/memap/memap.py", "test/pylint.py"] if __name__ == "__main__": for python_module in FILES: