├── .flake8 ├── .github └── workflows │ ├── pr-check.yml │ └── publish-to-pypi.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── ci_scripts └── templates │ └── .copyright.tmpl ├── pyproject.toml ├── requirements-test.txt ├── requirements.txt ├── setup.py ├── src └── htrun │ ├── .coveragerc │ ├── __init__.py │ ├── host_tests │ ├── __init__.py │ ├── base_host_test.py │ ├── default_auto.py │ ├── detect_auto.py │ ├── dev_null_auto.py │ ├── echo.py │ ├── hello_auto.py │ ├── rtc_auto.py │ └── wait_us_auto.py │ ├── host_tests_conn_proxy │ ├── __init__.py │ ├── conn_primitive.py │ ├── conn_primitive_fastmodel.py │ ├── conn_primitive_remote.py │ ├── conn_primitive_serial.py │ └── conn_proxy.py │ ├── host_tests_logger │ ├── __init__.py │ └── ht_logger.py │ ├── host_tests_plugins │ ├── __init__.py │ ├── host_test_plugins.py │ ├── host_test_registry.py │ ├── module_copy_jn51xx.py │ ├── module_copy_mps2.py │ ├── module_copy_pyocd.py │ ├── module_copy_shell.py │ ├── module_copy_silabs.py │ ├── module_copy_stlink.py │ ├── module_copy_stprogrammer.py │ ├── module_copy_to_target.py │ ├── module_copy_ublox.py │ ├── module_power_cycle_target.py │ ├── module_reset_jn51xx.py │ ├── module_reset_mps2.py │ ├── module_reset_pyocd.py │ ├── module_reset_silabs.py │ ├── module_reset_stlink.py │ ├── module_reset_target.py │ └── module_reset_ublox.py │ ├── host_tests_registry │ ├── __init__.py │ └── host_registry.py │ ├── host_tests_runner │ ├── __init__.py │ ├── host_test.py │ ├── host_test_default.py │ └── target_base.py │ ├── host_tests_toolbox │ ├── __init__.py │ └── host_functional.py │ └── htrun.py ├── test ├── __init__.py ├── basic.py └── host_tests │ ├── __init__.py │ ├── basic_ht.py │ ├── conn_primitive_remote.py │ ├── event_callback_decorator.py │ ├── host_registry.py │ ├── host_test_base.py │ ├── host_test_os_detect.py │ ├── host_test_plugins.py │ ├── host_test_scheme.py │ ├── mps2_copy.py │ ├── mps2_reset.py │ ├── test_conn_primitive_serial.py │ └── test_target_base.py ├── test_requirements.txt └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # black, the super-great auto-formatter has decided that 88 is a good number. 3 | # I'm inclined to agree, or not to dissagree. 4 | max-line-length = 88 5 | docstring-convention = google 6 | exclude = 7 | .git, 8 | .tox, 9 | .venv, 10 | __pycache__, 11 | dist, 12 | test/*, 13 | ignore = 14 | # W503: line break before binary operator (this is no longer PEP8 compliant) 15 | W503, 16 | # E203: whitespace before ':' (this is not PEP8 compliant) 17 | E203, 18 | per-file-ignores = 19 | # Package level __init__ files improve user experience by short cutting 20 | # imports. 21 | # F401: imported but unused 22 | __init__.py:F401 23 | src/htrun/*/__init__.py:F401 24 | # We don't care about docstrings in test classes 25 | src/htrun/host_tests/*: D 26 | -------------------------------------------------------------------------------- /.github/workflows/pr-check.yml: -------------------------------------------------------------------------------- 1 | name: Test greentea tools 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.6, 3.9] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | 20 | - name: Install tox 21 | run: pip install tox 22 | 23 | - name: Code Formatting and Static Analysis 24 | run: tox -e linting 25 | 26 | 27 | test: 28 | runs-on: ${{ matrix.os }} 29 | strategy: 30 | matrix: 31 | os: [ubuntu-latest, macos-latest, windows-latest] 32 | python-version: [3.6, 3.9] 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | with: 37 | fetch-depth: 0 # Fetch history so setuptools-scm can calculate the version correctly 38 | - name: Setup Python 39 | uses: actions/setup-python@v2 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | 43 | - name: Install tox 44 | run: pip install tox 45 | 46 | - name: Run tests on ${{ matrix.os }} py ${{ matrix.python-version }} 47 | run: tox -e py 48 | 49 | - name: Create Coverage Report 50 | run: | 51 | set -xe 52 | python -m pip install coverage[toml] 53 | python -m coverage xml 54 | if: matrix.os == 'ubuntu-latest' && matrix.python-version == 3.9 55 | 56 | - name: Upload Coverage Report 57 | if: matrix.os == 'ubuntu-latest' && matrix.python-version == 3.9 58 | uses: "codecov/codecov-action@v1" 59 | with: 60 | fail_ci_if_error: true 61 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish greentea-host to PyPI 2 | on: 3 | push: 4 | tags: 5 | - v[0-9]+* # Only publish releases for version tags 6 | 7 | jobs: 8 | build-and-publish: 9 | runs-on: ubuntu-latest 10 | name: Build and publish Python distributions to PyPI 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 # Fetch history so setuptools-scm can calculate the version correctly 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: 3.9 21 | 22 | - name: Install pypa/build 23 | run: python -m pip install --user build 24 | 25 | - name: Build a binary wheel and a source tarball 26 | run: python -m build --sdist --wheel --outdir dist/ . 27 | 28 | - name: Publish distribution to PyPI 29 | uses: pypa/gh-action-pypi-publish@master 30 | with: 31 | password: ${{ secrets.PYPI_API_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PyCharm 2 | .idea/ 3 | 4 | # macOS 5 | .DS_Store 6 | 7 | # Python 8 | *.pyc 9 | __pycache__/ 10 | *.egg-info/ 11 | 12 | # Coverage.py 13 | .coverage* 14 | htmlcov/ 15 | 16 | # Package 17 | dist/ 18 | release-dist/ 19 | 20 | # Tox envs 21 | .tox/ 22 | 23 | # User envs 24 | .venv/ 25 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/kdeyev/licenseheaders.git 3 | rev: 'master' 4 | hooks: 5 | - id: licenseheaders 6 | exclude: \.yaml$|\.yml$ 7 | args: ["-t", "ci_scripts/templates/.copyright.tmpl", "-cy", "-f"] 8 | 9 | - repo: https://github.com/psf/black 10 | rev: 20.8b1 11 | hooks: 12 | - id: black 13 | 14 | - repo: https://gitlab.com/pycqa/flake8 15 | rev: 3.8.3 16 | hooks: 17 | - id: flake8 18 | additional_dependencies: [flake8-docstrings] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude *.py[cod] __pycache__ *.so 2 | prune .git 3 | prune .github 4 | exclude .pre-commit-config.yaml 5 | -------------------------------------------------------------------------------- /ci_scripts/templates/.copyright.tmpl: -------------------------------------------------------------------------------- 1 | Copyright (c) ${years} Arm Limited and Contributors. All rights reserved. 2 | SPDX-License-Identifier: Apache-2.0 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | [build-system] 7 | requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] 8 | build-backend = "setuptools.build_meta" 9 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | mock>=2 2 | coverage 3 | coveralls 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mbed-ls>=1.8.6 2 | PySerial>=3.0,<4.0 3 | setuptools-scm>=1.17.0,<2 4 | prettytable<3.0,>=2.0 5 | six>=1.0,<2.0 6 | colorama>=0.3,<0.5 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """PyPI Package definition for greentea-host (htrun).""" 6 | import os 7 | from io import open 8 | from distutils.core import setup 9 | from setuptools import find_packages 10 | 11 | DESCRIPTION = ( 12 | "greentea-host (htrun) is a command line tool " 13 | "that enables automated testing on embedded platforms." 14 | ) 15 | OWNER_NAMES = "Mbed team" 16 | OWNER_EMAILS = "support@mbed.com" 17 | 18 | repository_dir = os.path.dirname(__file__) 19 | 20 | 21 | def read(fname): 22 | """Read the string content of a file. 23 | 24 | Args: 25 | name: the name of the file to read relative to this file's directory. 26 | 27 | Returns: 28 | String content of the opened file. 29 | """ 30 | with open(os.path.join(repository_dir, fname), mode="r") as f: 31 | return f.read() 32 | 33 | 34 | with open(os.path.join(repository_dir, "requirements.txt")) as fh: 35 | requirements = fh.readlines() 36 | 37 | with open(os.path.join(repository_dir, "test_requirements.txt")) as fh: 38 | test_requirements = fh.readlines() 39 | 40 | python_requires = ">=3.5.*,<4" 41 | setup( 42 | name="greentea-host", 43 | description=DESCRIPTION, 44 | long_description=read("README.md"), 45 | long_description_content_type="text/markdown", 46 | author=OWNER_NAMES, 47 | author_email=OWNER_EMAILS, 48 | maintainer=OWNER_NAMES, 49 | maintainer_email=OWNER_EMAILS, 50 | url="https://github.com/ARMmbed/greentea", 51 | packages=find_packages("src"), 52 | package_dir={"": "src"}, 53 | license="Apache-2.0", 54 | test_suite="test", 55 | entry_points={ 56 | "console_scripts": ["htrun=htrun.htrun:main"], 57 | }, 58 | classifiers=( 59 | "Development Status :: 5 - Production/Stable", 60 | "Intended Audience :: Developers", 61 | "License :: OSI Approved :: Apache Software License", 62 | "Programming Language :: Python :: 3.6", 63 | "Programming Language :: Python :: 3.7", 64 | "Programming Language :: Python :: 3.8", 65 | "Programming Language :: Python :: 3.9", 66 | "Programming Language :: Python", 67 | "Topic :: Software Development :: Build Tools", 68 | "Topic :: Software Development :: Embedded Systems", 69 | "Topic :: Software Development :: Testing", 70 | ), 71 | include_package_data=True, 72 | use_scm_version=True, 73 | python_requires=python_requires, 74 | install_requires=requirements, 75 | tests_require=test_requirements, 76 | extras_require={"pyocd": ["pyocd>=0.32.0"]}, 77 | ) 78 | -------------------------------------------------------------------------------- /src/htrun/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | htrun 4 | -------------------------------------------------------------------------------- /src/htrun/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | """The htrun package. 7 | 8 | Flash, reset and perform host supervised tests on Mbed enabled platforms. 9 | Write your own programs (import this package) or use 'htrun' command line tool 10 | instead. 11 | """ 12 | 13 | import imp 14 | import sys 15 | from optparse import OptionParser 16 | from optparse import SUPPRESS_HELP 17 | from . import host_tests_plugins 18 | from .host_tests_registry import HostRegistry # noqa: F401 19 | from .host_tests import BaseHostTest, event_callback # noqa: F401 20 | 21 | # Set the default baud rate 22 | DEFAULT_BAUD_RATE = 9600 23 | 24 | ############################################################################### 25 | # Functional interface for test supervisor registry 26 | ############################################################################### 27 | 28 | 29 | def get_plugin_caps(methods=None): 30 | """Return the capabilities of a plugin.""" 31 | if not methods: 32 | methods = ["CopyMethod", "ResetMethod"] 33 | result = {} 34 | for method in methods: 35 | result[method] = host_tests_plugins.get_plugin_caps(method) 36 | return result 37 | 38 | 39 | def init_host_test_cli_params(): 40 | """Create CLI parser object and return populated options object. 41 | 42 | Options object can be used to populate host test selector script. 43 | 44 | Returns: 45 | 'options' object returned from OptionParser class. 46 | """ 47 | parser = OptionParser() 48 | 49 | parser.add_option( 50 | "-m", 51 | "--micro", 52 | dest="micro", 53 | help="Target microcontroller name", 54 | metavar="MICRO", 55 | ) 56 | 57 | parser.add_option( 58 | "-p", "--port", dest="port", help="Serial port of the target", metavar="PORT" 59 | ) 60 | 61 | parser.add_option( 62 | "-d", 63 | "--disk", 64 | dest="disk", 65 | help="Target disk (mount point) path", 66 | metavar="DISK_PATH", 67 | ) 68 | 69 | parser.add_option( 70 | "-t", 71 | "--target-id", 72 | dest="target_id", 73 | help="Unique Target Id", 74 | metavar="TARGET_ID", 75 | ) 76 | 77 | parser.add_option( 78 | "", 79 | "--sync", 80 | dest="sync_behavior", 81 | default=2, 82 | type=int, 83 | help=( 84 | "Define how many times __sync packet will be sent to device: 0: " 85 | "none; -1: forever; 1,2,3... - number of times (Default 2 time)" 86 | ), 87 | metavar="SYNC_BEHAVIOR", 88 | ) 89 | 90 | parser.add_option( 91 | "", 92 | "--sync-timeout", 93 | dest="sync_timeout", 94 | default=5, 95 | type=int, 96 | help="Define delay in seconds between __sync packet (Default is 5 seconds)", 97 | metavar="SYNC_TIMEOUT", 98 | ) 99 | 100 | parser.add_option( 101 | "-f", 102 | "--image-path", 103 | dest="image_path", 104 | help="Path with target's binary image", 105 | metavar="IMAGE_PATH", 106 | ) 107 | 108 | copy_methods_str = "Plugin support: " + ", ".join( 109 | host_tests_plugins.get_plugin_caps("CopyMethod") 110 | ) 111 | 112 | parser.add_option( 113 | "-c", 114 | "--copy", 115 | dest="copy_method", 116 | help="Copy (flash the target) method selector. " + copy_methods_str, 117 | metavar="COPY_METHOD", 118 | ) 119 | 120 | parser.add_option( 121 | "", 122 | "--retry-copy", 123 | dest="retry_copy", 124 | default=3, 125 | type=int, 126 | help="Number of attempts to flash the target", 127 | metavar="RETRY_COPY", 128 | ) 129 | 130 | parser.add_option( 131 | "", 132 | "--tag-filters", 133 | dest="tag_filters", 134 | default="", 135 | type=str, 136 | help=( 137 | "Comma seperated list of device tags used when allocating a target " 138 | "to specify required hardware or attributes [--tag-filters tag1,tag2]" 139 | ), 140 | metavar="TAG_FILTERS", 141 | ) 142 | 143 | reset_methods_str = "Plugin support: " + ", ".join( 144 | host_tests_plugins.get_plugin_caps("ResetMethod") 145 | ) 146 | 147 | parser.add_option( 148 | "-r", 149 | "--reset", 150 | dest="forced_reset_type", 151 | help="Forces different type of reset. " + reset_methods_str, 152 | ) 153 | 154 | parser.add_option( 155 | "-C", 156 | "--program_cycle_s", 157 | dest="program_cycle_s", 158 | default=4, 159 | help=( 160 | "Program cycle sleep. Define how many seconds you want wait after " 161 | "copying binary onto target (Default is 4 second)" 162 | ), 163 | type="float", 164 | metavar="PROGRAM_CYCLE_S", 165 | ) 166 | 167 | parser.add_option( 168 | "-R", 169 | "--reset-timeout", 170 | dest="forced_reset_timeout", 171 | default=1, 172 | metavar="NUMBER", 173 | type="float", 174 | help=( 175 | "When forcing a reset using option -r you can set up after reset " 176 | "idle delay in seconds (Default is 1 second)" 177 | ), 178 | ) 179 | 180 | parser.add_option( 181 | "--process-start-timeout", 182 | dest="process_start_timeout", 183 | default=60, 184 | metavar="NUMBER", 185 | type="float", 186 | help=( 187 | "This sets the maximum time in seconds to wait for an internal " 188 | "process to start. This mostly only affects machines under heavy " 189 | "load (Default is 60 seconds)" 190 | ), 191 | ) 192 | 193 | parser.add_option( 194 | "-e", 195 | "--enum-host-tests", 196 | dest="enum_host_tests", 197 | action="append", 198 | default=["./test/host_tests"], 199 | help="Define directory with local host tests", 200 | ) 201 | 202 | parser.add_option( 203 | "", 204 | "--test-cfg", 205 | dest="json_test_configuration", 206 | help="Pass to host test class data about host test configuration", 207 | ) 208 | 209 | parser.add_option( 210 | "", 211 | "--list", 212 | dest="list_reg_hts", 213 | default=False, 214 | action="store_true", 215 | help="Prints registered host test and exits", 216 | ) 217 | 218 | parser.add_option( 219 | "", 220 | "--plugins", 221 | dest="list_plugins", 222 | default=False, 223 | action="store_true", 224 | help="Prints registered plugins and exits", 225 | ) 226 | 227 | parser.add_option( 228 | "-g", 229 | "--grm", 230 | dest="global_resource_mgr", 231 | help=( 232 | 'Global resource manager: ":' 233 | '[:]", Ex. "module_name:10.2.123.43:3334", ' 234 | 'module_name:https://example.com"' 235 | ), 236 | ) 237 | 238 | # Show --fm option only if "fm_agent" module installed 239 | try: 240 | imp.find_module("fm_agent") 241 | except ImportError: 242 | fm_help = SUPPRESS_HELP 243 | else: 244 | fm_help = ( 245 | "Fast Model connection, This option requires mbed-fastmodel-agent " 246 | 'module installed, list CONFIGs via "mbedfm"' 247 | ) 248 | parser.add_option( 249 | "", 250 | "--fm", 251 | dest="fast_model_connection", 252 | metavar="CONFIG", 253 | default=None, 254 | help=fm_help, 255 | ) 256 | 257 | parser.add_option( 258 | "", 259 | "--run", 260 | dest="run_binary", 261 | default=False, 262 | action="store_true", 263 | help="Runs binary image on target (workflow: flash, reset, output console)", 264 | ) 265 | 266 | parser.add_option( 267 | "", 268 | "--skip-flashing", 269 | dest="skip_flashing", 270 | default=False, 271 | action="store_true", 272 | help="Skips use of copy/flash plugin. Note: target will not be reflashed", 273 | ) 274 | 275 | parser.add_option( 276 | "", 277 | "--skip-reset", 278 | dest="skip_reset", 279 | default=False, 280 | action="store_true", 281 | help="Skips use of reset plugin. Note: target will not be reset", 282 | ) 283 | 284 | parser.add_option( 285 | "-P", 286 | "--polling-timeout", 287 | dest="polling_timeout", 288 | default=60, 289 | metavar="NUMBER", 290 | type="int", 291 | help=( 292 | "Timeout in sec for readiness of mount point and serial port of " 293 | "local or remote device. Default 60 sec" 294 | ), 295 | ) 296 | 297 | parser.add_option( 298 | "-b", 299 | "--send-break", 300 | dest="send_break_cmd", 301 | default=False, 302 | action="store_true", 303 | help=( 304 | "Send reset signal to board on specified port (-p PORT) and print " 305 | "serial output. You can combine this with (-r RESET_TYPE) switch" 306 | ), 307 | ) 308 | 309 | parser.add_option( 310 | "", 311 | "--baud-rate", 312 | dest="baud_rate", 313 | help=( 314 | "Baud rate of target, overrides values from mbed-ls, disk/mount " 315 | "point (-d, --disk-path), and serial port -p :" 316 | ), 317 | metavar="BAUD_RATE", 318 | ) 319 | 320 | parser.add_option( 321 | "-v", 322 | "--verbose", 323 | dest="verbose", 324 | default=False, 325 | action="store_true", 326 | help="More verbose mode", 327 | ) 328 | 329 | parser.add_option( 330 | "", 331 | "--serial-output-file", 332 | dest="serial_output_file", 333 | default=None, 334 | help="Save target serial output to this file.", 335 | ) 336 | 337 | parser.add_option( 338 | "", 339 | "--compare-log", 340 | dest="compare_log", 341 | default=None, 342 | help="Log file to compare with the serial output from target.", 343 | ) 344 | 345 | parser.add_option( 346 | "", 347 | "--version", 348 | dest="version", 349 | default=False, 350 | action="store_true", 351 | help="Prints package version and exits", 352 | ) 353 | 354 | parser.add_option( 355 | "", 356 | "--format", 357 | dest="format", 358 | help="Image file format passed to pyocd (elf, bin, hex, axf...).", 359 | ) 360 | 361 | parser.description = ( 362 | """Flash, reset and perform host supervised tests on Mbed enabled platforms""" 363 | ) 364 | parser.epilog = ( 365 | """Example: htrun -d E: -p COM5 -f "test.bin" -C 4 -c shell -m K64F""" 366 | ) 367 | 368 | (options, _) = parser.parse_args() 369 | 370 | if len(sys.argv) == 1: 371 | parser.print_help() 372 | sys.exit() 373 | 374 | return options 375 | -------------------------------------------------------------------------------- /src/htrun/host_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Base host test class.""" 6 | 7 | from .base_host_test import BaseHostTest, event_callback 8 | -------------------------------------------------------------------------------- /src/htrun/host_tests/base_host_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import inspect 7 | import six 8 | from time import time 9 | from inspect import isfunction, ismethod 10 | 11 | 12 | class BaseHostTestAbstract(object): 13 | """Base class for host-test test cases. 14 | 15 | Defines an interface of setup, test and teardown methods subclasses should 16 | implement. 17 | 18 | This class also performs common 'housekeeping' tasks such as pushing/popping 19 | messages on the event_queue and handling test config. 20 | """ 21 | 22 | name = "" # name of the host test (used for local registration) 23 | __event_queue = None # To main even loop 24 | __dut_event_queue = None # To DUT 25 | script_location = None # Path to source file used to load host test 26 | __config = {} 27 | 28 | def __notify_prn(self, text): 29 | if self.__event_queue: 30 | self.__event_queue.put(("__notify_prn", text, time())) 31 | 32 | def __notify_conn_lost(self, text): 33 | if self.__event_queue: 34 | self.__event_queue.put(("__notify_conn_lost", text, time())) 35 | 36 | def __notify_sync_failed(self, text): 37 | if self.__event_queue: 38 | self.__event_queue.put(("__notify_sync_failed", text, time())) 39 | 40 | def __notify_dut(self, key, value): 41 | """Send data over serial to DUT.""" 42 | if self.__dut_event_queue: 43 | self.__dut_event_queue.put((key, value, time())) 44 | 45 | def notify_complete(self, result=None): 46 | """Notify the main event loop that a host test is complete. 47 | 48 | Args: 49 | result: True for success, False failure. 50 | """ 51 | if self.__event_queue: 52 | self.__event_queue.put(("__notify_complete", result, time())) 53 | 54 | def reset_dut(self, value): 55 | """Reset the device under test. 56 | 57 | Args: 58 | value: Value to send with the reset message. 59 | """ 60 | if self.__event_queue: 61 | self.__event_queue.put(("__reset_dut", value, time())) 62 | 63 | def reset(self): 64 | """Reset the device under test and continue running the host test.""" 65 | if self.__event_queue: 66 | self.__event_queue.put(("__reset", "0", time())) 67 | 68 | def notify_conn_lost(self, text): 69 | """Notify main event loop of a DUT-host connection error. 70 | 71 | Args: 72 | text: Additional text to send with the notification. 73 | """ 74 | self.__notify_conn_lost(text) 75 | 76 | def log(self, text): 77 | """Send log message to main event loop. 78 | 79 | Args: 80 | text: Additional text to send with the notification. 81 | """ 82 | self.__notify_prn(text) 83 | 84 | def send_kv(self, key, value): 85 | """Send Key-Value pair to the DUT. 86 | 87 | Args: 88 | key: Key part of KV pair. 89 | value: Value part of KV pair. 90 | """ 91 | self.__notify_dut(key, value) 92 | 93 | def setup_communication(self, event_queue, dut_event_queue, config={}): 94 | """Setup queues used for comms between DUT and host. 95 | 96 | Args: 97 | event_queue: List of KV messages sent toward the host. 98 | dut_event_queue: List of KV messages sent toward the DUT. 99 | config: Test config. 100 | """ 101 | self.__event_queue = event_queue # To main even loop 102 | self.__dut_event_queue = dut_event_queue # To DUT 103 | self.__config = config 104 | 105 | def get_config_item(self, name): 106 | """Get an item from the config by name. 107 | 108 | Args: 109 | name: Name of config parameter to get. 110 | 111 | Returns: 112 | Value of the config parameter with the given name. None if not found. 113 | """ 114 | return self.__config.get(name, None) 115 | 116 | def setup(self): 117 | """Setup tests and callbacks.""" 118 | raise NotImplementedError 119 | 120 | def result(self): 121 | """Return host test result (True, False or None).""" 122 | raise NotImplementedError 123 | 124 | def teardown(self): 125 | """Test teardown.""" 126 | raise NotImplementedError 127 | 128 | 129 | def event_callback(key): 130 | """Decorator for defining a event callback method. 131 | 132 | Adds an "event_key" attribute to the decorated function, which is set to the passed 133 | key. 134 | """ 135 | 136 | def decorator(func): 137 | func.event_key = key 138 | return func 139 | 140 | return decorator 141 | 142 | 143 | class HostTestCallbackBase(BaseHostTestAbstract): 144 | def __init__(self): 145 | BaseHostTestAbstract.__init__(self) 146 | self.__callbacks = {} 147 | self.__restricted_callbacks = [ 148 | "__coverage_start", 149 | "__testcase_start", 150 | "__testcase_finish", 151 | "__testcase_summary", 152 | "__exit", 153 | "__exit_event_queue", 154 | ] 155 | 156 | self.__consume_by_default = [ 157 | "__coverage_start", 158 | "__testcase_start", 159 | "__testcase_finish", 160 | "__testcase_count", 161 | "__testcase_name", 162 | "__testcase_summary", 163 | "__rxd_line", 164 | ] 165 | 166 | self.__assign_default_callbacks() 167 | self.__assign_decorated_callbacks() 168 | 169 | def __callback_default(self, key, value, timestamp): 170 | """Default callback.""" 171 | # self.log("CALLBACK: key=%s, value=%s, timestamp=%f"% (key, value, timestamp)) 172 | pass 173 | 174 | def __default_end_callback(self, key, value, timestamp): 175 | """Default handler for event 'end' that gives test result from target. 176 | 177 | This callback is not decorated as we don't know in what order this 178 | callback will be registered. We want to let users override this callback. 179 | Hence it should be registered before registering user defined callbacks. 180 | """ 181 | self.notify_complete(value == "success") 182 | 183 | def __assign_default_callbacks(self): 184 | """Assign default callback handlers.""" 185 | for key in self.__consume_by_default: 186 | self.__callbacks[key] = self.__callback_default 187 | # Register default handler for event 'end' before assigning user defined 188 | # callbacks to let users over write it. 189 | self.register_callback("end", self.__default_end_callback) 190 | 191 | def __assign_decorated_callbacks(self): 192 | """Look for any callback methods decorated with @event_callback 193 | 194 | Example: 195 | Define a method with @event_callback decorator like: 196 | 197 | @event_callback('') 198 | def event_handler(self, key, value, timestamp): 199 | do something.. 200 | """ 201 | for name, method in inspect.getmembers(self, inspect.ismethod): 202 | key = getattr(method, "event_key", None) 203 | if key: 204 | self.register_callback(key, method) 205 | 206 | def register_callback(self, key, callback, force=False): 207 | """Register callback for a specific event (key: event name). 208 | 209 | Args: 210 | key: Name of the event. 211 | callback: Callable which will be registered for event "key". 212 | force: God mode. 213 | """ 214 | 215 | # Non-string keys are not allowed 216 | if type(key) is not str: 217 | raise TypeError("event non-string keys are not allowed") 218 | 219 | # And finally callback should be callable 220 | if not callable(callback): 221 | raise TypeError("event callback should be callable") 222 | 223 | # Check if callback has all three required parameters (key, value, timestamp) 224 | # When callback is class method should have 4 arguments (self, key, value, 225 | # timestamp) 226 | if ismethod(callback): 227 | arg_count = six.get_function_code(callback).co_argcount 228 | if arg_count != 4: 229 | err_msg = "callback 'self.%s('%s', ...)' defined with %d arguments" % ( 230 | callback.__name__, 231 | key, 232 | arg_count, 233 | ) 234 | err_msg += ( 235 | ", should have 4 arguments: self.%s(self, key, value, timestamp)" 236 | % callback.__name__ 237 | ) 238 | raise TypeError(err_msg) 239 | 240 | # When callback is just a function should have 3 arguments func(key, value, 241 | # timestamp) 242 | if isfunction(callback): 243 | arg_count = six.get_function_code(callback).co_argcount 244 | if arg_count != 3: 245 | err_msg = "callback '%s('%s', ...)' defined with %d arguments" % ( 246 | callback.__name__, 247 | key, 248 | arg_count, 249 | ) 250 | err_msg += ( 251 | ", should have 3 arguments: %s(key, value, timestamp)" 252 | % callback.__name__ 253 | ) 254 | raise TypeError(err_msg) 255 | 256 | if not force: 257 | # Event starting with '__' are reserved 258 | if key.startswith("__"): 259 | raise ValueError("event key starting with '__' are reserved") 260 | 261 | # We predefined few callbacks you can't use 262 | if key in self.__restricted_callbacks: 263 | raise ValueError( 264 | "we predefined few callbacks you can't use e.g. '%s'" % key 265 | ) 266 | 267 | self.__callbacks[key] = callback 268 | 269 | def get_callbacks(self): 270 | return self.__callbacks 271 | 272 | def setup(self): 273 | pass 274 | 275 | def result(self): 276 | pass 277 | 278 | def teardown(self): 279 | pass 280 | 281 | 282 | class BaseHostTest(HostTestCallbackBase): 283 | 284 | __BaseHostTest_Called = False 285 | 286 | def base_host_test_inited(self): 287 | """Check if BaseHostTest ctor was called. 288 | 289 | Call to BaseHostTest is required in order to force required 290 | interfaces implementation. 291 | 292 | Returns: 293 | True if ctor was called. 294 | """ 295 | return self.__BaseHostTest_Called 296 | 297 | def __init__(self): 298 | HostTestCallbackBase.__init__(self) 299 | self.__BaseHostTest_Called = True 300 | -------------------------------------------------------------------------------- /src/htrun/host_tests/default_auto.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Default host test.""" 6 | 7 | from .. import BaseHostTest 8 | 9 | 10 | class DefaultAuto(BaseHostTest): 11 | """Waits for serial port output from the DUT. 12 | 13 | Only recognises the test completion message from greentea-client. 14 | """ 15 | 16 | pass 17 | -------------------------------------------------------------------------------- /src/htrun/host_tests/detect_auto.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Auto detection host test.""" 6 | 7 | import re 8 | from .. import BaseHostTest 9 | 10 | 11 | class DetectPlatformTest(BaseHostTest): 12 | """Test to auto detect the platform.""" 13 | 14 | PATTERN_MICRO_NAME = r"Target '(\w+)'" 15 | re_detect_micro_name = re.compile(PATTERN_MICRO_NAME) 16 | 17 | def result(self): 18 | """Not implemented.""" 19 | raise NotImplementedError 20 | 21 | def test(self, selftest): 22 | """Run test.""" 23 | result = True 24 | 25 | c = selftest.mbed.serial_readline() # {{start}} preamble 26 | if c is None: 27 | return selftest.RESULT_IO_SERIAL 28 | 29 | selftest.notify(c.strip()) 30 | selftest.notify("HOST: Detecting target name...") 31 | 32 | c = selftest.mbed.serial_readline() 33 | if c is None: 34 | return selftest.RESULT_IO_SERIAL 35 | selftest.notify(c.strip()) 36 | 37 | # Check for target name 38 | m = self.re_detect_micro_name.search(c) 39 | if m and len(m.groups()): 40 | micro_name = m.groups()[0] 41 | micro_cmp = selftest.mbed.options.micro == micro_name 42 | result = result and micro_cmp 43 | selftest.notify( 44 | "HOST: MUT Target name '%s', expected '%s'... [%s]" 45 | % ( 46 | micro_name, 47 | selftest.mbed.options.micro, 48 | "OK" if micro_cmp else "FAIL", 49 | ) 50 | ) 51 | 52 | for i in range(0, 2): 53 | c = selftest.mbed.serial_readline() 54 | if c is None: 55 | return selftest.RESULT_IO_SERIAL 56 | selftest.notify(c.strip()) 57 | 58 | return selftest.RESULT_SUCCESS if result else selftest.RESULT_FAILURE 59 | -------------------------------------------------------------------------------- /src/htrun/host_tests/dev_null_auto.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Test dev null.""" 6 | from .. import BaseHostTest 7 | 8 | 9 | class DevNullTest(BaseHostTest): 10 | """DevNullTest.""" 11 | 12 | __result = None 13 | 14 | def _callback_result(self, key, value, timestamp): 15 | # We should not see result data in this test 16 | self.__result = False 17 | 18 | def _callback_to_stdout(self, key, value, timestamp): 19 | self.__result = True 20 | self.log("_callback_to_stdout !") 21 | 22 | def setup(self): 23 | """Set up test.""" 24 | self.register_callback("end", self._callback_result) 25 | self.register_callback("to_null", self._callback_result) 26 | self.register_callback("to_stdout", self._callback_to_stdout) 27 | 28 | def result(self): 29 | """Return test result.""" 30 | return self.__result 31 | -------------------------------------------------------------------------------- /src/htrun/host_tests/echo.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Test device echo.""" 6 | 7 | import uuid 8 | from .. import BaseHostTest 9 | 10 | 11 | class EchoTest(BaseHostTest): 12 | """EchoTest.""" 13 | 14 | __result = None 15 | echo_count = 0 16 | count = 0 17 | uuid_sent = [] 18 | uuid_recv = [] 19 | 20 | def __send_echo_uuid(self): 21 | if self.echo_count: 22 | str_uuid = str(uuid.uuid4()) 23 | self.send_kv("echo", str_uuid) 24 | self.uuid_sent.append(str_uuid) 25 | self.echo_count -= 1 26 | 27 | def _callback_echo(self, key, value, timestamp): 28 | self.uuid_recv.append(value) 29 | self.__send_echo_uuid() 30 | 31 | def _callback_echo_count(self, key, value, timestamp): 32 | # Handshake 33 | self.echo_count = int(value) 34 | self.send_kv(key, value) 35 | # Send first echo to echo server on DUT 36 | self.__send_echo_uuid() 37 | 38 | def setup(self): 39 | """Set up the test.""" 40 | self.register_callback("echo", self._callback_echo) 41 | self.register_callback("echo_count", self._callback_echo_count) 42 | 43 | def result(self): 44 | """Report test result.""" 45 | self.__result = self.uuid_sent == self.uuid_recv 46 | return self.__result 47 | 48 | def teardown(self): 49 | """Tear down test resources.""" 50 | pass 51 | -------------------------------------------------------------------------------- /src/htrun/host_tests/hello_auto.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """'Hello world' test case.""" 6 | from .. import BaseHostTest 7 | 8 | 9 | class HelloTest(BaseHostTest): 10 | """'Hello world' test case.""" 11 | 12 | HELLO_WORLD = "Hello World" 13 | 14 | __result = None 15 | 16 | def _callback_hello_world(self, key, value, timestamp): 17 | self.__result = value == self.HELLO_WORLD 18 | self.notify_complete() 19 | 20 | def setup(self): 21 | """Set up the test.""" 22 | self.register_callback("hello_world", self._callback_hello_world) 23 | 24 | def result(self): 25 | """Return the test result.""" 26 | return self.__result 27 | 28 | def teardown(self): 29 | """Tear down the test case.""" 30 | pass 31 | -------------------------------------------------------------------------------- /src/htrun/host_tests/rtc_auto.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """RTC auto test.""" 6 | 7 | import re 8 | from time import strftime, gmtime 9 | from .. import BaseHostTest 10 | 11 | 12 | class RTCTest(BaseHostTest): 13 | """Test RTC.""" 14 | 15 | PATTERN_RTC_VALUE = r"\[(\d+)\] \[(\d+-\d+-\d+ \d+:\d+:\d+ [AaPpMm]{2})\]" 16 | re_detect_rtc_value = re.compile(PATTERN_RTC_VALUE) 17 | 18 | __result = None 19 | timestamp = None 20 | rtc_reads = [] 21 | 22 | def _callback_timestamp(self, key, value, timestamp): 23 | self.timestamp = int(value) 24 | 25 | def _callback_rtc(self, key, value, timestamp): 26 | self.rtc_reads.append((key, value, timestamp)) 27 | 28 | def _callback_end(self, key, value, timestamp): 29 | self.notify_complete() 30 | 31 | def setup(self): 32 | """Set up the test.""" 33 | self.register_callback("timestamp", self._callback_timestamp) 34 | self.register_callback("rtc", self._callback_rtc) 35 | self.register_callback("end", self._callback_end) 36 | 37 | def result(self): 38 | """Report test result.""" 39 | 40 | def check_strftimes_format(t): 41 | m = self.re_detect_rtc_value.search(t) 42 | if m and len(m.groups()): 43 | sec, time_str = int(m.groups()[0]), m.groups()[1] 44 | correct_time_str = strftime("%Y-%m-%d %H:%M:%S", gmtime(float(sec))) 45 | return time_str == correct_time_str 46 | return False 47 | 48 | ts = [t for _, t, _ in self.rtc_reads] 49 | self.__result = all(filter(check_strftimes_format, ts)) 50 | return self.__result 51 | 52 | def teardown(self): 53 | """Tear down the test.""" 54 | pass 55 | -------------------------------------------------------------------------------- /src/htrun/host_tests/wait_us_auto.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Test reads single characters from stdio and measures time between occurrences.""" 6 | 7 | from .. import BaseHostTest 8 | 9 | 10 | class WaitusTest(BaseHostTest): 11 | """Test ticker timing.""" 12 | 13 | __result = None 14 | DEVIATION = 0.10 # +/-10% 15 | ticks = [] 16 | 17 | def _callback_exit(self, key, value, timeout): 18 | self.notify_complete() 19 | 20 | def _callback_tick(self, key, value, timestamp): 21 | """{{tick;%d}}}.""" 22 | self.log("tick! " + str(timestamp)) 23 | self.ticks.append((key, value, timestamp)) 24 | 25 | def setup(self): 26 | """Set up the test case.""" 27 | self.register_callback("exit", self._callback_exit) 28 | self.register_callback("tick", self._callback_tick) 29 | 30 | def result(self): 31 | """Report test result.""" 32 | 33 | def sub_timestamps(t1, t2): 34 | delta = t1 - t2 35 | deviation = abs(delta - 1.0) 36 | # return True if delta > 0 and deviation <= self.DEVIATION else False 37 | return deviation <= self.DEVIATION 38 | 39 | # Check if time between ticks was accurate 40 | if self.ticks: 41 | # If any ticks were recorded 42 | timestamps = [timestamp for _, _, timestamp in self.ticks] 43 | self.log(str(timestamps)) 44 | m = map(sub_timestamps, timestamps[1:], timestamps[:-1]) 45 | self.log(str(m)) 46 | self.__result = all(m) 47 | else: 48 | self.__result = False 49 | return self.__result 50 | 51 | def teardown(self): 52 | """Tear down test.""" 53 | pass 54 | -------------------------------------------------------------------------------- /src/htrun/host_tests_conn_proxy/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """conn_proxy package.""" 6 | 7 | from .conn_proxy import conn_process 8 | -------------------------------------------------------------------------------- /src/htrun/host_tests_conn_proxy/conn_primitive.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Module defines ConnectorPrimitive base class for device connection and comms.""" 6 | from ..host_tests_logger import HtrunLogger 7 | 8 | 9 | class ConnectorPrimitiveException(Exception): 10 | """Exception in connector primitive module.""" 11 | 12 | pass 13 | 14 | 15 | class ConnectorPrimitive(object): 16 | """Base class for communicating with DUT.""" 17 | 18 | def __init__(self, name): 19 | """Initialise object. 20 | 21 | Args: 22 | name: Name to display in the log. 23 | """ 24 | self.LAST_ERROR = None 25 | self.logger = HtrunLogger(name) 26 | self.polling_timeout = 60 27 | 28 | def write_kv(self, key, value): 29 | """Write a Key-Value protocol message. 30 | 31 | A Key-Value protocol message is in the form '{{key;value}}'. The greentea tests 32 | running on the DUT recognise messages in this format and act according to the 33 | given commands. 34 | 35 | Args: 36 | key: Key part of the Key-Value protocol message. 37 | value: Value part of the Key-Value message. 38 | 39 | Returns: 40 | Buffer containing the K-V message on success, None on failure. 41 | """ 42 | # All Key-Value messages ends with newline character 43 | kv_buff = "{{%s;%s}}" % (key, value) + "\n" 44 | 45 | if self.write(kv_buff): 46 | self.logger.prn_txd(kv_buff.rstrip()) 47 | return kv_buff 48 | else: 49 | return None 50 | 51 | def read(self, count): 52 | """Read data from DUT. 53 | 54 | Args: 55 | count: Number of bytes to read. 56 | 57 | Returns: 58 | Bytes read. 59 | """ 60 | raise NotImplementedError 61 | 62 | def write(self, payload, log=False): 63 | """Write data to the DUT. 64 | 65 | Args: 66 | payload: Buffer with data to send. 67 | log: Set to True to enable logging for this function. 68 | 69 | Returns: 70 | Payload (what was actually sent - if possible to establish that). 71 | """ 72 | raise NotImplementedError 73 | 74 | def flush(self): 75 | """Flush read/write channels of the DUT.""" 76 | raise NotImplementedError 77 | 78 | def reset(self): 79 | """Reset the DUT.""" 80 | raise NotImplementedError 81 | 82 | def connected(self): 83 | """Check if there is a connection to the DUT. 84 | 85 | Returns: 86 | True if there is connection to the DUT (read/write/flush API works). 87 | """ 88 | raise NotImplementedError 89 | 90 | def error(self): 91 | """LAST_ERROR value. 92 | 93 | Returns: 94 | Value of self.LAST_ERROR 95 | """ 96 | return self.LAST_ERROR 97 | 98 | def finish(self): 99 | """Close the connection to the DUT and perform any clean up operations.""" 100 | raise NotImplementedError 101 | -------------------------------------------------------------------------------- /src/htrun/host_tests_conn_proxy/conn_primitive_fastmodel.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Connect to fast models.""" 6 | 7 | from .conn_primitive import ConnectorPrimitive, ConnectorPrimitiveException 8 | 9 | 10 | class FastmodelConnectorPrimitive(ConnectorPrimitive): 11 | """ConnectorPrimitive for a FastModel. 12 | 13 | Wrapper around fm_agent module. 14 | """ 15 | 16 | def __init__(self, name, config): 17 | """Initialise the FastModel. 18 | 19 | Args: 20 | name: Name of the FastModel. 21 | config: Map of config parameters describing the state of the FastModel. 22 | """ 23 | ConnectorPrimitive.__init__(self, name) 24 | self.config = config 25 | self.fm_config = config.get("fm_config", None) 26 | self.platform_name = config.get("platform_name", None) 27 | self.image_path = config.get("image_path", None) 28 | self.polling_timeout = int(config.get("polling_timeout", 60)) 29 | 30 | # FastModel Agent tool-kit 31 | self.fm_agent_module = None 32 | self.resource = None 33 | 34 | # Initialize FastModel 35 | if self.__fastmodel_init(): 36 | 37 | # FastModel Launch load and run, equivalent to DUT connection, flashing and 38 | # reset... 39 | self.__fastmodel_launch() 40 | self.__fastmodel_load(self.image_path) 41 | self.__fastmodel_run() 42 | 43 | def __fastmodel_init(self): 44 | """Import the fm_agent module and set up the FastModel simulator. 45 | 46 | Raises: 47 | ConnectorPrimitiveException: fm_agent import failed, or the FastModel setup 48 | failed. 49 | """ 50 | self.logger.prn_inf("Initializing FastModel...") 51 | 52 | try: 53 | self.fm_agent_module = __import__("fm_agent") 54 | except ImportError as e: 55 | self.logger.prn_err( 56 | "unable to load mbed-fastmodel-agent module. Check if the module " 57 | "install correctly." 58 | ) 59 | self.fm_agent_module = None 60 | self.logger.prn_err("Importing failed : %s" % str(e)) 61 | raise ConnectorPrimitiveException("Importing failed : %s" % str(e)) 62 | try: 63 | self.resource = self.fm_agent_module.FastmodelAgent(logger=self.logger) 64 | self.resource.setup_simulator(self.platform_name, self.fm_config) 65 | if self.__resource_allocated(): 66 | pass 67 | except self.fm_agent_module.SimulatorError as e: 68 | self.logger.prn_err("module fm_agent, create() failed: %s" % str(e)) 69 | raise ConnectorPrimitiveException( 70 | "FastModel Initializing failed as throw SimulatorError!" 71 | ) 72 | 73 | return True 74 | 75 | def __fastmodel_launch(self): 76 | """Start the FastModel. 77 | 78 | Raises: 79 | ConnectorPrimitiveException: Simulator start-up failed. 80 | """ 81 | self.logger.prn_inf("Launching FastModel...") 82 | try: 83 | if not self.resource.start_simulator(): 84 | raise ConnectorPrimitiveException( 85 | "FastModel running failed, run_simulator() return False!" 86 | ) 87 | except self.fm_agent_module.SimulatorError as e: 88 | self.logger.prn_err("start_simulator() failed: %s" % str(e)) 89 | raise ConnectorPrimitiveException( 90 | "FastModel launching failed as throw FastModelError!" 91 | ) 92 | 93 | def __fastmodel_run(self): 94 | """Run the FastModel simulator. 95 | 96 | Raises: 97 | ConnectorPrimitiveException: Failed to run the simulator. 98 | """ 99 | self.logger.prn_inf("Running FastModel...") 100 | try: 101 | if not self.resource.run_simulator(): 102 | raise ConnectorPrimitiveException( 103 | "FastModel running failed, run_simulator() return False!" 104 | ) 105 | except self.fm_agent_module.SimulatorError as e: 106 | self.logger.prn_err("run_simulator() failed: %s" % str(e)) 107 | raise ConnectorPrimitiveException( 108 | "FastModel running failed as throw SimulatorError!" 109 | ) 110 | 111 | def __fastmodel_load(self, filename): 112 | """Load a firmware image to the FastModel. 113 | 114 | This is the functional equivalent of flashing a physical DUT. 115 | 116 | Args: 117 | filename: Path to the image to load. 118 | """ 119 | self.logger.prn_inf("loading FastModel with image '%s'..." % filename) 120 | try: 121 | if not self.resource.load_simulator(filename): 122 | raise ConnectorPrimitiveException( 123 | "FastModel loading failed, load_simulator() return False!" 124 | ) 125 | except self.fm_agent_module.SimulatorError as e: 126 | self.logger.prn_err("run_simulator() failed: %s" % str(e)) 127 | raise ConnectorPrimitiveException( 128 | "FastModel loading failed as throw SimulatorError!" 129 | ) 130 | 131 | def __resource_allocated(self): 132 | """Check if the FastModel agent resource been 'allocated'. 133 | 134 | Returns: 135 | True if the FastModel agent is available. 136 | """ 137 | if self.resource: 138 | return True 139 | else: 140 | self.logger.prn_err("FastModel resource not available!") 141 | return False 142 | 143 | def read(self, count): 144 | """Read data from the FastModel. 145 | 146 | Args: 147 | count: Not used for FastModels. 148 | 149 | Returns: 150 | The data from the FastModel if the read was successful, otherwise False. 151 | """ 152 | if not self.__resource_allocated(): 153 | return False 154 | 155 | try: 156 | return self.resource.read() 157 | except self.fm_agent_module.SimulatorError as e: 158 | self.logger.prn_err( 159 | "FastmodelConnectorPrimitive.read() failed: %s" % str(e) 160 | ) 161 | 162 | def write(self, payload, log=False): 163 | """Send text to the FastModel. 164 | 165 | Args: 166 | payload: Text to send to the FastModel. 167 | log: Log the text payload if True. 168 | """ 169 | if self.__resource_allocated(): 170 | if log: 171 | self.logger.prn_txd(payload) 172 | try: 173 | self.resource.write(payload) 174 | except self.fm_agent_module.SimulatorError as e: 175 | self.logger.prn_err( 176 | "FastmodelConnectorPrimitive.write() failed: %s" % str(e) 177 | ) 178 | else: 179 | return True 180 | else: 181 | return False 182 | 183 | def flush(self): 184 | """Flush is not supported in the FastModel_module.""" 185 | pass 186 | 187 | def connected(self): 188 | """Check if the FastModel is running.""" 189 | if self.__resource_allocated(): 190 | return self.resource.is_simulator_alive 191 | else: 192 | return False 193 | 194 | def finish(self): 195 | """Shut down the FastModel.""" 196 | if self.__resource_allocated(): 197 | try: 198 | self.resource.shutdown_simulator() 199 | self.resource = None 200 | except self.fm_agent_module.SimulatorError as e: 201 | self.logger.prn_err( 202 | "FastmodelConnectorPrimitive.finish() failed: %s" % str(e) 203 | ) 204 | 205 | def reset(self): 206 | """Reset the FastModel.""" 207 | if self.__resource_allocated(): 208 | try: 209 | if not self.resource.reset_simulator(): 210 | self.logger.prn_err( 211 | "FastModel reset failed, reset_simulator() return False!" 212 | ) 213 | except self.fm_agent_module.SimulatorError as e: 214 | self.logger.prn_err( 215 | "FastmodelConnectorPrimitive.reset() failed: %s" % str(e) 216 | ) 217 | 218 | def __del__(self): 219 | """Shut down the FastModel when garbage collected.""" 220 | self.finish() 221 | -------------------------------------------------------------------------------- /src/htrun/host_tests_conn_proxy/conn_primitive_remote.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """ConnectorPrimitive enabling remote communication with a DUT.""" 6 | import time 7 | from .. import DEFAULT_BAUD_RATE 8 | from .conn_primitive import ConnectorPrimitive 9 | 10 | 11 | class RemoteConnectorPrimitive(ConnectorPrimitive): 12 | """Connect to a remote device using a global resource manager (grm). 13 | 14 | This object will import an arbitrary python module it uses as a "remote client", to 15 | connect to a device over IP. The object expects the remote client module name, IP 16 | address and port to be specified in the `config` dictionary passed to __init__. 17 | """ 18 | 19 | def __init__(self, name, config, importer=__import__): 20 | """Populate instance attributes with device and grm data.""" 21 | ConnectorPrimitive.__init__(self, name) 22 | self.config = config 23 | self.target_id = self.config.get("target_id", None) 24 | self.grm_host = config.get("grm_host", None) 25 | self.grm_port = config.get("grm_port", None) 26 | if self.grm_port: 27 | self.grm_port = int(self.grm_port) 28 | self.grm_module = config.get("grm_module", "unknown") 29 | self.platform_name = config.get("platform_name", None) 30 | self.baudrate = config.get("baudrate", DEFAULT_BAUD_RATE) 31 | self.image_path = config.get("image_path", None) 32 | self.forced_reset_timeout = config.get("forced_reset_timeout", 0) 33 | self.allocate_requirements = { 34 | "platform_name": self.platform_name, 35 | "power_on": True, 36 | "connected": True, 37 | } 38 | 39 | if self.config.get("tags"): 40 | self.allocate_requirements["tags"] = {} 41 | for tag in config["tags"].split(","): 42 | self.allocate_requirements["tags"][tag] = True 43 | 44 | # Global Resource Mgr tool-kit 45 | self.remote_module = None 46 | self.selected_resource = None 47 | self.client = None 48 | 49 | # Initialize remote resource manager 50 | self.__remote_init(importer) 51 | 52 | def __remote_init(self, importer): 53 | """Import the "remote client" module, use it to connect to the DUT. 54 | 55 | Args: 56 | importer: Callable that will import the module by name. 57 | """ 58 | # We want to load global resource manager module by name from command line 59 | # (switch --grm) 60 | try: 61 | self.remote_module = importer(self.grm_module) 62 | except ImportError as error: 63 | self.logger.prn_err( 64 | "unable to load global resource manager '%s' module!" % self.grm_module 65 | ) 66 | self.logger.prn_err(str(error)) 67 | self.remote_module = None 68 | return False 69 | 70 | self.logger.prn_inf( 71 | "remote resources initialization: remote(host=%s, port=%s)" 72 | % (self.grm_host, self.grm_port) 73 | ) 74 | 75 | # Connect to remote global resource manager 76 | self.client = self.remote_module.create(host=self.grm_host, port=self.grm_port) 77 | 78 | # First get the resources 79 | resources = self.client.get_resources() 80 | self.logger.prn_inf("remote resources count: %d" % len(resources)) 81 | 82 | # Query for available resource 83 | # Automatic selection and allocation of a resource 84 | try: 85 | self.selected_resource = self.client.allocate(self.allocate_requirements) 86 | except Exception as error: 87 | self.logger.prn_err( 88 | "can't allocate resource: '%s', reason: %s" 89 | % (self.platform_name, str(error)) 90 | ) 91 | return False 92 | 93 | # Remote DUT connection, flashing and reset... 94 | try: 95 | self.__remote_flashing(self.image_path, forceflash=True) 96 | self.__remote_connect(baudrate=self.baudrate) 97 | self.__remote_reset(delay=self.forced_reset_timeout) 98 | except Exception as error: 99 | self.logger.prn_err(str(error)) 100 | self.__remote_release() 101 | return False 102 | return True 103 | 104 | def __remote_connect(self, baudrate=DEFAULT_BAUD_RATE): 105 | """Open a remote connection to the DUT. 106 | 107 | Args: 108 | baudrate: The baud rate the remote client uses to connect to the DUT. 109 | """ 110 | self.logger.prn_inf( 111 | "opening connection to platform at baudrate='%s'" % baudrate 112 | ) 113 | if not self.selected_resource: 114 | raise Exception("remote resource not exists!") 115 | try: 116 | serial_parameters = self.remote_module.SerialParameters(baudrate=baudrate) 117 | self.selected_resource.open_connection(parameters=serial_parameters) 118 | except Exception: 119 | self.logger.prn_inf("open_connection() failed") 120 | raise 121 | 122 | def __remote_disconnect(self): 123 | """Close the connection to the selected DUT.""" 124 | if not self.selected_resource: 125 | raise Exception("remote resource not exists!") 126 | try: 127 | if self.connected(): 128 | self.selected_resource.close_connection() 129 | except Exception as error: 130 | self.logger.prn_err( 131 | "RemoteConnectorPrimitive.disconnect() failed, reason: " + str(error) 132 | ) 133 | 134 | def __remote_reset(self, delay=0): 135 | """Reset the DUT remotely. 136 | 137 | Args: 138 | delay: Time to wait after sending the reset command. 139 | """ 140 | self.logger.prn_inf("remote resources reset...") 141 | if not self.selected_resource: 142 | raise Exception("remote resource not exists!") 143 | try: 144 | if self.selected_resource.reset() is False: 145 | raise Exception("remote resources reset failed!") 146 | except Exception: 147 | self.logger.prn_inf("reset() failed") 148 | raise 149 | 150 | # Post-reset sleep 151 | if delay: 152 | self.logger.prn_inf("waiting %.2f sec after reset" % delay) 153 | time.sleep(delay) 154 | 155 | def __remote_flashing(self, filename, forceflash=False): 156 | """Flash the DUT remotely. 157 | 158 | Args: 159 | filename: Path to the image to flash to the remote target. 160 | forceflash: Force flashing, this is just forwarded to the remote client. 161 | """ 162 | self.logger.prn_inf("remote resources flashing with '%s'..." % filename) 163 | if not self.selected_resource: 164 | raise Exception("remote resource not exists!") 165 | try: 166 | if self.selected_resource.flash(filename, forceflash=forceflash) is False: 167 | raise Exception("remote resource flashing failed!") 168 | except Exception: 169 | self.logger.prn_inf("flash() failed") 170 | raise 171 | 172 | def read(self, count): 173 | """Read data from the DUT. 174 | 175 | Args: 176 | count: Number of bytes to read. 177 | """ 178 | if not self.connected(): 179 | raise Exception("remote resource not exists!") 180 | data = str() 181 | try: 182 | data = self.selected_resource.read(count) 183 | except Exception as error: 184 | self.logger.prn_err( 185 | "RemoteConnectorPrimitive.read(%d): %s" % (count, str(error)) 186 | ) 187 | return data 188 | 189 | def write(self, payload, log=False): 190 | """Send some text to the DUT. 191 | 192 | Args: 193 | payload: Text payload to send to the DUT. 194 | log: Log the payload. 195 | """ 196 | if self.connected(): 197 | try: 198 | self.selected_resource.write(payload) 199 | if log: 200 | self.logger.prn_txd(payload) 201 | return True 202 | except Exception as error: 203 | self.LAST_ERROR = "remote write error: %s" % str(error) 204 | self.logger.prn_err(str(error)) 205 | return False 206 | 207 | def flush(self): 208 | """No-op.""" 209 | pass 210 | 211 | def allocated(self): 212 | """Check if the selected resource is allocated.""" 213 | return ( 214 | self.remote_module 215 | and self.selected_resource 216 | and self.selected_resource.is_allocated 217 | ) 218 | 219 | def connected(self): 220 | """Check if the selected resource is connected.""" 221 | return self.allocated() and self.selected_resource.is_connected 222 | 223 | def __remote_release(self): 224 | """Release the remote resource.""" 225 | try: 226 | if self.allocated(): 227 | self.selected_resource.release() 228 | self.selected_resource = None 229 | except Exception as error: 230 | self.logger.prn_err( 231 | "RemoteConnectorPrimitive.release failed, reason: " + str(error) 232 | ) 233 | 234 | def finish(self): 235 | """Disconnect the resource and release the allocation.""" 236 | if self.allocated(): 237 | self.__remote_disconnect() 238 | self.__remote_release() 239 | 240 | def reset(self): 241 | """Reset the selected resource.""" 242 | self.__remote_reset(delay=self.forced_reset_timeout) 243 | 244 | def __del__(self): 245 | """Disconnect from the remote client when object is garbage collected.""" 246 | self.finish() 247 | -------------------------------------------------------------------------------- /src/htrun/host_tests_conn_proxy/conn_primitive_serial.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Connects to a device's serial port.""" 6 | 7 | import time 8 | from serial import Serial, SerialException 9 | 10 | from .. import host_tests_plugins 11 | from ..host_tests_plugins.host_test_plugins import HostTestPluginBase 12 | from .conn_primitive import ConnectorPrimitive, ConnectorPrimitiveException 13 | 14 | 15 | class SerialConnectorPrimitive(ConnectorPrimitive): 16 | """ConnectorPrimitive implementation using serial IO.""" 17 | 18 | def __init__(self, name, port, baudrate, config): 19 | """Initialise with serial params. 20 | 21 | Args: 22 | name: Target name to display in the log. 23 | port: Serial COM port. 24 | baudrate: Baudrate to use for serial comms. 25 | config: Map of config parameters describing the state of the DUT. 26 | """ 27 | ConnectorPrimitive.__init__(self, name) 28 | self.port = port 29 | self.baudrate = int(baudrate) 30 | self.read_timeout = 0.01 # 10 milli sec 31 | self.write_timeout = 5 32 | self.config = config 33 | self.target_id = self.config.get("target_id", None) 34 | self.mcu = self.config.get("mcu", None) 35 | self.polling_timeout = config.get("polling_timeout", 60) 36 | self.forced_reset_timeout = config.get("forced_reset_timeout", 1) 37 | self.skip_reset = config.get("skip_reset", False) 38 | self.serial = None 39 | 40 | # Assume the provided serial port is good. Don't attempt to use the 41 | # target_id to re-discover the serial port, as the board may not be a 42 | # fully valid DAPLink-compatable or Mbed Enabled board (it may be 43 | # missing a mount point). Do not attempt to check if the serial port 44 | # for given target_id changed. We will attempt to open the port and 45 | # pass the already opened port object (not name) to the reset plugin. 46 | serial_port = None 47 | if self.port is not None: 48 | # A serial port was provided. 49 | # Don't pass in the target_id, so that no change in serial port via 50 | # auto-discovery happens. 51 | self.logger.prn_inf("using specified port '%s'" % (self.port)) 52 | serial_port = HostTestPluginBase().check_serial_port_ready( 53 | self.port, target_id=None, timeout=self.polling_timeout 54 | ) 55 | else: 56 | # No serial port was provided. 57 | # Fallback to auto-discovery via target_id. 58 | self.logger.prn_inf("getting serial port via mbedls)") 59 | serial_port = HostTestPluginBase().check_serial_port_ready( 60 | self.port, target_id=self.target_id, timeout=self.polling_timeout 61 | ) 62 | 63 | if serial_port is None: 64 | raise ConnectorPrimitiveException("Serial port not ready!") 65 | 66 | if serial_port != self.port: 67 | # Serial port changed for given targetID 68 | self.logger.prn_inf( 69 | "serial port changed from '%s to '%s')" % (self.port, serial_port) 70 | ) 71 | self.port = serial_port 72 | 73 | startTime = time.time() 74 | self.logger.prn_inf( 75 | "serial(port=%s, baudrate=%d, read_timeout=%s, write_timeout=%d)" 76 | % (self.port, self.baudrate, self.read_timeout, self.write_timeout) 77 | ) 78 | while time.time() - startTime < self.polling_timeout: 79 | try: 80 | # TIMEOUT: While creating Serial object timeout is delibrately passed as 81 | # 0. Because blocking in Serial.read impacts thread and mutliprocess 82 | # functioning in Python. Hence, instead in self.read() s delay (sleep()) 83 | # is inserted to let serial buffer collect data and avoid spinning on 84 | # non blocking read(). 85 | self.serial = Serial( 86 | self.port, 87 | baudrate=self.baudrate, 88 | timeout=0, 89 | write_timeout=self.write_timeout, 90 | ) 91 | except SerialException as e: 92 | self.serial = None 93 | self.LAST_ERROR = ( 94 | "connection lost, serial.Serial(%s, %d, %d, %d): %s" 95 | % ( 96 | self.port, 97 | self.baudrate, 98 | self.read_timeout, 99 | self.write_timeout, 100 | str(e), 101 | ) 102 | ) 103 | self.logger.prn_err(str(e)) 104 | self.logger.prn_err( 105 | "Retry after 1 sec until %s seconds" % self.polling_timeout 106 | ) 107 | else: 108 | if not self.skip_reset: 109 | self.reset_dev_via_serial(delay=self.forced_reset_timeout) 110 | break 111 | time.sleep(1) 112 | 113 | def reset_dev_via_serial(self, delay=1): 114 | """Reset device using selected method. 115 | 116 | Calls one of the reset plugins. 117 | 118 | Args: 119 | delay: Time to wait after sending the reset command. 120 | """ 121 | reset_type = self.config.get("reset_type", "default") 122 | if not reset_type: 123 | reset_type = "default" 124 | disk = self.config.get("disk", None) 125 | 126 | self.logger.prn_inf("reset device using '%s' plugin..." % reset_type) 127 | result = host_tests_plugins.call_plugin( 128 | "ResetMethod", 129 | reset_type, 130 | serial=self.serial, 131 | disk=disk, 132 | mcu=self.mcu, 133 | target_id=self.target_id, 134 | polling_timeout=self.config.get("polling_timeout"), 135 | ) 136 | # Post-reset sleep 137 | if delay: 138 | self.logger.prn_inf("waiting %.2f sec after reset" % delay) 139 | time.sleep(delay) 140 | self.logger.prn_inf("wait for it...") 141 | return result 142 | 143 | def read(self, count): 144 | """Read data from the serial port RX buffer. 145 | 146 | Args: 147 | count: Number of bytes to read. 148 | """ 149 | # TIMEOUT: Since read is called in a loop, wait for self.timeout period before 150 | # calling serial.read(). See comment on serial.Serial() call above about 151 | # timeout. 152 | time.sleep(self.read_timeout) 153 | c = str() 154 | try: 155 | if self.serial: 156 | c = self.serial.read(count) 157 | except SerialException as e: 158 | self.serial = None 159 | self.LAST_ERROR = "connection lost, serial.read(%d): %s" % (count, str(e)) 160 | self.logger.prn_err(str(e)) 161 | return c 162 | 163 | def write(self, payload, log=False): 164 | """Write data to serial port TX buffer. 165 | 166 | Args: 167 | payload: Bytes to write to the serial port. 168 | log: Log the payload. 169 | """ 170 | try: 171 | if self.serial: 172 | self.serial.write(payload.encode("utf-8")) 173 | if log: 174 | self.logger.prn_txd(payload) 175 | return True 176 | except SerialException as e: 177 | self.serial = None 178 | self.LAST_ERROR = "connection lost, serial.write(%d bytes): %s" % ( 179 | len(payload), 180 | str(e), 181 | ) 182 | self.logger.prn_err(str(e)) 183 | return False 184 | 185 | def flush(self): 186 | """Flush the serial IO.""" 187 | if self.serial: 188 | self.serial.flush() 189 | 190 | def connected(self): 191 | """Return True if connected to serial port.""" 192 | return bool(self.serial) 193 | 194 | def finish(self): 195 | """Close the serial port.""" 196 | if self.serial: 197 | self.serial.close() 198 | 199 | def reset(self): 200 | """Send serial break to reset the device.""" 201 | self.reset_dev_via_serial(self.forced_reset_timeout) 202 | 203 | def __del__(self): 204 | """Release resources when garbage collected.""" 205 | self.finish() 206 | -------------------------------------------------------------------------------- /src/htrun/host_tests_logger/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Logging package.""" 6 | 7 | from .ht_logger import HtrunLogger 8 | -------------------------------------------------------------------------------- /src/htrun/host_tests_logger/ht_logger.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Logger.""" 6 | 7 | import sys 8 | import logging 9 | from functools import partial 10 | 11 | 12 | class HtrunLogger(object): 13 | """Yet another logger flavour.""" 14 | 15 | def __init__(self, name): 16 | """Initialise logger to stdout.""" 17 | logging.basicConfig( 18 | stream=sys.stdout, 19 | format="[%(created).2f][%(name)s]%(message)s", 20 | level=logging.DEBUG, 21 | ) 22 | self.logger = logging.getLogger(name) 23 | self.format_str = "[%(logger_level)s] %(message)s" 24 | 25 | def __prn_log(self, logger_level, text, timestamp=None): 26 | self.logger.debug( 27 | self.format_str 28 | % { 29 | "logger_level": logger_level, 30 | "message": text, 31 | } 32 | ) 33 | 34 | self.prn_dbg = partial(__prn_log, self, "DBG") 35 | self.prn_wrn = partial(__prn_log, self, "WRN") 36 | self.prn_err = partial(__prn_log, self, "ERR") 37 | self.prn_inf = partial(__prn_log, self, "INF") 38 | self.prn_txt = partial(__prn_log, self, "TXT") 39 | self.prn_txd = partial(__prn_log, self, "TXD") 40 | self.prn_rxd = partial(__prn_log, self, "RXD") 41 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | """greentea-host-test-plugins package. 7 | 8 | This package contains plugins used by host test to reset, flash devices etc. 9 | This package can be extended with new packages to add more generic functionality. 10 | """ 11 | 12 | from . import host_test_registry 13 | 14 | # This plugins provide 'flashing' and 'reset' methods to host test scripts 15 | from . import module_copy_shell 16 | from . import module_copy_to_target 17 | from . import module_reset_target 18 | from . import module_power_cycle_target 19 | from . import module_copy_pyocd 20 | from . import module_reset_pyocd 21 | 22 | # Additional, non standard platforms 23 | from . import module_copy_silabs 24 | from . import module_reset_silabs 25 | from . import module_copy_stlink 26 | from . import module_copy_stprogrammer 27 | from . import module_reset_stlink 28 | from . import module_copy_ublox 29 | from . import module_reset_ublox 30 | from . import module_reset_mps2 31 | from . import module_copy_mps2 32 | 33 | # import module_copy_jn51xx 34 | # import module_reset_jn51xx 35 | 36 | 37 | # Plugin registry instance 38 | HOST_TEST_PLUGIN_REGISTRY = host_test_registry.HostTestRegistry() 39 | 40 | # Static plugin registration 41 | # Some plugins are commented out if they are not stable or not commonly used 42 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_to_target.load_plugin()) 43 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_shell.load_plugin()) 44 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_target.load_plugin()) 45 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_pyocd.load_plugin()) 46 | 47 | # Extra platforms support 48 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_mps2.load_plugin()) 49 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_mps2.load_plugin()) 50 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_silabs.load_plugin()) 51 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_silabs.load_plugin()) 52 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_stlink.load_plugin()) 53 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_stprogrammer.load_plugin()) 54 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_stlink.load_plugin()) 55 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_power_cycle_target.load_plugin()) 56 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_pyocd.load_plugin()) 57 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_ublox.load_plugin()) 58 | HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_ublox.load_plugin()) 59 | # HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_copy_jn51xx.load_plugin()) 60 | # HOST_TEST_PLUGIN_REGISTRY.register_plugin(module_reset_jn51xx.load_plugin()) 61 | 62 | # TODO: extend plugin loading to files with name module_*.py loaded ad-hoc 63 | 64 | ############################################################################### 65 | # Functional interface for host test plugin registry 66 | ############################################################################### 67 | 68 | 69 | def call_plugin(type, capability, *args, **kwargs): 70 | """Call a plugin from the HOST_TEST_PLUGIN_REGISTRY. 71 | 72 | Args: 73 | capability: Plugin capability we want to call. 74 | args: Additional parameters passed to plugin. 75 | kwargs: Additional parameters passed to plugin. 76 | 77 | Returns: 78 | True if the call succeeded, otherwise False. 79 | """ 80 | return HOST_TEST_PLUGIN_REGISTRY.call_plugin(type, capability, *args, **kwargs) 81 | 82 | 83 | def get_plugin_caps(type): 84 | """Get a list of all capabilities for a plugin type. 85 | 86 | Args: 87 | type: Type of a plugin. 88 | 89 | Returns: 90 | List of all capabilities for plugin family with the same type. If there are no 91 | capabilities an empty list is returned. 92 | """ 93 | return HOST_TEST_PLUGIN_REGISTRY.get_plugin_caps(type) 94 | 95 | 96 | def get_plugin_info(): 97 | """Get 'information' about the plugins currently in the registry. 98 | 99 | Returns: 100 | Dictionary of 'information' about the plugins in HOST_TEST_PLUGIN_REGISTRY. 101 | """ 102 | return HOST_TEST_PLUGIN_REGISTRY.get_dict() 103 | 104 | 105 | def print_plugin_info(): 106 | """Print plugin information in a user friendly way.""" 107 | print(HOST_TEST_PLUGIN_REGISTRY) 108 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/host_test_registry.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Registry of available host test plugins.""" 6 | 7 | 8 | class HostTestRegistry: 9 | """Register and store host test plugins for further usage.""" 10 | 11 | # Here we actually store all the plugins 12 | PLUGINS = {} # 'Plugin Name' : Plugin Object 13 | 14 | def print_error(self, text): 15 | """Print an error message to the console. 16 | 17 | Args: 18 | text: Error message reason. 19 | """ 20 | print("Plugin load failed. Reason: %s" % text) 21 | 22 | def register_plugin(self, plugin): 23 | """Store a plugin in the registry. 24 | 25 | This method also calls the plugin's setup() method to configure the plugin. 26 | 27 | Args: 28 | plugin: Plugin instance. 29 | 30 | Returns: 31 | True if plugin setup was successful and plugin can be registered, else 32 | False. 33 | """ 34 | # TODO: 35 | # - check for unique caps for specified type 36 | if plugin.name not in self.PLUGINS: 37 | if plugin.setup(): # Setup plugin can be completed without errors 38 | self.PLUGINS[plugin.name] = plugin 39 | return True 40 | else: 41 | self.print_error("%s setup failed" % plugin.name) 42 | else: 43 | self.print_error("%s already loaded" % plugin.name) 44 | return False 45 | 46 | def call_plugin(self, type, capability, *args, **kwargs): 47 | """Execute the first plugin found with a particular 'type' and 'capability'. 48 | 49 | Args: 50 | type: Plugin type. 51 | capability: Plugin capability name. 52 | args: Additional plugin parameters. 53 | kwargs: Additional plugin parameters. 54 | 55 | Returns: 56 | True if a plugin was found and execution succeeded, otherwise False. 57 | """ 58 | for plugin_name in self.PLUGINS: 59 | plugin = self.PLUGINS[plugin_name] 60 | if plugin.type == type and capability in plugin.capabilities: 61 | return plugin.execute(capability, *args, **kwargs) 62 | return False 63 | 64 | def get_plugin_caps(self, type): 65 | """List all capabilities for plugins with the specified type. 66 | 67 | Args: 68 | type: Plugin type. 69 | 70 | Returns: 71 | List of capabilities found. If there are no capabilities an empty 72 | list is returned. 73 | """ 74 | result = [] 75 | for plugin_name in self.PLUGINS: 76 | plugin = self.PLUGINS[plugin_name] 77 | if plugin.type == type: 78 | result.extend(plugin.capabilities) 79 | return sorted(result) 80 | 81 | def load_plugin(self, name): 82 | """Import a plugin module. 83 | 84 | Args: 85 | name: Name of the module to import. 86 | 87 | Returns: 88 | Imported module. 89 | 90 | Raises: 91 | ImportError: The module with the given name was not found. 92 | """ 93 | mod = __import__("module_%s" % name) 94 | return mod 95 | 96 | def get_string(self): 97 | """User friendly printing method to show hooked plugins. 98 | 99 | Returns: 100 | PrettyTable formatted string describing the contents of the plugin 101 | registry. 102 | """ 103 | from prettytable import PrettyTable, HEADER 104 | 105 | column_names = [ 106 | "name", 107 | "type", 108 | "capabilities", 109 | "stable", 110 | "os_support", 111 | "required_parameters", 112 | ] 113 | pt = PrettyTable(column_names, junction_char="|", hrules=HEADER) 114 | for column in column_names: 115 | pt.align[column] = "l" 116 | for plugin_name in sorted(self.PLUGINS.keys()): 117 | name = self.PLUGINS[plugin_name].name 118 | type = self.PLUGINS[plugin_name].type 119 | stable = self.PLUGINS[plugin_name].stable 120 | capabilities = ", ".join(self.PLUGINS[plugin_name].capabilities) 121 | is_os_supported = self.PLUGINS[plugin_name].is_os_supported() 122 | required_parameters = ", ".join( 123 | self.PLUGINS[plugin_name].required_parameters 124 | ) 125 | row = [ 126 | name, 127 | type, 128 | capabilities, 129 | stable, 130 | is_os_supported, 131 | required_parameters, 132 | ] 133 | pt.add_row(row) 134 | return pt.get_string() 135 | 136 | def get_dict(self): 137 | """Return a dictionary of registered plugins.""" 138 | result = {} 139 | for plugin_name in sorted(self.PLUGINS.keys()): 140 | name = self.PLUGINS[plugin_name].name 141 | type = self.PLUGINS[plugin_name].type 142 | stable = self.PLUGINS[plugin_name].stable 143 | capabilities = self.PLUGINS[plugin_name].capabilities 144 | is_os_supported = self.PLUGINS[plugin_name].is_os_supported() 145 | required_parameters = self.PLUGINS[plugin_name].required_parameters 146 | result[plugin_name] = { 147 | "name": name, 148 | "type": type, 149 | "stable": stable, 150 | "capabilities": capabilities, 151 | "os_support": is_os_supported, 152 | "required_parameters": required_parameters, 153 | } 154 | return result 155 | 156 | def __str__(self): 157 | """Return str representation of object.""" 158 | return self.get_string() 159 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_copy_jn51xx.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Copy to devices using JN51xxProgrammer.exe.""" 6 | 7 | import os 8 | from .host_test_plugins import HostTestPluginBase 9 | 10 | 11 | class HostTestPluginCopyMethod_JN51xx(HostTestPluginBase): 12 | """Plugin interface adaptor for the JN51xxProgrammer tool.""" 13 | 14 | name = "HostTestPluginCopyMethod_JN51xx" 15 | type = "CopyMethod" 16 | capabilities = ["jn51xx"] 17 | required_parameters = ["image_path", "serial"] 18 | 19 | def __init__(self): 20 | """Initialise plugin.""" 21 | HostTestPluginBase.__init__(self) 22 | 23 | def is_os_supported(self, os_name=None): 24 | """Plugin only supported on Windows.""" 25 | # If no OS name provided use host OS name 26 | if not os_name: 27 | os_name = self.host_os_support() 28 | 29 | # This plugin only works on Windows 30 | if os_name and os_name.startswith("Windows"): 31 | return True 32 | return False 33 | 34 | def setup(self, *args, **kwargs): 35 | """Configure plugin. 36 | 37 | This function should be called before plugin execute() method is used. 38 | """ 39 | self.JN51XX_PROGRAMMER = "JN51xxProgrammer.exe" 40 | return True 41 | 42 | def execute(self, capability, *args, **kwargs): 43 | """Copy a firmware image to a JN51xx target using JN51xxProgrammer.exe. 44 | 45 | If the "capability" name is not 'jn51xx' this method will just fail. 46 | 47 | Args: 48 | capability: Capability name. 49 | args: Additional arguments. 50 | kwargs: Additional arguments. 51 | 52 | Returns: 53 | True if the copy succeeded, otherwise False. 54 | """ 55 | if not kwargs["image_path"]: 56 | self.print_plugin_error("Error: image path not specified") 57 | return False 58 | 59 | if not kwargs["serial"]: 60 | self.print_plugin_error("Error: serial port not set (not opened?)") 61 | return False 62 | 63 | result = False 64 | if self.check_parameters(capability, *args, **kwargs): 65 | if kwargs["image_path"] and kwargs["serial"]: 66 | image_path = os.path.normpath(kwargs["image_path"]) 67 | serial_port = kwargs["serial"] 68 | if capability == "jn51xx": 69 | # Example: 70 | # JN51xxProgrammer.exe -s COM15 -f -V0 71 | cmd = [ 72 | self.JN51XX_PROGRAMMER, 73 | "-s", 74 | serial_port, 75 | "-f", 76 | image_path, 77 | "-V0", 78 | ] 79 | result = self.run_command(cmd) 80 | return result 81 | 82 | 83 | def load_plugin(): 84 | """Return plugin available in this module.""" 85 | return HostTestPluginCopyMethod_JN51xx() 86 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_copy_mps2.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """MPS2 specific flashing / binary setup functions.""" 6 | 7 | import os 8 | from shutil import copy 9 | from .host_test_plugins import HostTestPluginBase 10 | 11 | 12 | class HostTestPluginCopyMethod_MPS2(HostTestPluginBase): 13 | """Plugin interface adapter for a shell 'copy' command.""" 14 | 15 | name = "HostTestPluginCopyMethod_MPS2" 16 | type = "CopyMethod" 17 | stable = True 18 | capabilities = ["mps2"] 19 | required_parameters = ["image_path", "destination_disk"] 20 | 21 | def __init__(self): 22 | """Initialise plugin.""" 23 | HostTestPluginBase.__init__(self) 24 | 25 | def mps2_copy(self, image_path, destination_disk): 26 | """mps2 copy method for "mbed enabled" devices. 27 | 28 | Copies the file to the MPS2, using shutil.copy. Prepends the file 29 | extension with 'mbed'. 30 | 31 | Args: 32 | image_path: Path to file to be copied. 33 | destination_disk: Path to destination (mbed mount point). 34 | 35 | Returns; 36 | True if copy (flashing) was successful, otherwise False. 37 | """ 38 | result = True 39 | # Keep the same extension in the test spec and on the MPS2 40 | _, extension = os.path.splitext(image_path) 41 | destination_path = os.path.join(destination_disk, "mbed" + extension) 42 | try: 43 | copy(image_path, destination_path) 44 | # sync command on mac ignores command line arguments. 45 | if os.name == "posix": 46 | result = self.run_command("sync -f %s" % destination_path, shell=True) 47 | except Exception as e: 48 | self.print_plugin_error( 49 | "shutil.copy('%s', '%s')" % (image_path, destination_path) 50 | ) 51 | self.print_plugin_error("Error: %s" % str(e)) 52 | result = False 53 | 54 | return result 55 | 56 | def setup(self, *args, **kwargs): 57 | """Configure plugin. 58 | 59 | This function should be called before plugin execute() method is used. 60 | """ 61 | return True 62 | 63 | def execute(self, capability, *args, **kwargs): 64 | """Copy a firmware image to a device using the mps2_copy method. 65 | 66 | If the "capability" name is not 'mps2' this method will just fail. 67 | 68 | Returns: 69 | True if copy operation succeeded, otherwise False. 70 | """ 71 | if not kwargs["image_path"]: 72 | self.print_plugin_error("Error: image path not specified") 73 | return False 74 | 75 | if not kwargs["destination_disk"]: 76 | self.print_plugin_error("Error: destination disk not specified") 77 | return False 78 | 79 | # This optional parameter can be used if TargetID is provided (-t switch) 80 | target_id = kwargs.get("target_id", None) 81 | pooling_timeout = kwargs.get("polling_timeout", 60) 82 | result = False 83 | 84 | if self.check_parameters(capability, *args, **kwargs): 85 | # Capability 'default' is a dummy capability 86 | if kwargs["image_path"] and kwargs["destination_disk"]: 87 | if capability == "mps2": 88 | image_path = os.path.normpath(kwargs["image_path"]) 89 | destination_disk = os.path.normpath(kwargs["destination_disk"]) 90 | # Wait for mount point to be ready 91 | # if mount point changed according to target_id use new mount point 92 | # available in result (_, destination_disk) of 93 | # check_mount_point_ready 94 | result, destination_disk = self.check_mount_point_ready( 95 | destination_disk, target_id=target_id, timeout=pooling_timeout 96 | ) # Blocking 97 | if result: 98 | result = self.mps2_copy(image_path, destination_disk) 99 | return result 100 | 101 | 102 | def load_plugin(): 103 | """Return plugin available in this module.""" 104 | return HostTestPluginCopyMethod_MPS2() 105 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_copy_pyocd.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Flash a firmware image to a device using PyOCD.""" 6 | 7 | import os 8 | from .host_test_plugins import HostTestPluginBase 9 | 10 | try: 11 | from pyocd.core.helpers import ConnectHelper 12 | from pyocd.flash.file_programmer import FileProgrammer 13 | 14 | PYOCD_PRESENT = True 15 | except ImportError: 16 | PYOCD_PRESENT = False 17 | 18 | 19 | class HostTestPluginCopyMethod_pyOCD(HostTestPluginBase): 20 | """Plugin interface adaptor for pyOCD.""" 21 | 22 | name = "HostTestPluginCopyMethod_pyOCD" 23 | type = "CopyMethod" 24 | stable = True 25 | capabilities = ["pyocd"] 26 | required_parameters = ["image_path", "target_id"] 27 | 28 | def __init__(self): 29 | """Initialise plugin.""" 30 | HostTestPluginBase.__init__(self) 31 | 32 | def setup(self, *args, **kwargs): 33 | """Configure plugin. 34 | 35 | This function should be called before plugin execute() method is used. 36 | """ 37 | return True 38 | 39 | def execute(self, capability, *args, **kwargs): 40 | """Flash a firmware image to a device using pyOCD. 41 | 42 | In this implementation we don't seem to care what the capability name is. 43 | 44 | Args: 45 | capability: Capability name. 46 | args: Additional arguments. 47 | kwargs: Additional arguments. 48 | 49 | Returns: 50 | True if flashing succeeded, otherwise False. 51 | """ 52 | if not PYOCD_PRESENT: 53 | self.print_plugin_error( 54 | 'The "pyocd" feature is not installed. Please run ' 55 | '"pip install mbed-os-tools[pyocd]" to enable the "pyocd" copy plugin.' 56 | ) 57 | return False 58 | 59 | if not self.check_parameters(capability, *args, **kwargs): 60 | return False 61 | 62 | if not kwargs["image_path"]: 63 | self.print_plugin_error("Error: image path not specified") 64 | return False 65 | 66 | if not kwargs["target_id"]: 67 | self.print_plugin_error("Error: Target ID") 68 | return False 69 | 70 | target_id = kwargs["target_id"] 71 | image_path = os.path.normpath(kwargs["image_path"]) 72 | with ConnectHelper.session_with_chosen_probe( 73 | unique_id=target_id, resume_on_disconnect=False 74 | ) as session: 75 | # Performance hack! 76 | # Eventually pyOCD will know default clock speed 77 | # per target 78 | test_clock = 10000000 79 | target_type = session.board.target_type 80 | if target_type == "nrf51": 81 | # Override clock since 10MHz is too fast 82 | test_clock = 1000000 83 | if target_type == "ncs36510": 84 | # Override clock since 10MHz is too fast 85 | test_clock = 1000000 86 | 87 | # Configure link 88 | session.probe.set_clock(test_clock) 89 | 90 | # Program the file 91 | programmer = FileProgrammer(session) 92 | programmer.program(image_path, format=kwargs["format"]) 93 | 94 | return True 95 | 96 | 97 | def load_plugin(): 98 | """Return plugin available in this module.""" 99 | return HostTestPluginCopyMethod_pyOCD() 100 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_copy_shell.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Wrapper around cp/xcopy/copy.""" 6 | import os 7 | from os.path import join, basename 8 | from .host_test_plugins import HostTestPluginBase 9 | 10 | 11 | class HostTestPluginCopyMethod_Shell(HostTestPluginBase): 12 | """Plugin interface adaptor for shell copy commands.""" 13 | 14 | # Plugin interface 15 | name = "HostTestPluginCopyMethod_Shell" 16 | type = "CopyMethod" 17 | stable = True 18 | capabilities = ["shell", "cp", "copy", "xcopy"] 19 | required_parameters = ["image_path", "destination_disk"] 20 | 21 | def __init__(self): 22 | """Initialise the plugin.""" 23 | HostTestPluginBase.__init__(self) 24 | 25 | def setup(self, *args, **kwargs): 26 | """Configure plugin. 27 | 28 | This function should be called before plugin execute() method is used. 29 | """ 30 | return True 31 | 32 | def execute(self, capability, *args, **kwargs): 33 | """Copy an image to a destination disk using a shell copy command. 34 | 35 | "capability" is used to select which command to invoke, valid 36 | capabilities are "shell", "cp", "copy" and "xcopy". 37 | 38 | Args: 39 | capability: Capability name. 40 | args: Additional arguments. 41 | kwargs: Additional arguments. 42 | 43 | Returns: 44 | True if the copy succeeded, otherwise False. 45 | """ 46 | if not kwargs["image_path"]: 47 | self.print_plugin_error("Error: image path not specified") 48 | return False 49 | 50 | if not kwargs["destination_disk"]: 51 | self.print_plugin_error("Error: destination disk not specified") 52 | return False 53 | 54 | # This optional parameter can be used if TargetID is provided (-t switch) 55 | target_id = kwargs.get("target_id", None) 56 | pooling_timeout = kwargs.get("polling_timeout", 60) 57 | 58 | result = False 59 | if self.check_parameters(capability, *args, **kwargs): 60 | if kwargs["image_path"] and kwargs["destination_disk"]: 61 | image_path = os.path.normpath(kwargs["image_path"]) 62 | destination_disk = os.path.normpath(kwargs["destination_disk"]) 63 | # Wait for mount point to be ready 64 | # if mount point changed according to target_id use new mount point 65 | # available in result (_, destination_disk) of check_mount_point_ready 66 | mount_res, destination_disk = self.check_mount_point_ready( 67 | destination_disk, target_id=target_id, timeout=pooling_timeout 68 | ) # Blocking 69 | if not mount_res: 70 | return result # mount point is not ready return 71 | # Prepare correct command line parameter values 72 | image_base_name = basename(image_path) 73 | destination_path = join(destination_disk, image_base_name) 74 | if capability == "shell": 75 | if os.name == "nt": 76 | capability = "copy" 77 | elif os.name == "posix": 78 | capability = "cp" 79 | if capability == "cp" or capability == "copy" or capability == "copy": 80 | copy_method = capability 81 | cmd = [copy_method, image_path, destination_path] 82 | if os.name == "posix": 83 | result = self.run_command(cmd, shell=False) 84 | if os.uname()[0] == "Linux": 85 | result = result and self.run_command( 86 | ["sync", "-f", destination_path] 87 | ) 88 | else: 89 | result = result and self.run_command(["sync"]) 90 | else: 91 | result = self.run_command(cmd) 92 | return result 93 | 94 | 95 | def load_plugin(): 96 | """Return plugin available in this module.""" 97 | return HostTestPluginCopyMethod_Shell() 98 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_copy_silabs.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Copy firmware images to silab devices using the eACommander.exe tool.""" 6 | 7 | import os 8 | from .host_test_plugins import HostTestPluginBase 9 | 10 | 11 | class HostTestPluginCopyMethod_Silabs(HostTestPluginBase): 12 | """Plugin interface adapter for eACommander.exe.""" 13 | 14 | name = "HostTestPluginCopyMethod_Silabs" 15 | type = "CopyMethod" 16 | capabilities = ["eACommander", "eACommander-usb"] 17 | required_parameters = ["image_path", "destination_disk"] 18 | stable = True 19 | 20 | def __init__(self): 21 | """Initialise plugin.""" 22 | HostTestPluginBase.__init__(self) 23 | 24 | def setup(self, *args, **kwargs): 25 | """Configure plugin. 26 | 27 | This function should be called before plugin execute() method is used. 28 | """ 29 | self.EACOMMANDER_CMD = "eACommander.exe" 30 | return True 31 | 32 | def execute(self, capability, *args, **kwargs): 33 | """Copy a firmware image to a silab device using eACommander.exe. 34 | 35 | The "capability" name must be eACommander or this method will just fail. 36 | 37 | Args: 38 | capability: Capability name. 39 | args: Additional arguments. 40 | kwargs: Additional arguments. 41 | 42 | Returns: 43 | True if the copy was successful, otherwise False. 44 | """ 45 | result = False 46 | if self.check_parameters(capability, *args, **kwargs) is True: 47 | image_path = os.path.normpath(kwargs["image_path"]) 48 | destination_disk = os.path.normpath(kwargs["destination_disk"]) 49 | if capability == "eACommander": 50 | cmd = [ 51 | self.EACOMMANDER_CMD, 52 | "--serialno", 53 | destination_disk, 54 | "--flash", 55 | image_path, 56 | "--resettype", 57 | "2", 58 | "--reset", 59 | ] 60 | result = self.run_command(cmd) 61 | elif capability == "eACommander-usb": 62 | cmd = [ 63 | self.EACOMMANDER_CMD, 64 | "--usb", 65 | destination_disk, 66 | "--flash", 67 | image_path, 68 | ] 69 | result = self.run_command(cmd) 70 | return result 71 | 72 | 73 | def load_plugin(): 74 | """Return plugin available in this module.""" 75 | return HostTestPluginCopyMethod_Silabs() 76 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_copy_stlink.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Implements a plugin to flash ST devices using ST-LINK-CLI.""" 6 | 7 | import os 8 | from .host_test_plugins import HostTestPluginBase 9 | 10 | 11 | class HostTestPluginCopyMethod_Stlink(HostTestPluginBase): 12 | """Plugin interface adaptor for the ST-LINK-CLI.""" 13 | 14 | # Plugin interface 15 | name = "HostTestPluginCopyMethod_Stlink" 16 | type = "CopyMethod" 17 | capabilities = ["stlink"] 18 | required_parameters = ["image_path"] 19 | 20 | def __init__(self): 21 | """Initialise the object.""" 22 | HostTestPluginBase.__init__(self) 23 | 24 | def is_os_supported(self, os_name=None): 25 | """Check if the OS is supported.""" 26 | # If no OS name provided use host OS name 27 | if not os_name: 28 | os_name = self.host_os_support() 29 | 30 | # This plugin only works on Windows 31 | if os_name and os_name.startswith("Windows"): 32 | return True 33 | return False 34 | 35 | def setup(self, *args, **kwargs): 36 | """Configure plugin. 37 | 38 | This function should be called before plugin execute() method is used. 39 | """ 40 | self.ST_LINK_CLI = "ST-LINK_CLI.exe" 41 | return True 42 | 43 | def execute(self, capability, *args, **kwargs): 44 | """Copy a firmware image to a deice using the ST-LINK-CLI. 45 | 46 | If the "capability" name is not 'stlink' this method will just fail. 47 | 48 | Args: 49 | capability: Capability name. 50 | args: Additional arguments. 51 | kwargs: Additional arguments. 52 | 53 | Returns: 54 | True if the copy succeeded, otherwise False. 55 | """ 56 | result = False 57 | if self.check_parameters(capability, *args, **kwargs) is True: 58 | image_path = os.path.normpath(kwargs["image_path"]) 59 | if capability == "stlink": 60 | # Example: 61 | # ST-LINK_CLI.exe -p \ 62 | # "C:\Work\mbed\build\test\DISCO_F429ZI\GCC_ARM\MBED_A1\basic.bin" 63 | cmd = [self.ST_LINK_CLI, "-p", image_path, "0x08000000", "-V"] 64 | result = self.run_command(cmd) 65 | return result 66 | 67 | 68 | def load_plugin(): 69 | """Return plugin available in this module.""" 70 | return HostTestPluginCopyMethod_Stlink() 71 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_copy_stprogrammer.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | """Implements a plugin to flash ST devices using STM32CubeProgrammer. 7 | 8 | https://www.st.com/en/development-tools/stm32cubeprog.html 9 | """ 10 | 11 | import os 12 | from .host_test_plugins import HostTestPluginBase 13 | 14 | 15 | class HostTestPluginCopyMethod_STProgrammer(HostTestPluginBase): 16 | """Plugin interface adaptor for STM32CubeProgrammer.""" 17 | 18 | # Plugin interface 19 | name = "HostTestPluginCopyMethod_STProgrammer" 20 | type = "CopyMethod" 21 | capabilities = ["stprog"] 22 | required_parameters = ["image_path"] 23 | 24 | def __init__(self): 25 | """Initialise the object.""" 26 | HostTestPluginBase.__init__(self) 27 | 28 | def setup(self, *args, **kwargs): 29 | """Configure plugin. 30 | 31 | This function should be called before plugin execute() method is used. 32 | """ 33 | self.CLI = "STM32_Programmer_CLI" 34 | return True 35 | 36 | def execute(self, capability, *args, **kwargs): 37 | """Copy a firmware image to a device using STM32CubeProgrammer. 38 | 39 | If the "capability" name is not 'stprog' this method will just fail. 40 | 41 | Args: 42 | capability: Capability name. 43 | args: Additional arguments. 44 | kwargs: Additional arguments. 45 | 46 | Returns: 47 | True if the copy succeeded, otherwise False. 48 | """ 49 | from shutil import which 50 | 51 | if which(self.CLI) is None: 52 | print("%s is not part of environment PATH" % self.CLI) 53 | return False 54 | 55 | result = False 56 | if self.check_parameters(capability, *args, **kwargs) is True: 57 | image_path = os.path.normpath(kwargs["image_path"]) 58 | if capability == "stprog": 59 | cmd = [ 60 | self.CLI, 61 | "-c", 62 | "port=SWD", 63 | "mode=UR", 64 | "-w", 65 | image_path, 66 | "0x08000000", 67 | "-v", 68 | "-rst", 69 | ] 70 | result = self.run_command(cmd) 71 | return result 72 | 73 | 74 | def load_plugin(): 75 | """Return plugin available in this module.""" 76 | return HostTestPluginCopyMethod_STProgrammer() 77 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_copy_to_target.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Copy to DAPLink enabled devices using file operations.""" 6 | 7 | import os 8 | from shutil import copy 9 | from .host_test_plugins import HostTestPluginBase 10 | 11 | 12 | class HostTestPluginCopyMethod_Target(HostTestPluginBase): 13 | """Generic flashing method for DAPLink enabled platforms using file copy.""" 14 | 15 | def __init__(self): 16 | """Initialise the plugin.""" 17 | HostTestPluginBase.__init__(self) 18 | 19 | def generic_target_copy(self, image_path, destination_disk): 20 | """Target copy method for "Mbed enabled" devices. 21 | 22 | Args: 23 | image_path: Path to binary file to be flashed. 24 | destination_disk: Path to destination (target mount point). 25 | 26 | Returns: 27 | True if copy (flashing) was successful, otherwise False. 28 | """ 29 | result = True 30 | if not destination_disk.endswith("/") and not destination_disk.endswith("\\"): 31 | destination_disk += "/" 32 | try: 33 | copy(image_path, destination_disk) 34 | except Exception as e: 35 | self.print_plugin_error( 36 | "shutil.copy('%s', '%s')" % (image_path, destination_disk) 37 | ) 38 | self.print_plugin_error("Error: %s" % str(e)) 39 | result = False 40 | return result 41 | 42 | name = "HostTestPluginCopyMethod_Target" 43 | type = "CopyMethod" 44 | stable = True 45 | capabilities = ["shutil", "default"] 46 | required_parameters = ["image_path", "destination_disk"] 47 | 48 | def setup(self, *args, **kwargs): 49 | """Configure plugin.""" 50 | return True 51 | 52 | def execute(self, capability, *args, **kwargs): 53 | """Copy a firmware image to a DAPLink compatible device's filesystem. 54 | 55 | The "capability" name must be "shutil" or this function will just fail. 56 | 57 | Returns: 58 | True if copy was successful, otherwise False. 59 | """ 60 | if not kwargs["image_path"]: 61 | self.print_plugin_error("Error: image path not specified") 62 | return False 63 | 64 | if not kwargs["destination_disk"]: 65 | self.print_plugin_error("Error: destination disk not specified") 66 | return False 67 | 68 | pooling_timeout = kwargs.get("polling_timeout", 60) 69 | 70 | result = False 71 | if self.check_parameters(capability, *args, **kwargs): 72 | # Capability 'default' is a dummy capability 73 | if kwargs["image_path"] and kwargs["destination_disk"]: 74 | if capability == "shutil": 75 | image_path = os.path.normpath(kwargs["image_path"]) 76 | destination_disk = os.path.normpath(kwargs["destination_disk"]) 77 | # Wait for mount point to be ready 78 | # if mount point changed according to target_id use new mount point 79 | # available in result (_, destination_disk) of 80 | # check_mount_point_ready 81 | mount_res, destination_disk = self.check_mount_point_ready( 82 | destination_disk, 83 | target_id=self.target_id, 84 | timeout=pooling_timeout, 85 | ) # Blocking 86 | if mount_res: 87 | result = self.generic_target_copy(image_path, destination_disk) 88 | return result 89 | 90 | 91 | def load_plugin(): 92 | """Return plugin available in this module.""" 93 | return HostTestPluginCopyMethod_Target() 94 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_copy_ublox.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Copy images to ublox devices using FlashErase.exe.""" 6 | 7 | import os 8 | from .host_test_plugins import HostTestPluginBase 9 | 10 | 11 | class HostTestPluginCopyMethod_ublox(HostTestPluginBase): 12 | """Plugin interface adaptor for the FlashErase.exe tool.""" 13 | 14 | name = "HostTestPluginCopyMethod_ublox" 15 | type = "CopyMethod" 16 | capabilities = ["ublox"] 17 | required_parameters = ["image_path"] 18 | 19 | def is_os_supported(self, os_name=None): 20 | """Plugin only works on Windows. 21 | 22 | Args: 23 | os_name: Name of the current OS. 24 | """ 25 | # If no OS name provided use host OS name 26 | if not os_name: 27 | os_name = self.host_os_support() 28 | 29 | # This plugin only works on Windows 30 | if os_name and os_name.startswith("Windows"): 31 | return True 32 | return False 33 | 34 | def setup(self, *args, **kwargs): 35 | """Configure plugin. 36 | 37 | This function should be called before plugin execute() method is used. 38 | """ 39 | self.FLASH_ERASE = "FlashErase.exe" 40 | return True 41 | 42 | def execute(self, capability, *args, **kwargs): 43 | """Copy an image to a ublox device using FlashErase.exe. 44 | 45 | The "capability" name must be "ublox" or this method will just fail. 46 | 47 | Args: 48 | capability: Capability name. 49 | args: Additional arguments. 50 | kwargs: Additional arguments. 51 | 52 | Returns: 53 | True if the copy succeeded, otherwise False. 54 | """ 55 | result = False 56 | if self.check_parameters(capability, *args, **kwargs) is True: 57 | image_path = os.path.normpath(kwargs["image_path"]) 58 | if capability == "ublox": 59 | # Example: 60 | # FLASH_ERASE -c 2 -s 0xD7000 -l 0x20000 -f "binary_file.bin" 61 | cmd = [ 62 | self.FLASH_ERASE, 63 | "-c", 64 | "A", 65 | "-s", 66 | "0xD7000", 67 | "-l", 68 | "0x20000", 69 | "-f", 70 | image_path, 71 | ] 72 | result = self.run_command(cmd) 73 | return result 74 | 75 | 76 | def load_plugin(): 77 | """Return plugin available in this module.""" 78 | return HostTestPluginCopyMethod_ublox() 79 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_power_cycle_target.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Power cycle devices using the 'Mbed TAS RM REST API'.""" 6 | 7 | import os 8 | import json 9 | import time 10 | import requests 11 | from .host_test_plugins import HostTestPluginBase 12 | 13 | 14 | class HostTestPluginPowerCycleResetMethod(HostTestPluginBase): 15 | """Plugin interface adaptor for Mbed TAS RM REST API.""" 16 | 17 | name = "HostTestPluginPowerCycleResetMethod" 18 | type = "ResetMethod" 19 | stable = True 20 | capabilities = ["power_cycle"] 21 | required_parameters = ["target_id", "device_info"] 22 | 23 | def __init__(self): 24 | """Initialise plugin.""" 25 | HostTestPluginBase.__init__(self) 26 | 27 | def setup(self, *args, **kwargs): 28 | """Configure plugin. 29 | 30 | This function should be called before plugin execute() method is used. 31 | """ 32 | return True 33 | 34 | def execute(self, capability, *args, **kwargs): 35 | """Power cycle a device using the TAS RM API. 36 | 37 | If the "capability" name is not "power_cycle" this method will just fail. 38 | 39 | Args: 40 | capability: Capability name. 41 | args: Additional arguments. 42 | kwargs: Additional arguments. 43 | 44 | Returns: 45 | True if the power cycle succeeded, otherwise False. 46 | """ 47 | if "target_id" not in kwargs or not kwargs["target_id"]: 48 | self.print_plugin_error("Error: This plugin requires unique target_id") 49 | return False 50 | 51 | if "device_info" not in kwargs or type(kwargs["device_info"]) is not dict: 52 | self.print_plugin_error( 53 | "Error: This plugin requires dict parameter 'device_info' passed by " 54 | "the caller." 55 | ) 56 | return False 57 | 58 | result = False 59 | if self.check_parameters(capability, *args, **kwargs) is True: 60 | if capability in HostTestPluginPowerCycleResetMethod.capabilities: 61 | target_id = kwargs["target_id"] 62 | device_info = kwargs["device_info"] 63 | ret = self.__get_mbed_tas_rm_addr() 64 | if ret: 65 | ip, port = ret 66 | result = self.__hw_reset(ip, port, target_id, device_info) 67 | return result 68 | 69 | def __get_mbed_tas_rm_addr(self): 70 | """Get IP and Port of mbed tas rm service.""" 71 | try: 72 | ip = os.environ["MBED_TAS_RM_IP"] 73 | port = os.environ["MBED_TAS_RM_PORT"] 74 | return ip, port 75 | except KeyError as e: 76 | self.print_plugin_error( 77 | "HOST: Failed to read environment variable (" 78 | + str(e) 79 | + "). Can't perform hardware reset." 80 | ) 81 | 82 | return None 83 | 84 | def __hw_reset(self, ip, port, target_id, device_info): 85 | """Reset target device using TAS RM API.""" 86 | switch_off_req = { 87 | "name": "switchResource", 88 | "sub_requests": [ 89 | { 90 | "resource_type": "mbed_platform", 91 | "resource_id": target_id, 92 | "switch_command": "OFF", 93 | } 94 | ], 95 | } 96 | 97 | switch_on_req = { 98 | "name": "switchResource", 99 | "sub_requests": [ 100 | { 101 | "resource_type": "mbed_platform", 102 | "resource_id": target_id, 103 | "switch_command": "ON", 104 | } 105 | ], 106 | } 107 | 108 | result = False 109 | 110 | # reset target 111 | switch_off_req = self.__run_request(ip, port, switch_off_req) 112 | if switch_off_req is None: 113 | self.print_plugin_error("HOST: Failed to communicate with TAS RM!") 114 | return result 115 | 116 | if "error" in switch_off_req["sub_requests"][0]: 117 | self.print_plugin_error( 118 | "HOST: Failed to reset target. error = %s" 119 | % switch_off_req["sub_requests"][0]["error"] 120 | ) 121 | return result 122 | 123 | def poll_state(required_state): 124 | switch_state_req = { 125 | "name": "switchResource", 126 | "sub_requests": [ 127 | { 128 | "resource_type": "mbed_platform", 129 | "resource_id": target_id, 130 | "switch_command": "STATE", 131 | } 132 | ], 133 | } 134 | resp = self.__run_request(ip, port, switch_state_req) 135 | start = time.time() 136 | while ( 137 | resp 138 | and ( 139 | resp["sub_requests"][0]["state"] != required_state 140 | or ( 141 | required_state == "ON" 142 | and resp["sub_requests"][0]["mount_point"] == "Not Connected" 143 | ) 144 | ) 145 | and (time.time() - start) < 300 146 | ): 147 | time.sleep(2) 148 | resp = self.__run_request(ip, port, resp) 149 | return resp 150 | 151 | poll_state("OFF") 152 | 153 | self.__run_request(ip, port, switch_on_req) 154 | resp = poll_state("ON") 155 | if ( 156 | resp 157 | and resp["sub_requests"][0]["state"] == "ON" 158 | and resp["sub_requests"][0]["mount_point"] != "Not Connected" 159 | ): 160 | for k, v in resp["sub_requests"][0].viewitems(): 161 | device_info[k] = v 162 | result = True 163 | else: 164 | self.print_plugin_error("HOST: Failed to reset device %s" % target_id) 165 | 166 | return result 167 | 168 | @staticmethod 169 | def __run_request(ip, port, request): 170 | headers = {"Content-type": "application/json", "Accept": "text/plain"} 171 | get_resp = requests.get( 172 | "http://%s:%s/" % (ip, port), data=json.dumps(request), headers=headers 173 | ) 174 | resp = get_resp.json() 175 | if get_resp.status_code == 200: 176 | return resp 177 | else: 178 | return None 179 | 180 | 181 | def load_plugin(): 182 | """Return plugin available in this module.""" 183 | return HostTestPluginPowerCycleResetMethod() 184 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_reset_jn51xx.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Reset method using the JN51xxProgrammer.exe.""" 6 | 7 | from .host_test_plugins import HostTestPluginBase 8 | 9 | 10 | class HostTestPluginResetMethod_JN51xx(HostTestPluginBase): 11 | """Plugin interface adaptor for JN51xxProgrammer.exe.""" 12 | 13 | name = "HostTestPluginResetMethod_JN51xx" 14 | type = "ResetMethod" 15 | capabilities = ["jn51xx"] 16 | required_parameters = ["serial"] 17 | stable = False 18 | 19 | def __init__(self): 20 | """Initialise the plugin.""" 21 | HostTestPluginBase.__init__(self) 22 | 23 | def is_os_supported(self, os_name=None): 24 | """Plugin is only supported on Windows.""" 25 | # If no OS name provided use host OS name 26 | if not os_name: 27 | os_name = self.host_os_support() 28 | 29 | # This plugin only works on Windows 30 | if os_name and os_name.startswith("Windows"): 31 | return True 32 | return False 33 | 34 | def setup(self, *args, **kwargs): 35 | """Configure plugin. 36 | 37 | This function should be called before plugin execute() method is used. 38 | """ 39 | # Note you need to have eACommander.exe on your system path! 40 | self.JN51XX_PROGRAMMER = "JN51xxProgrammer.exe" 41 | return True 42 | 43 | def execute(self, capability, *args, **kwargs): 44 | """Reset a device using the JN51xxProgrammer.exe. 45 | 46 | If the "capability" name is not 'jn51xx' this method will just fail. 47 | 48 | Args: 49 | capability: Capability name. 50 | args: Additional arguments. 51 | kwargs: Additional arguments. 52 | 53 | Returns: 54 | True if the reset succeeded, otherwise False. 55 | """ 56 | if not kwargs["serial"]: 57 | self.print_plugin_error("Error: serial port not set (not opened?)") 58 | return False 59 | 60 | result = False 61 | if self.check_parameters(capability, *args, **kwargs): 62 | if kwargs["serial"]: 63 | if capability == "jn51xx": 64 | # Example: The device should be automatically reset before the 65 | # programmer disconnects. Issuing a command with no file to program 66 | # or read will put the device into programming mode and then reset 67 | # it. E.g. $ JN51xxProgrammer.exe -s COM5 -V0 COM5: Detected JN5179 68 | # with MAC address 00:15:8D:00:01:24:E0:37 69 | serial_port = kwargs["serial"] 70 | cmd = [self.JN51XX_PROGRAMMER, "-s", serial_port, "-V0"] 71 | result = self.run_command(cmd) 72 | return result 73 | 74 | 75 | def load_plugin(): 76 | """Return plugin available in this module.""" 77 | return HostTestPluginResetMethod_JN51xx() 78 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_reset_mps2.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Implements reset method for the ARM_MPS2 platform.""" 6 | 7 | import os 8 | import time 9 | 10 | from .host_test_plugins import HostTestPluginBase 11 | 12 | # Note: This plugin is not fully functional, needs improvements 13 | 14 | 15 | class HostTestPluginResetMethod_MPS2(HostTestPluginBase): 16 | """Plugin used to reset ARM_MPS2 platform. 17 | 18 | Supports reboot.txt startup from standby state, reboots when in run mode. 19 | """ 20 | 21 | # Plugin interface 22 | name = "HostTestPluginResetMethod_MPS2" 23 | type = "ResetMethod" 24 | capabilities = ["reboot.txt"] 25 | required_parameters = ["disk"] 26 | 27 | def __init__(self): 28 | """Initialise the plugin.""" 29 | HostTestPluginBase.__init__(self) 30 | 31 | def touch_file(self, path): 32 | """Touch file and set timestamp to items.""" 33 | with open(path, "a"): 34 | os.utime(path, None) 35 | 36 | def setup(self, *args, **kwargs): 37 | """Prepare / configure plugin to work. 38 | 39 | This method can receive plugin specific parameters by kwargs and 40 | ignore other parameters which may affect other plugins. 41 | """ 42 | return True 43 | 44 | def execute(self, capability, *args, **kwargs): 45 | """Reboot a device. 46 | 47 | The "capability" name must be 'reboot.txt' or this method will just fail. 48 | 49 | Args: 50 | capability: Capability name. 51 | args: Additional arguments. 52 | kwargs: Additional arguments. 53 | 54 | Returns: 55 | True if the reset succeeded, otherwise False. 56 | """ 57 | result = False 58 | if not kwargs["disk"]: 59 | self.print_plugin_error("Error: disk not specified") 60 | return False 61 | 62 | destination_disk = kwargs.get("disk", None) 63 | 64 | # This optional parameter can be used if TargetID is provided (-t switch) 65 | target_id = kwargs.get("target_id", None) 66 | pooling_timeout = kwargs.get("polling_timeout", 60) 67 | if self.check_parameters(capability, *args, **kwargs) is True: 68 | 69 | if capability == "reboot.txt": 70 | reboot_file_path = os.path.join(destination_disk, capability) 71 | reboot_fh = open(reboot_file_path, "w") 72 | reboot_fh.close() 73 | # Make sure the file is written to the board before continuing 74 | if os.name == "posix": 75 | self.run_command("sync -f %s" % reboot_file_path, shell=True) 76 | time.sleep(3) # sufficient delay for device to boot up 77 | result, destination_disk = self.check_mount_point_ready( 78 | destination_disk, target_id=target_id, timeout=pooling_timeout 79 | ) 80 | return result 81 | 82 | 83 | def load_plugin(): 84 | """Return plugin available in this module.""" 85 | return HostTestPluginResetMethod_MPS2() 86 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_reset_pyocd.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Use PyOCD to reset a target.""" 6 | 7 | from .host_test_plugins import HostTestPluginBase 8 | 9 | try: 10 | from pyocd.core.helpers import ConnectHelper 11 | 12 | PYOCD_PRESENT = True 13 | except ImportError: 14 | PYOCD_PRESENT = False 15 | 16 | 17 | class HostTestPluginResetMethod_pyOCD(HostTestPluginBase): 18 | """Plugin interface.""" 19 | 20 | name = "HostTestPluginResetMethod_pyOCD" 21 | type = "ResetMethod" 22 | stable = True 23 | capabilities = ["pyocd"] 24 | required_parameters = ["target_id"] 25 | 26 | def __init__(self): 27 | """Initialise plugin.""" 28 | HostTestPluginBase.__init__(self) 29 | 30 | def setup(self, *args, **kwargs): 31 | """Configure plugin. 32 | 33 | This is a no-op for this plugin. 34 | """ 35 | return True 36 | 37 | def execute(self, capability, *args, **kwargs): 38 | """Reset a target using pyOCD. 39 | 40 | The "capability" name must be "pyocd". If it isn't this method will just fail. 41 | 42 | Args: 43 | capability: Capability name. 44 | 45 | Returns: 46 | True if the reset was successful, otherwise False. 47 | """ 48 | if not PYOCD_PRESENT: 49 | self.print_plugin_error( 50 | 'The "pyocd" feature is not installed. Please run ' 51 | '"pip install mbed-os-tools[pyocd]" to enable the "pyocd" reset plugin.' 52 | ) 53 | return False 54 | 55 | if not kwargs["target_id"]: 56 | self.print_plugin_error("Error: target_id not set") 57 | return False 58 | 59 | result = False 60 | if self.check_parameters(capability, *args, **kwargs) is True: 61 | if kwargs["target_id"]: 62 | if capability == "pyocd": 63 | target_id = kwargs["target_id"] 64 | with ConnectHelper.session_with_chosen_probe( 65 | unique_id=target_id, resume_on_disconnect=False 66 | ) as session: 67 | session.target.reset() 68 | session.target.resume() 69 | result = True 70 | return result 71 | 72 | 73 | def load_plugin(): 74 | """Return plugin available in this module.""" 75 | return HostTestPluginResetMethod_pyOCD() 76 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_reset_silabs.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Implements a reset method using the eACommander tool.""" 6 | 7 | from .host_test_plugins import HostTestPluginBase 8 | 9 | 10 | class HostTestPluginResetMethod_SiLabs(HostTestPluginBase): 11 | """Plugin interface adaptor for the eACommander tool.""" 12 | 13 | # Plugin interface 14 | name = "HostTestPluginResetMethod_SiLabs" 15 | type = "ResetMethod" 16 | capabilities = ["eACommander", "eACommander-usb"] 17 | required_parameters = ["disk"] 18 | stable = True 19 | 20 | def __init__(self): 21 | """Initialise the plugin.""" 22 | HostTestPluginBase.__init__(self) 23 | 24 | def setup(self, *args, **kwargs): 25 | """Configure plugin. 26 | 27 | This function should be called before plugin execute() method is used. 28 | """ 29 | # Note you need to have eACommander.exe on your system path! 30 | self.EACOMMANDER_CMD = "eACommander.exe" 31 | return True 32 | 33 | def execute(self, capability, *args, **kwargs): 34 | """Reset a device using eACommander.exe. 35 | 36 | "capability" is used to select the reset method used by eACommander, 37 | either serial or USB. 38 | 39 | Args: 40 | capability: Capability name. 41 | args: Additional arguments. 42 | kwargs: Additional arguments. 43 | 44 | Returns: 45 | True if the reset succeeded, otherwise False. 46 | """ 47 | result = False 48 | if self.check_parameters(capability, *args, **kwargs) is True: 49 | disk = kwargs["disk"].rstrip("/\\") 50 | 51 | if capability == "eACommander": 52 | # For this copy method 'disk' will be 'serialno' for eACommander command 53 | # line parameters. 54 | # Note: Commands are executed in the order they are specified on the 55 | # command line 56 | cmd = [ 57 | self.EACOMMANDER_CMD, 58 | "--serialno", 59 | disk, 60 | "--resettype", 61 | "2", 62 | "--reset", 63 | ] 64 | result = self.run_command(cmd) 65 | elif capability == "eACommander-usb": 66 | # For this copy method 'disk' will be 'usb address' for eACommander 67 | # command line parameters 68 | # Note: Commands are executed in the order they are specified on the 69 | # command line 70 | cmd = [ 71 | self.EACOMMANDER_CMD, 72 | "--usb", 73 | disk, 74 | "--resettype", 75 | "2", 76 | "--reset", 77 | ] 78 | result = self.run_command(cmd) 79 | return result 80 | 81 | 82 | def load_plugin(): 83 | """Return plugin available in this module.""" 84 | return HostTestPluginResetMethod_SiLabs() 85 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_reset_stlink.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Implements STLINK_CLI copy method.""" 6 | 7 | import os 8 | import sys 9 | import tempfile 10 | from .host_test_plugins import HostTestPluginBase 11 | 12 | FIX_FILE_NAME = "enter_file.txt" 13 | 14 | 15 | class HostTestPluginResetMethod_Stlink(HostTestPluginBase): 16 | """Plugin interface adaptor for STLINK_CLI.""" 17 | 18 | # Plugin interface 19 | name = "HostTestPluginResetMethod_Stlink" 20 | type = "ResetMethod" 21 | capabilities = ["stlink"] 22 | required_parameters = [] 23 | stable = False 24 | 25 | def __init__(self): 26 | """Initialise plugin.""" 27 | HostTestPluginBase.__init__(self) 28 | 29 | def is_os_supported(self, os_name=None): 30 | """Plugin only supported on Windows.""" 31 | # If no OS name provided use host OS name 32 | if not os_name: 33 | os_name = self.host_os_support() 34 | 35 | # This plugin only works on Windows 36 | if os_name and os_name.startswith("Windows"): 37 | return True 38 | return False 39 | 40 | def setup(self, *args, **kwargs): 41 | """Configure plugin. 42 | 43 | This function should be called before plugin execute() method is used. 44 | """ 45 | # Note you need to have eACommander.exe on your system path! 46 | self.ST_LINK_CLI = "ST-LINK_CLI.exe" 47 | return True 48 | 49 | def create_stlink_fix_file(self, file_path): 50 | """Create a file with a line separator. 51 | 52 | This is to work around a bug in ST-LINK CLI that does not let the target run 53 | after burning it. 54 | See https://github.com/ARMmbed/mbed-os-tools/issues/147 for the details. 55 | 56 | Note: 57 | This method will exit the python interpreter if it encounters an OSError. 58 | 59 | Args: 60 | file_path: A path to write into this file. 61 | """ 62 | try: 63 | with open(file_path, "w") as fix_file: 64 | fix_file.write(os.linesep) 65 | except (OSError, IOError): 66 | self.print_plugin_error("Error opening STLINK-PRESS-ENTER-BUG file") 67 | sys.exit(1) 68 | 69 | def execute(self, capability, *args, **kwargs): 70 | """Reset a device using the ST-LINK-CLI. 71 | 72 | Note: 73 | This method will exit the python interpreter if it encounters an OSError. 74 | 75 | Args: 76 | capability: Capability name. 77 | args: Additional arguments. 78 | kwargs: Additional arguments. 79 | 80 | 81 | Returns: 82 | True if the reset succeeded, otherwise False. 83 | """ 84 | result = False 85 | if self.check_parameters(capability, *args, **kwargs) is True: 86 | if capability == "stlink": 87 | # Example: 88 | # ST-LINK_CLI.exe -Rst -Run 89 | cmd = [self.ST_LINK_CLI, "-Rst", "-Run"] 90 | 91 | # Due to the ST-LINK bug, we must press enter after burning the target 92 | # We do this here automatically by passing a file which contains an 93 | # `ENTER` (line separator) to the ST-LINK CLI as `stdin` for the running 94 | # process 95 | enter_file_path = os.path.join(tempfile.gettempdir(), FIX_FILE_NAME) 96 | self.create_stlink_fix_file(enter_file_path) 97 | try: 98 | with open(enter_file_path, "r") as fix_file: 99 | stdin_arg = kwargs.get("stdin", fix_file) 100 | result = self.run_command(cmd, stdin=stdin_arg) 101 | except (OSError, IOError): 102 | self.print_plugin_error("Error opening STLINK-PRESS-ENTER-BUG file") 103 | sys.exit(1) 104 | 105 | return result 106 | 107 | 108 | def load_plugin(): 109 | """Return plugin available in this module.""" 110 | return HostTestPluginResetMethod_Stlink() 111 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_reset_target.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Serial reset plugin.""" 6 | 7 | import re 8 | import pkg_resources 9 | from .host_test_plugins import HostTestPluginBase 10 | 11 | 12 | class HostTestPluginResetMethod_Target(HostTestPluginBase): 13 | """Plugin interface adapter for serial reset.""" 14 | 15 | # Plugin interface 16 | name = "HostTestPluginResetMethod_Target" 17 | type = "ResetMethod" 18 | stable = True 19 | capabilities = ["default"] 20 | required_parameters = ["serial"] 21 | 22 | def __init__(self): 23 | """Initialise plugin.""" 24 | HostTestPluginBase.__init__(self) 25 | self.re_float = re.compile(r"^\d+\.\d+") 26 | pyserial_version = pkg_resources.require("pyserial")[0].version 27 | self.pyserial_version = self.get_pyserial_version(pyserial_version) 28 | self.is_pyserial_v3 = float(self.pyserial_version) >= 3.0 29 | 30 | def get_pyserial_version(self, pyserial_version): 31 | """Retrieve pyserial module version. 32 | 33 | Returns: 34 | Float with pyserial module number. 35 | """ 36 | version = 3.0 37 | m = self.re_float.search(pyserial_version) 38 | if m: 39 | try: 40 | version = float(m.group(0)) 41 | except ValueError: 42 | version = 3.0 # We will assume you've got latest (3.0+) 43 | return version 44 | 45 | def safe_sendBreak(self, serial): 46 | """Closure for pyserial version dependent API calls.""" 47 | if self.is_pyserial_v3: 48 | return self._safe_sendBreak_v3_0(serial) 49 | return self._safe_sendBreak_v2_7(serial) 50 | 51 | def _safe_sendBreak_v2_7(self, serial): 52 | """Pyserial 2.7 API implementation of sendBreak/setBreak. 53 | 54 | Below API is deprecated for pyserial 3.x versions! 55 | http://pyserial.readthedocs.org/en/latest/pyserial_api.html#serial.Serial.sendBreak 56 | http://pyserial.readthedocs.org/en/latest/pyserial_api.html#serial.Serial.setBreak 57 | """ 58 | result = True 59 | try: 60 | serial.sendBreak() 61 | except Exception: 62 | # In Linux a termios.error is raised in sendBreak and in setBreak. 63 | # The following setBreak() is needed to release the reset signal on the 64 | # target mcu. 65 | try: 66 | serial.setBreak(False) 67 | except Exception: 68 | result = False 69 | return result 70 | 71 | def _safe_sendBreak_v3_0(self, serial): 72 | """Pyserial 3.x API implementation of send_break / break_condition. 73 | 74 | http://pyserial.readthedocs.org/en/latest/pyserial_api.html#serial.Serial.send_break 75 | http://pyserial.readthedocs.org/en/latest/pyserial_api.html#serial.Serial.break_condition 76 | """ 77 | result = True 78 | try: 79 | serial.send_break() 80 | except Exception: 81 | # In Linux a termios.error is raised in sendBreak and in setBreak. 82 | # The following break_condition = False is needed to release the reset 83 | # signal on the target mcu. 84 | try: 85 | serial.break_condition = False 86 | except Exception as e: 87 | self.print_plugin_error( 88 | "Error while doing 'serial.break_condition = False' : %s" % str(e) 89 | ) 90 | result = False 91 | return result 92 | 93 | def setup(self, *args, **kwargs): 94 | """Configure plugin. 95 | 96 | This function should be called before plugin execute() method is used. 97 | """ 98 | return True 99 | 100 | def execute(self, capability, *args, **kwargs): 101 | """Reset a device using serial break. 102 | 103 | Args: 104 | capability: Capability name. 105 | args: Additional arguments. 106 | kwargs: Additional arguments. 107 | 108 | Returns: 109 | True if the reset succeeded, otherwise False. 110 | """ 111 | if not kwargs["serial"]: 112 | self.print_plugin_error("Error: serial port not set (not opened?)") 113 | return False 114 | 115 | result = False 116 | if self.check_parameters(capability, *args, **kwargs) is True: 117 | if kwargs["serial"]: 118 | if capability == "default": 119 | serial = kwargs["serial"] 120 | result = self.safe_sendBreak(serial) 121 | return result 122 | 123 | 124 | def load_plugin(): 125 | """Return plugin available in this module.""" 126 | return HostTestPluginResetMethod_Target() 127 | -------------------------------------------------------------------------------- /src/htrun/host_tests_plugins/module_reset_ublox.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Reset ublox devices using the jlink.exe tool.""" 6 | 7 | from .host_test_plugins import HostTestPluginBase 8 | 9 | 10 | class HostTestPluginResetMethod_ublox(HostTestPluginBase): 11 | """Plugin interface adapter for jlink.exe.""" 12 | 13 | name = "HostTestPluginResetMethod_ublox" 14 | type = "ResetMethod" 15 | capabilities = ["ublox"] 16 | required_parameters = [] 17 | stable = False 18 | 19 | def is_os_supported(self, os_name=None): 20 | """Plugin is only supported on Windows.""" 21 | # If no OS name provided use host OS name 22 | if not os_name: 23 | os_name = self.host_os_support() 24 | 25 | # This plugin only works on Windows 26 | if os_name and os_name.startswith("Windows"): 27 | return True 28 | return False 29 | 30 | def setup(self, *args, **kwargs): 31 | """Configure plugin. 32 | 33 | This function should be called before plugin execute() method is used. 34 | 35 | Note: you need to have jlink.exe on your system path! 36 | """ 37 | self.JLINK = "jlink.exe" 38 | return True 39 | 40 | def execute(self, capability, *args, **kwargs): 41 | """Reset a ublox device using jlink.exe. 42 | 43 | The "capability" name must be "ublox" or this method will just fail. 44 | 45 | Args: 46 | capability: Capability name. 47 | args: Additional arguments. 48 | kwargs: Additional arguments. 49 | 50 | Returns: 51 | True if the reset was successful, otherwise False. 52 | """ 53 | result = False 54 | if self.check_parameters(capability, *args, **kwargs) is True: 55 | if capability == "ublox": 56 | # Example: 57 | # JLINK.exe --CommanderScript aCommandFile 58 | cmd = [self.JLINK, "-CommanderScript", r"reset.jlink"] 59 | result = self.run_command(cmd) 60 | return result 61 | 62 | 63 | def load_plugin(): 64 | """Return plugin available in this module.""" 65 | return HostTestPluginResetMethod_ublox() 66 | -------------------------------------------------------------------------------- /src/htrun/host_tests_registry/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | """host_registry package. 7 | 8 | Host registry is used to store all host tests (by id) which can be called from the test 9 | framework. 10 | """ 11 | 12 | from .host_registry import HostRegistry 13 | -------------------------------------------------------------------------------- /src/htrun/host_tests_registry/host_registry.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Registry of available host tests.""" 6 | 7 | try: 8 | from imp import load_source 9 | except ImportError: 10 | import importlib 11 | import sys 12 | 13 | def load_source(module_name, file_path): 14 | """Dynamically import a plugin module. 15 | 16 | Args: 17 | module_name: Name of the module to load. 18 | file_path: Path to the module. 19 | """ 20 | spec = importlib.util.spec_from_file_location(module_name, file_path) 21 | module = importlib.util.module_from_spec(spec) 22 | spec.loader.exec_module(module) 23 | sys.modules[module_name] = module 24 | return module 25 | 26 | 27 | from inspect import getmembers, isclass 28 | from os import listdir 29 | from os.path import abspath, exists, isdir, isfile, join 30 | 31 | from ..host_tests.base_host_test import BaseHostTest 32 | 33 | 34 | class HostRegistry: 35 | """Class stores registry with host tests and objects representing them.""" 36 | 37 | HOST_TESTS = {} # Map between host_test_name -> host_test_object 38 | 39 | def register_host_test(self, ht_name, ht_object): 40 | """Register host test object by name. 41 | 42 | Args: 43 | ht_name: Host test unique name. 44 | ht_object: Host test class object. 45 | """ 46 | if ht_name not in self.HOST_TESTS: 47 | self.HOST_TESTS[ht_name] = ht_object 48 | 49 | def unregister_host_test(self, ht_name): 50 | """Unregister host test object by name. 51 | 52 | Args: 53 | ht_name: Host test unique name. 54 | """ 55 | if ht_name in self.HOST_TESTS: 56 | del self.HOST_TESTS[ht_name] 57 | 58 | def get_host_test(self, ht_name): 59 | """Fetch host test object by name. 60 | 61 | Args: 62 | ht_name: Host test unique name. 63 | 64 | Returns: 65 | Host test callable object or None if object is not found. 66 | """ 67 | return self.HOST_TESTS[ht_name] if ht_name in self.HOST_TESTS else None 68 | 69 | def is_host_test(self, ht_name): 70 | """Check (by name) if host test object is registered already. 71 | 72 | Args: 73 | ht_name: Host test unique name. 74 | 75 | Returns: 76 | True if ht_name is registered (available), else False. 77 | """ 78 | return ht_name in self.HOST_TESTS and self.HOST_TESTS[ht_name] is not None 79 | 80 | def table(self, verbose=False): 81 | """Print list of registered host test classes (by name). 82 | 83 | For dev & debug purposes. 84 | """ 85 | from prettytable import PrettyTable, HEADER 86 | 87 | column_names = ["name", "class", "origin"] 88 | pt = PrettyTable(column_names, junction_char="|", hrules=HEADER) 89 | for column in column_names: 90 | pt.align[column] = "l" 91 | 92 | for name, host_test in sorted(self.HOST_TESTS.items()): 93 | cls_str = str(host_test.__class__) 94 | if host_test.script_location: 95 | src_path = host_test.script_location 96 | else: 97 | src_path = "htrun" 98 | pt.add_row([name, cls_str, src_path]) 99 | return pt.get_string() 100 | 101 | def register_from_path(self, path, verbose=False): 102 | """Enumerate and register locally stored host tests. 103 | 104 | Host test are derived from htrun.BaseHostTest classes 105 | 106 | Args: 107 | path: Path to the host tests directory. 108 | verbose: Enable verbose logging. 109 | """ 110 | if path: 111 | path = path.strip('"') 112 | if verbose: 113 | print("HOST: Inspecting '%s' for local host tests..." % path) 114 | if exists(path) and isdir(path): 115 | python_modules = [ 116 | f 117 | for f in listdir(path) 118 | if isfile(join(path, f)) and f.endswith(".py") 119 | ] 120 | for module_file in python_modules: 121 | self._add_module_to_registry(path, module_file, verbose) 122 | 123 | def _add_module_to_registry(self, path, module_file, verbose): 124 | module_name = module_file[:-3] 125 | try: 126 | mod = load_source(module_name, abspath(join(path, module_file))) 127 | except Exception as e: 128 | print( 129 | "HOST: Error while loading local host test module '%s'" 130 | % join(path, module_file) 131 | ) 132 | print("HOST: %s" % str(e)) 133 | return 134 | if verbose: 135 | print("HOST: Loading module '%s': %s" % (module_file, str(mod))) 136 | 137 | for name, obj in getmembers(mod): 138 | if ( 139 | isclass(obj) 140 | and issubclass(obj, BaseHostTest) 141 | and str(obj) != str(BaseHostTest) 142 | ): 143 | if obj.name: 144 | host_test_name = obj.name 145 | else: 146 | host_test_name = module_name 147 | host_test_cls = obj 148 | host_test_cls.script_location = join(path, module_file) 149 | if verbose: 150 | print( 151 | "HOST: Found host test implementation: %s -|> %s" 152 | % (str(obj), str(BaseHostTest)) 153 | ) 154 | print( 155 | "HOST: Registering '%s' as '%s'" 156 | % (str(host_test_cls), host_test_name) 157 | ) 158 | self.register_host_test(host_test_name, host_test_cls()) 159 | -------------------------------------------------------------------------------- /src/htrun/host_tests_runner/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """greentea-host-test-runner. 6 | 7 | This package contains basic host test implementation with algorithms to flash 8 | and reset devices. Functionality can be overridden by set of plugins which can 9 | provide specialised flashing and reset implementations. 10 | """ 11 | 12 | from pkg_resources import get_distribution 13 | 14 | __version__ = get_distribution("greentea-host").version 15 | -------------------------------------------------------------------------------- /src/htrun/host_tests_runner/host_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Classes performing test selection, test execution and reporting of test results.""" 6 | 7 | from sys import stdout 8 | from .target_base import TargetBase 9 | from . import __version__ 10 | 11 | 12 | class HostTestResults(object): 13 | """Test results set by host tests.""" 14 | 15 | def enum(self, **enums): 16 | """Return a new base type. 17 | 18 | Args: 19 | enums: Dictionary of namespaces for the type. 20 | """ 21 | return type("Enum", (), enums) 22 | 23 | def __init__(self): 24 | """Initialise the test result type.""" 25 | self.TestResults = self.enum( 26 | RESULT_SUCCESS="success", 27 | RESULT_FAILURE="failure", 28 | RESULT_ERROR="error", 29 | RESULT_END="end", 30 | RESULT_UNDEF="undefined", 31 | RESULT_TIMEOUT="timeout", 32 | RESULT_IOERR_COPY="ioerr_copy", 33 | RESULT_IOERR_DISK="ioerr_disk", 34 | RESULT_IO_SERIAL="ioerr_serial", 35 | RESULT_NO_IMAGE="no_image", 36 | RESULT_NOT_DETECTED="not_detected", 37 | RESULT_MBED_ASSERT="mbed_assert", 38 | RESULT_PASSIVE="passive", 39 | RESULT_BUILD_FAILED="build_failed", 40 | RESULT_SYNC_FAILED="sync_failed", 41 | ) 42 | 43 | # Magically creates attributes in this class corresponding 44 | # to RESULT_ elements in self.TestResults enum 45 | for attr in self.TestResults.__dict__: 46 | if attr.startswith("RESULT_"): 47 | setattr(self, attr, self.TestResults.__dict__[attr]) 48 | 49 | # Indexes of this list define string->int mapping between 50 | # actual strings with results 51 | self.TestResultsList = [ 52 | self.TestResults.RESULT_SUCCESS, 53 | self.TestResults.RESULT_FAILURE, 54 | self.TestResults.RESULT_ERROR, 55 | self.TestResults.RESULT_END, 56 | self.TestResults.RESULT_UNDEF, 57 | self.TestResults.RESULT_TIMEOUT, 58 | self.TestResults.RESULT_IOERR_COPY, 59 | self.TestResults.RESULT_IOERR_DISK, 60 | self.TestResults.RESULT_IO_SERIAL, 61 | self.TestResults.RESULT_NO_IMAGE, 62 | self.TestResults.RESULT_NOT_DETECTED, 63 | self.TestResults.RESULT_MBED_ASSERT, 64 | self.TestResults.RESULT_PASSIVE, 65 | self.TestResults.RESULT_BUILD_FAILED, 66 | self.TestResults.RESULT_SYNC_FAILED, 67 | ] 68 | 69 | def get_test_result_int(self, test_result_str): 70 | """Map test result string to unique integer. 71 | 72 | Args: 73 | test_result_str: Test results as a string. 74 | """ 75 | if test_result_str in self.TestResultsList: 76 | return self.TestResultsList.index(test_result_str) 77 | return -1 78 | 79 | def __getitem__(self, test_result_str): 80 | """Return integer test result code. 81 | 82 | Args: 83 | test_result_str: Test results as a string. 84 | """ 85 | return self.get_test_result_int(test_result_str) 86 | 87 | 88 | class Test(HostTestResults): 89 | """Base class for host test's test runner.""" 90 | 91 | def __init__(self, options): 92 | """Initialise the test runner. 93 | 94 | Args: 95 | options: Options instance describing the target. 96 | """ 97 | HostTestResults.__init__(self) 98 | self.target = TargetBase(options) 99 | 100 | def run(self): 101 | """Run a host test.""" 102 | pass 103 | 104 | def setup(self): 105 | """Set up and check if configuration for test is correct.""" 106 | pass 107 | 108 | def notify(self, msg): 109 | """Write a message to stdout. 110 | 111 | Flush immediately so the buffered data is immediately written to stdout. 112 | 113 | Args: 114 | msg: Text to write to stdout. 115 | """ 116 | stdout.write(msg) 117 | stdout.flush() 118 | 119 | def print_result(self, result): 120 | """Print test results in "KV" format packets. 121 | 122 | Args: 123 | result: A member of HostTestResults.RESULT_* enums. 124 | """ 125 | self.notify("{{%s}}\n" % result) 126 | self.notify("{{%s}}\n" % self.RESULT_END) 127 | 128 | def finish(self): 129 | """Finishes tasks and closes resources.""" 130 | pass 131 | 132 | def get_hello_string(self): 133 | """Hello string used as first print.""" 134 | return "host test executor ver. " + __version__ 135 | 136 | 137 | class DefaultTestSelectorBase(Test): 138 | """Test class with serial port initialization. 139 | 140 | This is a base for other test selectors. 141 | """ 142 | 143 | def __init__(self, options): 144 | """Initialise test selector. 145 | 146 | Args: 147 | options: Options instance describing the target. 148 | """ 149 | Test.__init__(self, options=options) 150 | -------------------------------------------------------------------------------- /src/htrun/host_tests_runner/target_base.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Base class for targets.""" 6 | 7 | import json 8 | import os 9 | from time import sleep 10 | from .. import host_tests_plugins as ht_plugins 11 | from mbed_lstools.main import create 12 | from .. import DEFAULT_BAUD_RATE 13 | from ..host_tests_logger import HtrunLogger 14 | 15 | 16 | class TargetBase: 17 | """TargetBase class for a host driven test. 18 | 19 | This class stores information necessary to communicate with the device 20 | under test. It is responsible for managing serial port communication 21 | between the host and the device. 22 | """ 23 | 24 | def __init__(self, options): 25 | """Initialise common target attributes.""" 26 | self.options = options 27 | self.logger = HtrunLogger("Greentea") 28 | # Options related to copy / reset the connected target device 29 | self.port = self.options.port 30 | self.mcu = self.options.micro 31 | self.disk = self.options.disk 32 | self.target_id = self.options.target_id 33 | self.image_path = ( 34 | self.options.image_path.strip('"') 35 | if self.options.image_path is not None 36 | else "" 37 | ) 38 | self.copy_method = self.options.copy_method 39 | self.retry_copy = self.options.retry_copy 40 | self.program_cycle_s = float( 41 | self.options.program_cycle_s 42 | if self.options.program_cycle_s is not None 43 | else 2.0 44 | ) 45 | self.polling_timeout = self.options.polling_timeout 46 | 47 | # Serial port settings 48 | self.serial_baud = DEFAULT_BAUD_RATE 49 | self.serial_timeout = 1 50 | 51 | # Users can use command to pass port speeds together with port name. E.g. 52 | # COM4:115200:1 53 | # Format is PORT:SPEED:TIMEOUT 54 | port_config = self.port.split(":") if self.port else "" 55 | if len(port_config) == 2: 56 | # -p COM4:115200 57 | self.port = port_config[0] 58 | self.serial_baud = int(port_config[1]) 59 | elif len(port_config) == 3: 60 | # -p COM4:115200:0.5 61 | self.port = port_config[0] 62 | self.serial_baud = int(port_config[1]) 63 | self.serial_timeout = float(port_config[2]) 64 | 65 | # Overriding baud rate value with command line specified value 66 | self.serial_baud = ( 67 | self.options.baud_rate if self.options.baud_rate else self.serial_baud 68 | ) 69 | 70 | # Test configuration in JSON format 71 | self.test_cfg = None 72 | if self.options.json_test_configuration is not None: 73 | # We need to normalize path before we open file 74 | json_test_configuration_path = self.options.json_test_configuration.strip( 75 | "\"'" 76 | ) 77 | try: 78 | self.logger.prn_inf( 79 | "Loading test configuration from '%s'..." 80 | % json_test_configuration_path 81 | ) 82 | with open(json_test_configuration_path) as data_file: 83 | self.test_cfg = json.load(data_file) 84 | except IOError as e: 85 | self.logger.prn_err( 86 | "Test configuration JSON file '{0}' I/O error({1}): {2}".format( 87 | json_test_configuration_path, e.errno, e.strerror 88 | ) 89 | ) 90 | except Exception as e: 91 | self.logger.prn_err("Test configuration JSON Unexpected error:", str(e)) 92 | raise 93 | 94 | def copy_image( 95 | self, 96 | image_path=None, 97 | disk=None, 98 | copy_method=None, 99 | port=None, 100 | mcu=None, 101 | retry_copy=5, 102 | ): 103 | """Copy an image to a target. 104 | 105 | Returns: 106 | True if the copy succeeded, otherwise False. 107 | """ 108 | 109 | def get_remount_count(disk_path, tries=2): 110 | """Get the remount count from 'DETAILS.TXT' file. 111 | 112 | Returns: 113 | Remount count, or None if not available. 114 | """ 115 | # In case of no disk path, nothing to do 116 | if disk_path is None: 117 | return None 118 | 119 | for cur_try in range(1, tries + 1): 120 | try: 121 | files_on_disk = [x.upper() for x in os.listdir(disk_path)] 122 | if "DETAILS.TXT" in files_on_disk: 123 | with open( 124 | os.path.join(disk_path, "DETAILS.TXT"), "r" 125 | ) as details_txt: 126 | for line in details_txt.readlines(): 127 | if "Remount count:" in line: 128 | return int(line.replace("Remount count: ", "")) 129 | # Remount count not found in file 130 | return None 131 | # 'DETAILS.TXT file not found 132 | else: 133 | return None 134 | 135 | except OSError as e: 136 | self.logger.prn_err( 137 | "Failed to get remount count due to OSError.", str(e) 138 | ) 139 | self.logger.prn_inf( 140 | "Retrying in 1 second (try %s of %s)" % (cur_try, tries) 141 | ) 142 | sleep(1) 143 | # Failed to get remount count 144 | return None 145 | 146 | def check_flash_error(target_id, disk, initial_remount_count): 147 | """Check for flash errors. 148 | 149 | Returns: 150 | False if FAIL.TXT present, else True. 151 | """ 152 | if not target_id: 153 | self.logger.prn_wrn( 154 | "Target ID not found: Skipping flash check and retry" 155 | ) 156 | return True 157 | 158 | if copy_method not in ["shell", "default"]: 159 | # We're using a "copy method" that may not necessarily require 160 | # an "Mbed Enabled" device. In this case we shouldn't use 161 | # mbedls.detect to attempt to rediscover the mount point, as 162 | # mbedls.detect is only compatible with Mbed Enabled devices. 163 | # It's best just to return `True` and continue here. This will 164 | # avoid the inevitable 2.5s delay caused by us repeatedly 165 | # attempting to enumerate Mbed Enabled devices in the code 166 | # below when none are connected. The user has specified a 167 | # non-Mbed plugin copy method, so we shouldn't delay them by 168 | # trying to check for Mbed Enabled devices. 169 | return True 170 | 171 | bad_files = set(["FAIL.TXT"]) 172 | # Re-try at max 5 times with 0.5 sec in delay 173 | for i in range(5): 174 | # mbed_lstools.main.create() should be done inside the loop. Otherwise 175 | # it will loop on same data. 176 | mbeds = create() 177 | mbed_list = mbeds.list_mbeds() # list of mbeds present 178 | # get first item in list with a matching target_id, if present 179 | mbed_target = next( 180 | (x for x in mbed_list if x["target_id"] == target_id), None 181 | ) 182 | 183 | if mbed_target is not None: 184 | if ( 185 | "mount_point" in mbed_target 186 | and mbed_target["mount_point"] is not None 187 | ): 188 | if initial_remount_count is not None: 189 | new_remount_count = get_remount_count(disk) 190 | if ( 191 | new_remount_count is not None 192 | and new_remount_count == initial_remount_count 193 | ): 194 | sleep(0.5) 195 | continue 196 | 197 | common_items = [] 198 | try: 199 | items = set( 200 | [ 201 | x.upper() 202 | for x in os.listdir(mbed_target["mount_point"]) 203 | ] 204 | ) 205 | common_items = bad_files.intersection(items) 206 | except OSError: 207 | print("Failed to enumerate disk files, retrying") 208 | continue 209 | 210 | for common_item in common_items: 211 | full_path = os.path.join( 212 | mbed_target["mount_point"], common_item 213 | ) 214 | self.logger.prn_err("Found %s" % (full_path)) 215 | bad_file_contents = "[failed to read bad file]" 216 | try: 217 | with open(full_path, "r") as bad_file: 218 | bad_file_contents = bad_file.read() 219 | except IOError as error: 220 | self.logger.prn_err( 221 | "Error opening '%s': %s" % (full_path, error) 222 | ) 223 | 224 | self.logger.prn_err( 225 | "Error file contents:\n%s" % bad_file_contents 226 | ) 227 | if common_items: 228 | return False 229 | sleep(0.5) 230 | return True 231 | 232 | # Set-up closure environment 233 | if not image_path: 234 | image_path = self.image_path 235 | if not disk: 236 | disk = self.disk 237 | if not copy_method: 238 | copy_method = self.copy_method 239 | if not port: 240 | port = self.port 241 | if not mcu: 242 | mcu = self.mcu 243 | if not retry_copy: 244 | retry_copy = self.retry_copy 245 | target_id = self.target_id 246 | 247 | if not image_path: 248 | self.logger.prn_err("Error: image path not specified") 249 | return False 250 | 251 | if not os.path.isfile(image_path): 252 | self.logger.prn_err("Error: image file (%s) not found" % image_path) 253 | return False 254 | 255 | for count in range(0, retry_copy): 256 | initial_remount_count = get_remount_count(disk) 257 | # Call proper copy method 258 | result = self.copy_image_raw(image_path, disk, copy_method, port, mcu) 259 | sleep(self.program_cycle_s) 260 | if not result: 261 | continue 262 | result = check_flash_error(target_id, disk, initial_remount_count) 263 | if result: 264 | break 265 | return result 266 | 267 | def copy_image_raw( 268 | self, image_path=None, disk=None, copy_method=None, port=None, mcu=None 269 | ): 270 | """Copy a firmware image to disk with the given copy_method. 271 | 272 | Handles exception and return code from shell copy commands. 273 | 274 | Args: 275 | image_path: Path to the firmware image to copy/flash. 276 | disk: Destination path forr the firmware image. 277 | copy_method: Copy plugin name to use. 278 | port: Serial COM port. 279 | mcu: Name of the MCU being targeted. 280 | 281 | Returns: 282 | True if copy succeeded, otherwise False. 283 | """ 284 | # image_path - Where is binary with target's firmware 285 | 286 | # Select copy_method 287 | # We override 'default' method with 'shell' method 288 | copy_method = { 289 | None: "shell", 290 | "default": "shell", 291 | }.get(copy_method, copy_method) 292 | 293 | result = ht_plugins.call_plugin( 294 | "CopyMethod", 295 | copy_method, 296 | image_path=image_path, 297 | mcu=mcu, 298 | serial=port, 299 | destination_disk=disk, 300 | target_id=self.target_id, 301 | pooling_timeout=self.polling_timeout, 302 | format=self.options.format, 303 | ) 304 | return result 305 | 306 | def hw_reset(self): 307 | """Perform hardware reset of target device. 308 | 309 | Returns: 310 | True if the reset succeeded, otherwise False. 311 | """ 312 | device_info = {} 313 | result = ht_plugins.call_plugin( 314 | "ResetMethod", 315 | "power_cycle", 316 | target_id=self.target_id, 317 | device_info=device_info, 318 | format=self.options.format, 319 | ) 320 | if result: 321 | self.port = device_info["serial_port"] 322 | self.disk = device_info["mount_point"] 323 | return result 324 | -------------------------------------------------------------------------------- /src/htrun/host_tests_toolbox/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """host_tests_toolbox package.""" 6 | 7 | from .host_functional import reset_dev 8 | from .host_functional import flash_dev 9 | from .host_functional import handle_send_break_cmd 10 | -------------------------------------------------------------------------------- /src/htrun/host_tests_toolbox/host_functional.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """API to flash and reset devices using plugin methods.""" 6 | import sys 7 | import json 8 | from time import sleep 9 | from serial import Serial, SerialException 10 | from .. import host_tests_plugins, DEFAULT_BAUD_RATE 11 | 12 | 13 | def flash_dev( 14 | disk=None, image_path=None, copy_method="default", port=None, program_cycle_s=4 15 | ): 16 | """Flash a firmware image to a device. 17 | 18 | Args: 19 | disk: Switch -d . 20 | image_path: Switch -f . 21 | copy_method: Switch -c (default: shell). 22 | port: Switch -p . 23 | program_cycle_s: Sleep time. 24 | """ 25 | if copy_method == "default": 26 | copy_method = "shell" 27 | result = False 28 | result = host_tests_plugins.call_plugin( 29 | "CopyMethod", 30 | copy_method, 31 | image_path=image_path, 32 | serial=port, 33 | destination_disk=disk, 34 | ) 35 | sleep(program_cycle_s) 36 | return result 37 | 38 | 39 | def reset_dev( 40 | port=None, 41 | disk=None, 42 | reset_type="default", 43 | reset_timeout=1, 44 | serial_port=None, 45 | baudrate=DEFAULT_BAUD_RATE, 46 | timeout=1, 47 | verbose=False, 48 | ): 49 | """Reset a device. 50 | 51 | Args: 52 | port: Switch -p . 53 | disk: Switch -d . 54 | reset_type: Switch -r . 55 | reset_timeout: Switch -R . 56 | serial_port: Serial port handler, set to None if you want this function 57 | to open serial. 58 | baudrate: Serial port baudrate. 59 | timeout: Serial port timeout. 60 | verbose: Verbose mode. 61 | """ 62 | result = False 63 | if not serial_port: 64 | try: 65 | with Serial(port, baudrate=baudrate, timeout=timeout) as serial_port: 66 | result = host_tests_plugins.call_plugin( 67 | "ResetMethod", reset_type, serial=serial_port, disk=disk 68 | ) 69 | sleep(reset_timeout) 70 | except SerialException as e: 71 | if verbose: 72 | print("%s" % (str(e))) 73 | result = False 74 | return result 75 | 76 | 77 | def handle_send_break_cmd( 78 | port, disk, reset_type=None, baudrate=None, timeout=1, verbose=False 79 | ): 80 | """Reset platform and print serial port output. 81 | 82 | Mix with switch -r RESET_TYPE and -p PORT for versatility. 83 | """ 84 | if not reset_type: 85 | reset_type = "default" 86 | 87 | port_config = port.split(":") 88 | if len(port_config) == 2: 89 | # -p COM4:115200 90 | port = port_config[0] 91 | baudrate = int(port_config[1]) if not baudrate else baudrate 92 | elif len(port_config) == 3: 93 | # -p COM4:115200:0.5 94 | port = port_config[0] 95 | baudrate = int(port_config[1]) if not baudrate else baudrate 96 | timeout = float(port_config[2]) 97 | 98 | # Use default baud rate value if not set 99 | if not baudrate: 100 | baudrate = DEFAULT_BAUD_RATE 101 | 102 | if verbose: 103 | print( 104 | "htrun: serial port configuration: %s:%s:%s" 105 | % (port, str(baudrate), str(timeout)) 106 | ) 107 | 108 | try: 109 | serial_port = Serial(port, baudrate=baudrate, timeout=timeout) 110 | except Exception as e: 111 | print("htrun: %s" % (str(e))) 112 | print( 113 | json.dumps( 114 | { 115 | "port": port, 116 | "disk": disk, 117 | "baudrate": baudrate, 118 | "timeout": timeout, 119 | "reset_type": reset_type, 120 | }, 121 | indent=4, 122 | ) 123 | ) 124 | return False 125 | 126 | serial_port.flush() 127 | # Reset using one of the plugins 128 | result = host_tests_plugins.call_plugin( 129 | "ResetMethod", reset_type, serial=serial_port, disk=disk 130 | ) 131 | if not result: 132 | print("htrun: reset plugin failed") 133 | print( 134 | json.dumps( 135 | { 136 | "port": port, 137 | "disk": disk, 138 | "baudrate": baudrate, 139 | "timeout": timeout, 140 | "reset_type": reset_type, 141 | }, 142 | indent=4, 143 | ) 144 | ) 145 | return False 146 | 147 | print("htrun: serial dump started (use ctrl+c to break)") 148 | try: 149 | while True: 150 | test_output = serial_port.read(512) 151 | if test_output: 152 | sys.stdout.write("%s" % test_output) 153 | if "{end}" in test_output: 154 | if verbose: 155 | print() 156 | print("htrun: stopped (found '{end}' terminator)") 157 | break 158 | except KeyboardInterrupt: 159 | print("ctrl+c break") 160 | 161 | serial_port.close() 162 | return True 163 | -------------------------------------------------------------------------------- /src/htrun/htrun.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2022 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Greentea Host Tests Runner.""" 6 | 7 | from multiprocessing import freeze_support 8 | from htrun import init_host_test_cli_params 9 | from htrun.host_tests_runner.host_test_default import DefaultTestSelector 10 | from htrun.host_tests_toolbox.host_functional import handle_send_break_cmd 11 | 12 | 13 | def main(): 14 | """Drive command line tool 'htrun' which is using DefaultTestSelector. 15 | 16 | 1. Create DefaultTestSelector object and pass command line parameters. 17 | 2. Call default test execution function run() to start test instrumentation. 18 | """ 19 | freeze_support() 20 | result = 0 21 | cli_params = init_host_test_cli_params() 22 | 23 | if cli_params.version: # --version 24 | import pkg_resources # part of setuptools 25 | 26 | version = pkg_resources.require("greentea-host")[0].version 27 | print(version) 28 | elif cli_params.send_break_cmd: # -b with -p PORT (and optional -r RESET_TYPE) 29 | handle_send_break_cmd( 30 | port=cli_params.port, 31 | disk=cli_params.disk, 32 | reset_type=cli_params.forced_reset_type, 33 | baudrate=cli_params.baud_rate, 34 | verbose=cli_params.verbose, 35 | ) 36 | else: 37 | test_selector = DefaultTestSelector(cli_params) 38 | try: 39 | result = test_selector.execute() 40 | # Ensure we don't return a negative value 41 | if result < 0 or result > 255: 42 | result = 1 43 | except (KeyboardInterrupt, SystemExit): 44 | test_selector.finish() 45 | result = 1 46 | raise 47 | else: 48 | test_selector.finish() 49 | 50 | return result 51 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Unit tests for greentea test suite.""" 6 | -------------------------------------------------------------------------------- /test/basic.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import unittest 7 | 8 | 9 | class BasicTestCase(unittest.TestCase): 10 | def setUp(self): 11 | pass 12 | 13 | def tearDown(self): 14 | pass 15 | 16 | def test_example(self): 17 | self.assertEqual(True, True) 18 | self.assertNotEqual(True, False) 19 | 20 | 21 | if __name__ == "__main__": 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /test/host_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | """Unit tests for htrun.""" 7 | -------------------------------------------------------------------------------- /test/host_tests/basic_ht.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import unittest 7 | 8 | from htrun import get_plugin_caps 9 | 10 | 11 | class BasicHostTestsTestCase(unittest.TestCase): 12 | def setUp(self): 13 | pass 14 | 15 | def tearDown(self): 16 | pass 17 | 18 | def test_get_plugin_caps(self): 19 | d = get_plugin_caps() 20 | self.assertIs(type(d), dict) 21 | 22 | 23 | if __name__ == "__main__": 24 | unittest.main() 25 | -------------------------------------------------------------------------------- /test/host_tests/conn_primitive_remote.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import unittest 7 | from unittest.mock import MagicMock 8 | 9 | from htrun.host_tests_conn_proxy.conn_primitive_remote import RemoteConnectorPrimitive 10 | 11 | 12 | class RemoteResourceMock(object): 13 | def __init__(self, requirements): 14 | self._is_allocated = True 15 | self._is_connected = True 16 | self.requirements = requirements 17 | self.open_connection = MagicMock() 18 | self.close_connection = MagicMock() 19 | self.write = MagicMock() 20 | self.read = MagicMock() 21 | self.read.return_value = "abc" 22 | self.disconnect = MagicMock() 23 | self.flash = MagicMock() 24 | self.reset = MagicMock() 25 | self.release = MagicMock() 26 | 27 | @property 28 | def is_connected(self): 29 | return self._is_connected 30 | 31 | @property 32 | def is_allocated(self): 33 | return self._is_allocated 34 | 35 | 36 | class RemoteModuleMock(object): 37 | class SerialParameters(object): 38 | def __init__(self, baudrate): 39 | self.baudrate = baudrate 40 | 41 | def __init__(self, host, port): 42 | self.host = host 43 | self.port = port 44 | self.is_allocated_mock = MagicMock() 45 | self.allocate = MagicMock() 46 | self.allocate.side_effect = lambda req: RemoteResourceMock(req) 47 | self.get_resources = MagicMock() 48 | self.get_resources.return_value = [1] 49 | 50 | @staticmethod 51 | def create(host, port): 52 | return RemoteModuleMock(host, port) 53 | 54 | 55 | class ConnPrimitiveRemoteTestCase(unittest.TestCase): 56 | def setUp(self): 57 | self.config = { 58 | "grm_module": "RemoteModuleMock", 59 | "tags": "a,b", 60 | "image_path": "test.bin", 61 | "platform_name": "my_platform", 62 | } 63 | self.importer = MagicMock() 64 | self.importer.side_effect = lambda x: RemoteModuleMock 65 | self.remote = RemoteConnectorPrimitive("remote", self.config, self.importer) 66 | 67 | def test_constructor(self): 68 | self.importer.assert_called_once_with("RemoteModuleMock") 69 | 70 | self.remote.client.get_resources.called_once() 71 | self.assertEqual(self.remote.remote_module, RemoteModuleMock) 72 | self.assertIsInstance(self.remote.client, RemoteModuleMock) 73 | self.assertIsInstance(self.remote.selected_resource, RemoteResourceMock) 74 | 75 | # allocate is called 76 | self.remote.client.allocate.assert_called_once_with( 77 | { 78 | "platform_name": self.config.get("platform_name"), 79 | "power_on": True, 80 | "connected": True, 81 | "tags": {"a": True, "b": True}, 82 | } 83 | ) 84 | 85 | # flash is called 86 | self.remote.selected_resource.open_connection.called_once_with("test.bin") 87 | 88 | # open_connection is called 89 | self.remote.selected_resource.open_connection.called_once() 90 | connect = self.remote.selected_resource.open_connection.call_args[1] 91 | self.assertEqual(connect["parameters"].baudrate, 9600) 92 | 93 | # reset once 94 | self.remote.selected_resource.reset.assert_called_once_with() 95 | 96 | def test_write(self): 97 | self.remote.write("abc") 98 | self.remote.selected_resource.write.assert_called_once_with("abc") 99 | 100 | def test_read(self): 101 | data = self.remote.read(6) 102 | self.remote.selected_resource.read.assert_called_once_with(6) 103 | self.assertEqual(data, "abc") 104 | 105 | def test_reset(self): 106 | self.remote.reset() 107 | self.assertEqual(self.remote.selected_resource.reset.call_count, 2) 108 | 109 | def test_finish(self): 110 | resource = self.remote.selected_resource 111 | self.remote.finish() 112 | self.assertEqual(self.remote.selected_resource, None) 113 | resource.close_connection.assert_called_once() 114 | resource.release.assert_called_once() 115 | 116 | 117 | if __name__ == "__main__": 118 | unittest.main() 119 | -------------------------------------------------------------------------------- /test/host_tests/event_callback_decorator.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import unittest 7 | 8 | from htrun.host_tests.base_host_test import BaseHostTest, event_callback 9 | 10 | 11 | class TestEvenCallbackDecorator(unittest.TestCase): 12 | def setUp(self): 13 | pass 14 | 15 | def tearDown(self): 16 | pass 17 | 18 | def test_event_callback_decorator(self): 19 | class Ht(BaseHostTest): 20 | @event_callback("Hi") 21 | def hi(self, key, value, timestamp): 22 | print("hi") 23 | 24 | @event_callback("Hello") 25 | def hello(self, key, value, timestamp): 26 | print("hello") 27 | 28 | h = Ht() 29 | h.setup() 30 | callbacks = h.get_callbacks() 31 | self.assertIn("Hi", callbacks) 32 | self.assertIn("Hello", callbacks) 33 | -------------------------------------------------------------------------------- /test/host_tests/host_registry.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Tests for the HostRegistry class.""" 6 | 7 | import unittest 8 | from htrun.host_tests_registry import HostRegistry 9 | from htrun import BaseHostTest 10 | 11 | 12 | class HostRegistryTestCase(unittest.TestCase): 13 | class HostTestClassMock(BaseHostTest): 14 | def setup(self): 15 | pass 16 | 17 | def result(self): 18 | pass 19 | 20 | def teardown(self): 21 | pass 22 | 23 | def setUp(self): 24 | self.HOSTREGISTRY = HostRegistry() 25 | 26 | def tearDown(self): 27 | pass 28 | 29 | def test_register_host_test(self): 30 | self.HOSTREGISTRY.register_host_test( 31 | "host_test_mock_auto", self.HostTestClassMock() 32 | ) 33 | self.assertEqual(True, self.HOSTREGISTRY.is_host_test("host_test_mock_auto")) 34 | 35 | def test_unregister_host_test(self): 36 | self.HOSTREGISTRY.register_host_test( 37 | "host_test_mock_2_auto", self.HostTestClassMock() 38 | ) 39 | self.assertEqual(True, self.HOSTREGISTRY.is_host_test("host_test_mock_2_auto")) 40 | self.assertNotEqual( 41 | None, self.HOSTREGISTRY.get_host_test("host_test_mock_2_auto") 42 | ) 43 | self.HOSTREGISTRY.unregister_host_test("host_test_mock_2_auto") 44 | self.assertEqual(False, self.HOSTREGISTRY.is_host_test("host_test_mock_2_auto")) 45 | 46 | def test_get_host_test(self): 47 | self.HOSTREGISTRY.register_host_test( 48 | "host_test_mock_3_auto", self.HostTestClassMock() 49 | ) 50 | self.assertEqual(True, self.HOSTREGISTRY.is_host_test("host_test_mock_3_auto")) 51 | self.assertNotEqual( 52 | None, self.HOSTREGISTRY.get_host_test("host_test_mock_3_auto") 53 | ) 54 | 55 | def test_is_host_test(self): 56 | self.assertEqual(False, self.HOSTREGISTRY.is_host_test("")) 57 | self.assertEqual(False, self.HOSTREGISTRY.is_host_test(None)) 58 | self.assertEqual(False, self.HOSTREGISTRY.is_host_test("xyz")) 59 | 60 | def test_host_test_str_not_empty(self): 61 | for ht_name in self.HOSTREGISTRY.HOST_TESTS: 62 | ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] 63 | self.assertNotEqual(None, ht) 64 | 65 | def test_host_test_has_name_attribute(self): 66 | for ht_name in self.HOSTREGISTRY.HOST_TESTS: 67 | ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] 68 | self.assertTrue(hasattr(ht, "setup")) 69 | self.assertTrue(hasattr(ht, "result")) 70 | self.assertTrue(hasattr(ht, "teardown")) 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /test/host_tests/host_test_base.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import unittest 7 | 8 | from htrun.host_tests_registry import HostRegistry 9 | 10 | 11 | class BaseHostTestTestCase(unittest.TestCase): 12 | def setUp(self): 13 | self.HOSTREGISTRY = HostRegistry() 14 | 15 | def tearDown(self): 16 | pass 17 | 18 | def test_host_test_has_setup_teardown_attribute(self): 19 | for ht_name in self.HOSTREGISTRY.HOST_TESTS: 20 | ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] 21 | self.assertTrue(hasattr(ht, "setup")) 22 | self.assertTrue(hasattr(ht, "teardown")) 23 | 24 | def test_host_test_has_no_rampUpDown_attribute(self): 25 | for ht_name in self.HOSTREGISTRY.HOST_TESTS: 26 | ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] 27 | self.assertFalse(hasattr(ht, "rampUp")) 28 | self.assertFalse(hasattr(ht, "rampDown")) 29 | 30 | 31 | if __name__ == "__main__": 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /test/host_tests/host_test_os_detect.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import unittest 7 | 8 | import os 9 | import re 10 | import sys 11 | import platform 12 | from htrun.host_tests_plugins.host_test_plugins import HostTestPluginBase 13 | 14 | 15 | class HostOSDetectionTestCase(unittest.TestCase): 16 | def setUp(self): 17 | self.plugin_base = HostTestPluginBase() 18 | self.os_names = ["Windows7", "Ubuntu", "LinuxGeneric", "Darwin"] 19 | self.re_float = re.compile("^\d+\.\d+$") 20 | 21 | def tearDown(self): 22 | pass 23 | 24 | def test_os_info(self): 25 | self.assertNotEqual(None, self.plugin_base.host_os_info()) 26 | 27 | def test_os_support(self): 28 | self.assertNotEqual(None, self.plugin_base.host_os_support()) 29 | 30 | def test_supported_os_name(self): 31 | self.assertIn(self.plugin_base.host_os_support(), self.os_names) 32 | 33 | def test_detect_os_support_ext(self): 34 | os_info = ( 35 | os.name, 36 | platform.system(), 37 | platform.release(), 38 | platform.version(), 39 | sys.platform, 40 | ) 41 | 42 | self.assertEqual(os_info, self.plugin_base.host_os_info()) 43 | 44 | 45 | if __name__ == "__main__": 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /test/host_tests/host_test_plugins.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import unittest 7 | 8 | from htrun.host_tests_plugins.module_reset_target import ( 9 | HostTestPluginResetMethod_Target, 10 | ) 11 | 12 | 13 | class HostOSDetectionTestCase(unittest.TestCase): 14 | def setUp(self): 15 | self.plugin_reset_target = HostTestPluginResetMethod_Target() 16 | 17 | def tearDown(self): 18 | pass 19 | 20 | def test_examle(self): 21 | pass 22 | 23 | def test_pyserial_version_detect(self): 24 | self.assertEqual(1.0, self.plugin_reset_target.get_pyserial_version("1.0")) 25 | self.assertEqual(1.0, self.plugin_reset_target.get_pyserial_version("1.0.0")) 26 | self.assertEqual(2.7, self.plugin_reset_target.get_pyserial_version("2.7")) 27 | self.assertEqual(2.7, self.plugin_reset_target.get_pyserial_version("2.7.1")) 28 | self.assertEqual(3.0, self.plugin_reset_target.get_pyserial_version("3.0")) 29 | self.assertEqual(3.0, self.plugin_reset_target.get_pyserial_version("3.0.1")) 30 | 31 | 32 | if __name__ == "__main__": 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /test/host_tests/host_test_scheme.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import six 7 | import unittest 8 | from htrun.host_tests_registry import HostRegistry 9 | 10 | 11 | class HostRegistryTestCase(unittest.TestCase): 12 | def setUp(self): 13 | self.HOSTREGISTRY = HostRegistry() 14 | 15 | def tearDown(self): 16 | pass 17 | 18 | def test_host_test_class_has_test_attr(self): 19 | """Check if host test has 'result' class member""" 20 | for i, ht_name in enumerate(self.HOSTREGISTRY.HOST_TESTS): 21 | ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] 22 | if ht is not None: 23 | self.assertEqual(True, hasattr(ht, "result")) 24 | 25 | def test_host_test_class_test_attr_callable(self): 26 | """Check if host test has callable 'result' class member""" 27 | for i, ht_name in enumerate(self.HOSTREGISTRY.HOST_TESTS): 28 | ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] 29 | if ht: 30 | self.assertEqual( 31 | True, hasattr(ht, "result") and callable(getattr(ht, "result")) 32 | ) 33 | 34 | def test_host_test_class_test_attr_callable_args_num(self): 35 | """Check if host test has callable setup(), result() and teardown() class member has 2 arguments""" 36 | for i, ht_name in enumerate(self.HOSTREGISTRY.HOST_TESTS): 37 | ht = self.HOSTREGISTRY.HOST_TESTS[ht_name] 38 | if ht and hasattr(ht, "setup") and callable(getattr(ht, "setup")): 39 | self.assertEqual(1, six.get_function_code(ht.setup).co_argcount) 40 | if ht and hasattr(ht, "result") and callable(getattr(ht, "result")): 41 | self.assertEqual(1, six.get_function_code(ht.result).co_argcount) 42 | if ht and hasattr(ht, "teardown") and callable(getattr(ht, "teardown")): 43 | self.assertEqual(1, six.get_function_code(ht.teardown).co_argcount) 44 | 45 | 46 | if __name__ == "__main__": 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /test/host_tests/mps2_copy.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import unittest 7 | import os 8 | 9 | from htrun.host_tests_plugins.module_copy_mps2 import HostTestPluginCopyMethod_MPS2 10 | 11 | 12 | class MPS2CopyTestCase(unittest.TestCase): 13 | def setUp(self): 14 | self.mps2_copy_plugin = HostTestPluginCopyMethod_MPS2() 15 | self.filename = "toto.bin" 16 | # Create the empty file named self.filename 17 | open(self.filename, "w+").close() 18 | 19 | def tearDown(self): 20 | os.remove(self.filename) 21 | 22 | def test_copy_bin(self): 23 | # Check that file has been copied as "mbed.bin" 24 | self.mps2_copy_plugin.mps2_copy(self.filename, ".") 25 | self.assertTrue(os.path.isfile("mbed.bin")) 26 | os.remove("mbed.bin") 27 | 28 | def test_copy_elf(self): 29 | # Check that file has been copied as "mbed.elf" 30 | os.rename(self.filename, "toto.elf") 31 | self.filename = "toto.elf" 32 | self.mps2_copy_plugin.mps2_copy(self.filename, ".") 33 | self.assertTrue(os.path.isfile("mbed.elf")) 34 | os.remove("mbed.elf") 35 | 36 | 37 | if __name__ == "__main__": 38 | unittest.main() 39 | -------------------------------------------------------------------------------- /test/host_tests/mps2_reset.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import unittest 7 | from unittest.mock import patch 8 | import os 9 | import time 10 | 11 | from htrun.host_tests_plugins.module_reset_mps2 import HostTestPluginResetMethod_MPS2 12 | 13 | 14 | class MPS2ResetTestCase(unittest.TestCase): 15 | def setUp(self): 16 | self.mps2_reset_plugin = HostTestPluginResetMethod_MPS2() 17 | 18 | def tearDown(self): 19 | pass 20 | 21 | @patch("os.name", "posix") 22 | @patch("time.sleep") 23 | @patch( 24 | "htrun.host_tests_plugins.module_reset_mps2.HostTestPluginResetMethod_MPS2.run_command" 25 | ) 26 | def test_check_sync(self, run_command_function, sleep_function): 27 | # Check that a sync call has correctly been executed 28 | self.mps2_reset_plugin.execute("reboot.txt", disk=".") 29 | args, _ = run_command_function.call_args 30 | self.assertTrue("sync" in args[0]) 31 | os.remove("reboot.txt") 32 | 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /test/host_tests/test_conn_primitive_serial.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import unittest 7 | import mock 8 | 9 | from htrun.host_tests_conn_proxy.conn_primitive_serial import SerialConnectorPrimitive 10 | from htrun.host_tests_conn_proxy.conn_primitive import ConnectorPrimitiveException 11 | 12 | 13 | @mock.patch("htrun.host_tests_conn_proxy.conn_primitive_serial.Serial") 14 | @mock.patch("htrun.host_tests_plugins.host_test_plugins.create") 15 | class ConnPrimitiveSerialTestCase(unittest.TestCase): 16 | def test_provided_serial_port_used_with_target_id(self, mock_create, mock_serial): 17 | platform_name = "irrelevant" 18 | target_id = "1234" 19 | port = "COM256" 20 | baudrate = "9600" 21 | 22 | # The mock list_mbeds() call needs to return a list of dictionaries, 23 | # and each dictionary must have a "serial_port", or else the 24 | # check_serial_port_ready() function we are testing will sleep waiting 25 | # for the serial port to become ready. 26 | mock_create().list_mbeds.return_value = [ 27 | { 28 | "target_id": target_id, 29 | "serial_port": port, 30 | "platform_name": platform_name, 31 | }, 32 | ] 33 | 34 | # Set skip_reset to avoid the use of a physical serial port. 35 | config = { 36 | "port": port, 37 | "baudrate": baudrate, 38 | "image_path": "test.bin", 39 | "platform_name": "kaysixtyfoureff", 40 | "target_id": "9900", 41 | "skip_reset": True, 42 | } 43 | connector = SerialConnectorPrimitive("SERI", port, baudrate, config=config) 44 | 45 | mock_create().list_mbeds.assert_not_called() 46 | 47 | def test_discovers_serial_port_with_target_id(self, mock_create, mock_serial): 48 | platform_name = "kaysixtyfoureff" 49 | target_id = "9900" 50 | port = "COM256" 51 | baudrate = "9600" 52 | 53 | mock_create().list_mbeds.return_value = [ 54 | { 55 | "target_id": target_id, 56 | "serial_port": port, 57 | "platform_name": platform_name, 58 | }, 59 | ] 60 | 61 | # Set skip_reset to avoid the use of a physical serial port. Don't pass 62 | # in a port, so that auto-detection based on target_id will find the 63 | # port for us (using our mock list_mbeds data). 64 | config = { 65 | "port": None, 66 | "baudrate": baudrate, 67 | "image_path": "test.bin", 68 | "platform_name": platform_name, 69 | "target_id": target_id, 70 | "skip_reset": True, 71 | } 72 | try: 73 | connector = SerialConnectorPrimitive("SERI", None, baudrate, config=config) 74 | except ConnectorPrimitiveException: 75 | # lol bad 76 | pass 77 | 78 | mock_create().list_mbeds.assert_called_once() 79 | 80 | 81 | if __name__ == "__main__": 82 | unittest.main() 83 | -------------------------------------------------------------------------------- /test/host_tests/test_target_base.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import shutil 6 | import mock 7 | import os 8 | import unittest 9 | from tempfile import mkdtemp 10 | 11 | from htrun.host_tests_runner.target_base import TargetBase 12 | 13 | 14 | class TemporaryDirectory(object): 15 | def __init__(self): 16 | self.fname = "tempdir" 17 | 18 | def __enter__(self): 19 | self.fname = mkdtemp() 20 | return self.fname 21 | 22 | def __exit__(self, *args, **kwargs): 23 | shutil.rmtree(self.fname) 24 | 25 | 26 | @mock.patch("htrun.host_tests_runner.target_base.ht_plugins") 27 | @mock.patch("htrun.host_tests_runner.target_base.create") 28 | class TestTargetBase(unittest.TestCase): 29 | def test_skips_discover_mbed_if_non_mbed_copy_method_used( 30 | self, mock_create, mock_ht_plugins 31 | ): 32 | with TemporaryDirectory() as tmpdir: 33 | image_path = os.path.join(tmpdir, "test.elf") 34 | with open(image_path, "w") as f: 35 | f.write("1234") 36 | 37 | options = mock.Mock( 38 | copy_method="pyocd", 39 | image_path=image_path, 40 | disk=None, 41 | port="port", 42 | micro="mcu", 43 | target_id="BK99", 44 | polling_timeout=5, 45 | program_cycle_s=None, 46 | json_test_configuration=None, 47 | format="blah", 48 | ) 49 | 50 | mbed = TargetBase(options) 51 | mbed.copy_image() 52 | 53 | mock_create().list_mbeds.assert_not_called() 54 | mock_ht_plugins.call_plugin.assert_called_once_with( 55 | "CopyMethod", 56 | options.copy_method, 57 | image_path=options.image_path, 58 | mcu=options.micro, 59 | serial=options.port, 60 | destination_disk=options.disk, 61 | target_id=options.target_id, 62 | pooling_timeout=options.polling_timeout, 63 | format=options.format, 64 | ) 65 | 66 | def test_discovers_mbed_if_mbed_copy_method_used( 67 | self, mock_create, mock_ht_plugins 68 | ): 69 | with TemporaryDirectory() as tmpdir: 70 | image_path = os.path.join(tmpdir, "test.elf") 71 | with open(image_path, "w") as f: 72 | f.write("1234") 73 | options = mock.Mock( 74 | copy_method="shell", 75 | image_path=image_path, 76 | disk=None, 77 | port="port", 78 | micro="mcu", 79 | target_id="BK99", 80 | polling_timeout=5, 81 | program_cycle_s=None, 82 | json_test_configuration=None, 83 | format="blah", 84 | ) 85 | 86 | mbed = TargetBase(options) 87 | mbed.copy_image() 88 | 89 | mock_create().list_mbeds.assert_called() 90 | mock_ht_plugins.call_plugin.assert_called_once_with( 91 | "CopyMethod", 92 | options.copy_method, 93 | image_path=options.image_path, 94 | mcu=options.micro, 95 | serial=options.port, 96 | destination_disk=options.disk, 97 | target_id=options.target_id, 98 | pooling_timeout=options.polling_timeout, 99 | format=options.format, 100 | ) 101 | 102 | 103 | if __name__ == "__main__": 104 | unittest.main() 105 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | mock>=2 2 | coverage 3 | coveralls 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = dev,linting,py36,py38,py39 3 | minversion = 3.3.0 4 | # Activate isolated build environment. tox will use a virtual environment 5 | # to build a source distribution from the source tree. 6 | isolated_build = True 7 | 8 | [testenv:linting] 9 | skip_install = True 10 | deps = 11 | pre-commit 12 | commands = pre-commit run --all-files 13 | 14 | [testenv] 15 | deps = 16 | -rrequirements-test.txt 17 | commands = coverage run -m unittest discover -s test -p "*.py" 18 | 19 | [testenv:dev] 20 | usedevelop = True 21 | envdir = .venv 22 | commands = 23 | deps = 24 | -rrequirements-test.txt 25 | --------------------------------------------------------------------------------