diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 333c65b..0000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -select = BLK,C,E,F,I,W -ignore = E203,W503 -max-line-length = 88 -application-import-names = ezcoo_cli -import-order-style = google \ No newline at end of file diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 0000000..9e57928 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,34 @@ +name: Build distribution +description: Builds the distribution + +runs: + using: composite + steps: + - name: Disable initramfs update + shell: bash + run: sudo sed -i 's/yes/no/g' /etc/initramfs-tools/update-initramfs.conf + + - name: Disable man-db update + shell: bash + run: sudo rm -f /var/lib/man-db/auto-update + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install uv + shell: bash + run: python3 -m pip install uv + + - name: Build distribution packages + shell: bash + run: uv build + env: + UV_PROJECT_ENVIRONMENT: .venv + + - name: Store distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ \ No newline at end of file diff --git a/.github/actions/check/action.yml b/.github/actions/check/action.yml new file mode 100644 index 0000000..c16f22e --- /dev/null +++ b/.github/actions/check/action.yml @@ -0,0 +1,42 @@ +name: Run linter and tests +description: Runs the format check, linter, type check and tests + +runs: + using: composite + steps: + - name: Disable initramfs update + shell: bash + run: sudo sed -i 's/yes/no/g' /etc/initramfs-tools/update-initramfs.conf + + - name: Disable man-db update + shell: bash + run: sudo rm -f /var/lib/man-db/auto-update + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install uv + shell: bash + run: python3 -m pip install uv + + - name: Setup project + shell: bash + run: uv sync --group dev + env: + UV_PROJECT_ENVIRONMENT: .venv + + - name: Run ruff check + uses: astral-sh/ruff-action@v3 + with: + args: "check" + + - name: Run ruff format check + uses: astral-sh/ruff-action@v3 + with: + args: "format --check" + + - name: Run tests + shell: bash + run: uv run pytest tests/ --replay -v \ No newline at end of file diff --git a/.github/workflows/check-and-build.yml b/.github/workflows/check-and-build.yml new file mode 100644 index 0000000..ba30266 --- /dev/null +++ b/.github/workflows/check-and-build.yml @@ -0,0 +1,28 @@ +name: Check and Build + +on: + workflow_call: + +jobs: + check: + name: Check + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check + uses: ./.github/actions/check + + build: + name: Build + runs-on: ubuntu-latest + needs: [check] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build + uses: ./.github/actions/build \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..26f4c86 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,9 @@ +name: Continuous Integration + +on: + pull_request: + branches: [main] + +jobs: + check-and-build: + uses: ./.github/workflows/check-and-build.yml \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6e4761..2be6a70 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ # Distribution / packaging .Python build/ +!.github/actions/build/ develop-eggs/ dist/ downloads/ @@ -127,3 +128,13 @@ dmypy.json # Pyre type checker .pyre/ + +# Devenv +.devenv* +devenv.local.nix + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/.vscode/settings.json b/.vscode/settings.json index fbccdce..045f34a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,8 @@ { - "python.analysis.typeCheckingMode": "basic", - "python.linting.flake8Enabled": true, - "python.linting.mypyEnabled": true, - "python.linting.mypyCategorySeverity.note": "Warning", - "[python]": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "ms-python.black-formatter" - }, -} \ No newline at end of file + "python.analysis.typeCheckingMode": "strict", + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff" + } +} + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b7f41fb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,136 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-10-17 + +### Added + +- **High-level KVM interface**: New `KVM` class in `kvm.py` providing type-safe, structured interface for library usage +- **Multi-device addressing**: Support for controlling multiple KVM switches on the same serial bus (addresses 0-99) +- **Device discovery**: New `discover` command to find all devices on the serial bus with their firmware versions +- **Comprehensive CLI commands** (replacing simple console.py): + - `status`: Show system status with firmware version and address + - `help`: Display device help information + - `input switch`: Switch inputs with output selection + - `output routing`: Query current input-to-output routing + - `output stream`: Check output stream status + - `edid get/set`: Manage EDID data for inputs + - `discover`: Find all devices on the serial bus + - Multiple output formats: `--format raw|json|pretty` +- **Comprehensive test suite**: Full test coverage with pytest + - Unit tests for `Device`, `KVM`, and CLI commands + - Integration tests for end-to-end workflows + - Hardware replay tests using pytest-reserial (no hardware needed for CI) + - Test coverage reporting with pytest-cov +- **Test scripts** for different testing scenarios: + - `test-record.sh`: Record serial traffic from real hardware + - `test-replay.sh`: Run tests using recorded traffic (CI-friendly) + - `test-with-hardware.sh`: Run tests with actual hardware +- **Enhanced documentation**: + - Extensive README with installation, usage examples, and library usage guide + - Development setup instructions with uv + - Testing documentation +- **Product documentation**: Added official EZCOO KVM switch manual (PDF) in `docs/` +- **CI/CD workflows**: GitHub Actions for automated testing and building on pull requests + - Composite actions for check and build steps + - Reusable workflow for check-and-build + - CI workflow triggered on PRs to main +- **Release documentation**: Complete manual release process guide in `RELEASING.md` including: + - Version bumping and changelog updates + - GitHub release creation + - PyPI publishing + - AUR package updates + +### Changed + +- **BREAKING**: License changed from Apache-2.0 to GPL-3.0-or-later +- **BREAKING**: Migrated from Poetry to uv for dependency management + - Removed `poetry.lock` and `poetry.toml` + - Added `uv.lock` and updated `pyproject.toml` to use PEP 621 format + - Changed build backend from poetry-core to hatchling +- **BREAKING**: Migrated from flake8 to ruff for linting and formatting + - Removed flake8, flake8-black, flake8-import-order + - Added ruff with comprehensive rule configuration + - Removed `.flake8` configuration file +- **BREAKING**: Complete CLI rewrite (`cli.py` replaces `console.py`) + - New command structure with subcommands and groups + - Added `--address` option for multi-device support + - Added `--format` option for output formatting (raw/json/pretty) + - Default output format changed from raw device response to human-readable pretty format + - Removed direct device command exposure + - All commands now use high-level KVM interface +- **BREAKING**: Enhanced `Device` class with improved error handling + - Added `DeviceError` and `DeviceConnectionError` exceptions + - Added command validation to prevent injection attacks + - Better error messages for connection and communication failures + - Configurable baudrate and timeout parameters + - Type hints updated to use modern Python 3.10+ syntax (`Self`, `type[]`) +- **BREAKING**: Response parsing now returns structured `KVMResponse[T]` objects + - Generic type parameter ensures type safety + - Includes raw command, raw response lines, and parsed response + - Enables both programmatic access and raw output +- **Type safety improvements**: + - Added `StreamState` enum for on/off states + - Generic `KVMResponse[T]` wrapper for all responses + - Proper type hints throughout codebase + - Dataclasses for all structured data +- **Dependencies**: + - Removed: `attrs`, `mypy`, `flake8` family + - Added: `pytest`, `pytest-cov`, `pytest-reserial`, `ruff` + - Updated: `click` to 8.1.3+, `pyserial` to 3.5+ + - Minimum Python version: 3.10 + +### Removed + +- **console.py**: Replaced by comprehensive `cli.py` with structured commands +- **Poetry configuration**: Migrated to uv +- **flake8 configuration**: Migrated to ruff +- **attrs dependency**: Replaced with standard library dataclasses + +### Fixed + +- Improved error handling in device communication with specific exception types +- Better validation of command responses with structured parsing +- More reliable serial port handling with proper connection error handling +- Command injection prevention through input validation + +### Development + +- Added `.vscode/settings.json` with Python and testing configurations +- Updated `.gitignore` with uv-specific patterns and test artifacts +- Enhanced `pyproject.toml` with: + - Ruff configuration (line length, linting rules) + - Pytest configuration (test paths, coverage options) + - Coverage configuration (source paths, exclusions) + - Dependency groups for dev dependencies + +## [0.1.1] - 2024-XX-XX + +### Changed + +- Bump dependencies + +## [0.1.0] - 2024-XX-XX + +### Fixed + +- Fix wrong baudrate + +### Changed + +- Move things around and refactoring + +## [0.0.1] - 2024-XX-XX + +### Added + +- Initial PoC implementation + +[0.2.0]: https://github.com/Luminger/ezcoo-cli/compare/0.1.1...0.2.0 +[0.1.1]: https://github.com/Luminger/ezcoo-cli/compare/0.1.0...0.1.1 +[0.1.0]: https://github.com/Luminger/ezcoo-cli/compare/0.0.1...0.1.0 +[0.0.1]: https://github.com/Luminger/ezcoo-cli/releases/tag/0.0.1 \ No newline at end of file diff --git a/LICENSE b/LICENSE index 1ccfa94..f288702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,674 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2023 Simon Brakhane - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 05410c1..c0e500c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,303 @@ # ezcoo-cli -A tool to control EZCOO KVM switches via the serial interface + +A tool to control EZCOO KVM switches via the serial interface. + +**Tested Devices:** EZCOO EZ-SW41HA-KVMU3L with firmware version 2.03 (should be equal to EZ-SW41HA-KVMU3P) + +## Installation + +### From PyPI + +Install using uv: + +```bash +uv add ezcoo-cli +``` + +### From AUR (Arch Linux) + +Install from the Arch User Repository: + +```bash +yay -S ezcoo-cli +# or +paru -S ezcoo-cli +``` + +AUR package: https://aur.archlinux.org/packages/ezcoo-cli + +### From Source + +```bash +git clone https://github.com/Luminger/ezcoo-cli +cd ezcoo-cli +uv sync +``` + +## CLI Usage + +The CLI provides commands to control your EZCOO KVM switch through a serial connection. + +### KVM Switching + +**Switch between inputs:** +```bash +# Switch to input 2 +ezcoo-cli input switch 2 + +# Switch to input 3 (output 1 is implicit) +ezcoo-cli input switch 3 --output 1 +``` + +**Check current status:** +```bash +# View system information +ezcoo-cli status + +# Check which input is currently active +ezcoo-cli output routing + +# Check stream status +ezcoo-cli output stream +``` + +**Get device information:** +```bash +# View available commands +ezcoo-cli help + +# Get raw device response (useful for debugging) +ezcoo-cli help --format raw +ezcoo-cli status --format raw +``` + +### Output Formats + +Most query commands support multiple output formats to suit different use cases. You can specify the format using the `--format` (or `-f`) flag: + +- **`pretty`** - Human-readable formatted output (default) +- **`json`** - Machine-readable JSON output for scripting and automation +- **`raw`** - Raw device response as received from the KVM + +For example, to get system status as JSON: +```bash +ezcoo-cli status --format json +# or using the short form +ezcoo-cli status -f json +``` + +> [!NOTE] +> **Breaking Change in v0.2.0:** Version 0.1.0 always printed raw output. Starting from v0.2.0, commands default to pretty-formatted output. Use `--format raw` to get the previous behavior. + +### Device Connection and Addressing + +By default, the tool connects to `/dev/ttyUSB0` at address 0 (single device mode). You can specify a different device and address: + +```bash +# Use a different serial device +ezcoo-cli -d /dev/ttyUSB1 input switch 2 + +# Communicate with device at address 5 +ezcoo-cli --address 5 status + +# Short form +ezcoo-cli -a 5 status +``` + +### Multi-Device Setup + +EZCOO devices support address-based multi-device setups where multiple KVM switches can share a single serial connection. Each device needs a unique address (0-99), with 0 being the default for single-device setups. + +**Discovering devices on the serial port:** +```bash +# Scan all addresses (0-99) +ezcoo-cli system discover + +# Scan specific range +ezcoo-cli system discover --start 0 --end 10 +``` + +**Changing device addresses:** +```bash +# Change device at address 0 to address 5 +ezcoo-cli system set-address 5 + +# Change device at address 5 to address 10 +ezcoo-cli --address 5 system set-address 10 +``` + +> [!WARNING] +> After changing a device's address, you must use the `--address` option to communicate with it at its new address. + +## Library Usage + +You can use ezcoo-cli as a library in your Python projects. There are two interfaces available: + +### High-Level KVM Interface (Recommended) + +The high-level interface provides type-safe, structured access to KVM functionality: + +```python +from pathlib import Path +from ezcoo_cli.kvm import KVM + +# Create KVM instance (default address 0) +kvm = KVM(Path("/dev/ttyUSB0")) + +# Get system information +status = kvm.get_system_status() +print(f"Firmware: {status.firmware_version}") +print(f"Address: {status.system_address}") + +# Switch inputs +kvm.switch_input(2) # Switch to input 2 + +# Get current routing +routing = kvm.get_output_routing() +print(f"Output {routing.output} -> Input {routing.input}") + +# Get stream status +stream = kvm.get_stream_status() +print(f"Stream enabled: {stream.enabled}") + +# Get help information +help_info = kvm.get_help() +print(f"Available commands: {help_info.total_commands}") + +# Working with devices at specific addresses +kvm_at_5 = KVM(Path("/dev/ttyUSB0"), address=5) +status = kvm_at_5.get_system_status() + +# Change device address +kvm.set_device_address(5) # Change from 0 to 5 +kvm.address = 5 # Update instance to use new address + +# Access raw response for any command +print(status.raw_response) # Raw device output +print(status.command) # Command that was sent +``` + +### Low-Level Device Interface + +For direct command access, use the Device class: + +```python +from pathlib import Path +from ezcoo_cli.device import Device + +# Basic usage +with Device(Path("/dev/ttyUSB0")) as device: + # Switch input 2 to output 1 + device.write("EZS OUT1 VS IN2") + + # Get help + device.write("EZH") + for line in device.readlines(): + print(line, end="") +``` + +## Development + +This project uses uv for dependency management and ruff for linting. + +```bash +# Install development dependencies +uv sync --dev + +# Run linting +uv run ruff check + +# Run formatting +uv run ruff format +``` + +## Testing + +The test suite uses pytest-reserial to record and replay serial device interactions, allowing tests to run without physical hardware. + +### Running Tests + +**Replay Mode (no hardware needed):** +```bash +./scripts/test-replay.sh +# or: uv run pytest tests/ --replay -v +``` + +**Hardware Mode (with real device):** +```bash +./scripts/test-with-hardware.sh +# or: uv run pytest tests/ -v +``` + +**Record Mode (capture new traffic):** +```bash +./scripts/test-record.sh +# or: uv run pytest tests/ --record -v +``` + +### Recorded Traffic + +pytest-reserial automatically records serial traffic in the `tests/` directory, with one recording file per test module. + +**Important:** Commit these recording files to version control so others can run tests without hardware. + +### Prerequisites for Recording + +- EZCOO device connected to `/dev/ttyUSB0` +- User has permissions to access serial device: + ```bash + sudo usermod -a -G dialout $USER + # Log out and back in for changes to take effect + ``` + +### Command Support Status + +Based on testing with EZCOO EZ-SW41HA-KVMU3L devices running firmware 2.03: + +#### Working GET Commands + +| Command | Description | Response | +|---------|-------------|----------| +| `EZSTA` | Get system status | System info with address, firmware, serial config | +| `EZH` | Get help | Complete command list | +| `EZG OUTx VS` | Get output routing | Current input routing | +| `EZG OUT1 STREAM` | Get stream status | Stream on/off status | + +#### Working SET Commands + +| Command | Description | Response | CLI Command | +|---------|-------------|----------|-------------| +| `EZS OUTx VS INy` | Switch input | No response (SET command) | `ezcoo-cli input switch ` | +| `EZS ADDR xx` | Set system address | No response (SET command) | `ezcoo-cli system set-address ` | + +#### Unsupported/Unimplemented Commands + +**Query commands that don't return data (Firmware 2.03):** + +These commands have been tested and confirmed to return no data on firmware 2.03. They are not exposed by the project as they don't work on this firmware version. + +| Command | Description | Test Result | +|---------|-------------|-------------| +| `EZG INx SIG STA` | Get input signal status | No response from device | +| `EZG INx EDID` | Get EDID information | No response from device | +| `EZG ADDR` | Get system address | No response from device | +| `EZG AUTO MODE` | Get auto switch mode status | No response from device | +| `EZG CAS` | Get cascade mode status | No response from device | +| `EZG STA` | Get system status (alternative) | No response from device (use `EZSTA` instead) | + +**SET commands with unknown/unclear effect (not implemented in this tool):** + +These SET commands have been tested and the device accepts them without errors (no response, which is normal for SET commands). However, their actual effect is unclear - either no observable changes occurred or the expected behavior was not seen. + +| Command | Description | Test Result | Reason Not Implemented | +|---------|-------------|-------------|------------------------| +| `EZS CAS EN/DIS` | Set cascade mode enable/disable | Accepted by device, no observable effect | Effect unclear | +| `EZS OUTx VIDEOy` | Set output video mode (BYPASS/4K->2K) | Accepted by device, no observable effect | Effect unclear | +| `EZS INx EDID y` | Set input EDID | Accepted by device, no observable effect | Effect unclear | +| `EZS RST` | Reset to factory defaults | Accepted by device, but address was NOT reset (still at 01 after reset) | Effect unclear - may not work or may only reset some settings | + +## License + +This project is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). + +See the [LICENSE](LICENSE) file for details. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..efb2a60 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,166 @@ +# Release Process + +This document describes the manual process for creating a new release of ezcoo-cli. + +## Prerequisites + +- Ensure you have the `gh` CLI tool installed and authenticated +- Ensure you have PyPI credentials configured (via `~/.pypirc` or environment variables) +- Ensure all tests pass locally: `./scripts/test-replay.sh` +- Ensure the working directory is clean (no uncommitted changes) + +## Release Steps + +### 1. Update Version + +Edit `pyproject.toml` and update the version number: + +```toml +[project] +version = "X.Y.Z" # Update this line +``` + +### 2. Update Changelog + +Create or update `CHANGELOG.md` with the changes in this release: + +```markdown +## [X.Y.Z] - YYYY-MM-DD + +### Added +- New features + +### Changed +- Changes to existing functionality + +### Fixed +- Bug fixes +``` + +### 3. Commit Version Bump + +```bash +git add pyproject.toml CHANGELOG.md +git commit -m "chore: bump version to X.Y.Z" +git push origin main +``` + +### 4. Create Git Tag + +```bash +git tag -a vX.Y.Z -m "Release vX.Y.Z" +git push origin vX.Y.Z +``` + +### 5. Build Distribution Packages + +```bash +uv build +``` + +This creates distribution files in the `dist/` directory: +- `ezcoo_cli-X.Y.Z-py3-none-any.whl` +- `ezcoo_cli-X.Y.Z.tar.gz` + +### 6. Create GitHub Release + +```bash +gh release create vX.Y.Z \ + --title "vX.Y.Z" \ + --notes-file CHANGELOG.md \ + dist/* +``` + +Or create the release manually via the GitHub web interface: +1. Go to https://github.com/YOUR_USERNAME/ezcoo-cli/releases/new +2. Select the tag `vX.Y.Z` +3. Set the release title to `vX.Y.Z` +4. Copy the changelog content into the description +5. Upload the files from `dist/` +6. Publish the release + +### 7. Publish to PyPI + +```bash +uv publish +``` + +### 9. Update AUR Package + +The AUR package needs to be updated after the PyPI release: + +1. Clone the AUR repository (if not already cloned): + ```bash + git clone ssh://aur@aur.archlinux.org/ezcoo-cli.git aur-ezcoo-cli + cd aur-ezcoo-cli + ``` + +2. Update the `PKGBUILD` file: + - Update `pkgver` to the new version (without the 'v' prefix) + - Update `pkgrel` to `1` (reset for new version) + - Update checksums by running: + ```bash + updpkgsums + ``` + +3. Update `.SRCINFO`: + ```bash + makepkg --printsrcinfo > .SRCINFO + ``` + +4. Test the package builds correctly: + ```bash + makepkg -si + ``` + +5. Commit and push to AUR: + ```bash + git add PKGBUILD .SRCINFO + git commit -m "Update to version X.Y.Z" + git push + ``` + + +### 8. Verify Release + +- Check the GitHub release: https://github.com/YOUR_USERNAME/ezcoo-cli/releases +- Check PyPI: https://pypi.org/project/ezcoo-cli/ +- Check AUR: https://aur.archlinux.org/packages/ezcoo-cli +- Test installation: `pip install ezcoo-cli==X.Y.Z` + +## Troubleshooting + +### PyPI Upload Fails + +If `uv publish` fails, you may need to configure PyPI credentials: + +```bash +# Using environment variables +export TWINE_USERNAME=__token__ +export TWINE_PASSWORD=pypi-... + +# Or create ~/.pypirc +[pypi] +username = __token__ +password = pypi-... +``` + +### GitHub Release Fails + +Ensure you have the `gh` CLI authenticated: + +```bash +gh auth login +``` + +### Version Already Exists + +If the version already exists on PyPI, you must bump to a new version. PyPI does not allow re-uploading the same version. + +## Post-Release + +After a successful release: + +1. Announce the release (if applicable) +2. Update any documentation that references version numbers +3. Close any related GitHub issues/milestones \ No newline at end of file diff --git a/docs/A1MFWJFYg8L.pdf b/docs/A1MFWJFYg8L.pdf new file mode 100644 index 0000000..a4b9b77 Binary files /dev/null and b/docs/A1MFWJFYg8L.pdf differ diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index f6f84a5..0000000 --- a/poetry.lock +++ /dev/null @@ -1,327 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - -[[package]] -name = "black" -version = "24.4.2" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, - {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, - {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, - {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, - {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, - {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, - {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, - {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, - {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, - {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, - {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, - {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, - {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, - {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, - {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, - {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, - {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, - {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, - {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, - {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, - {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, - {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "flake8" -version = "6.1.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, - {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.11.0,<2.12.0" -pyflakes = ">=3.1.0,<3.2.0" - -[[package]] -name = "flake8-black" -version = "0.3.6" -description = "flake8 plugin to call black as a code style validator" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8-black-0.3.6.tar.gz", hash = "sha256:0dfbca3274777792a5bcb2af887a4cad72c72d0e86c94e08e3a3de151bb41c34"}, - {file = "flake8_black-0.3.6-py3-none-any.whl", hash = "sha256:fe8ea2eca98d8a504f22040d9117347f6b367458366952862ac3586e7d4eeaca"}, -] - -[package.dependencies] -black = ">=22.1.0" -flake8 = ">=3" -tomli = {version = "*", markers = "python_version < \"3.11\""} - -[package.extras] -develop = ["build", "twine"] - -[[package]] -name = "flake8-import-order" -version = "0.18.2" -description = "Flake8 and pylama plugin that checks the ordering of import statements." -optional = false -python-versions = "*" -files = [ - {file = "flake8-import-order-0.18.2.tar.gz", hash = "sha256:e23941f892da3e0c09d711babbb0c73bc735242e9b216b726616758a920d900e"}, - {file = "flake8_import_order-0.18.2-py2.py3-none-any.whl", hash = "sha256:82ed59f1083b629b030ee9d3928d9e06b6213eb196fe745b3a7d4af2168130df"}, -] - -[package.dependencies] -pycodestyle = "*" -setuptools = "*" - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mypy" -version = "1.10.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, - {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, - {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, - {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, - {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, - {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, - {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, - {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, - {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, - {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, - {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, - {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, - {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, - {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, - {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, - {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, - {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, - {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, - {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "packaging" -version = "24.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] - -[[package]] -name = "pycodestyle" -version = "2.11.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, - {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, -] - -[[package]] -name = "pyflakes" -version = "3.1.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, - {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, -] - -[[package]] -name = "pyserial" -version = "3.5" -description = "Python Serial Port Extension" -optional = false -python-versions = "*" -files = [ - {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, - {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, -] - -[package.extras] -cp2110 = ["hidapi"] - -[[package]] -name = "setuptools" -version = "71.0.3" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-71.0.3-py3-none-any.whl", hash = "sha256:f501b6e6db709818dc76882582d9c516bf3b67b948864c5fa1d1624c09a49207"}, - {file = "setuptools-71.0.3.tar.gz", hash = "sha256:3d8531791a27056f4a38cd3e54084d8b1c4228ff9cf3f2d7dd075ec99f9fd70d"}, -] - -[package.extras] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (<7.4)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[metadata] -lock-version = "2.0" -python-versions = "^3.10" -content-hash = "8364e59737527da3f22d2813f93ca17522f9bf0594e9fa6e506af693ccffcaf4" diff --git a/poetry.toml b/poetry.toml deleted file mode 100644 index ab1033b..0000000 --- a/poetry.toml +++ /dev/null @@ -1,2 +0,0 @@ -[virtualenvs] -in-project = true diff --git a/pyproject.toml b/pyproject.toml index 20e9f21..3dcd0c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,55 @@ -[tool.poetry] +[project] name = "ezcoo-cli" -version = "0.1.0" +version = "0.2.0" description = "A tool to control EZCOO KVM switches via the serial interface" -authors = ["Simon Brakhane "] +authors = [{ name = "Simon Brakhane", email = "simon@brakhane.net" }] +requires-python = ">=3.10" readme = "README.md" -license = "Apache-2.0" +license = "GPL-3.0-or-later" +dependencies = ["pyserial>=3.5", "click>=8.1.3"] -[tool.poetry.scripts] -ezcoo-cli = "ezcoo_cli.console:main" +[project.scripts] +ezcoo-cli = "ezcoo_cli.cli:main" -[tool.poetry.dependencies] -python = "^3.10" -pyserial = "^3.5" -click = "^8.1.3" -attrs = "^23.1.0" +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry.group.dev.dependencies] -flake8 = "^6.0.0" -flake8-black = "^0.3.6" -flake8-import-order = "^0.18.2" -mypy = "^1.3.0" +[tool.ruff] +line-length = 120 -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +lint.select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C90", # mccabe complexity + "B", # flake8-bugbear (equivalent to BLK) +] + +[dependency-groups] +dev = [ + "pytest>=8.3.5", + "pytest-cov>=6.0.0", + "pytest-reserial>=0.4.2", + "ruff>=0.13.2", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = ["--cov=src/ezcoo_cli", "--cov-report=term-missing"] + +[tool.coverage.run] +source = ["src/ezcoo_cli"] +omit = ["*/tests/*", "*/__pycache__/*"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "@abstractmethod", +] diff --git a/scripts/test-record.sh b/scripts/test-record.sh new file mode 100755 index 0000000..4471c7e --- /dev/null +++ b/scripts/test-record.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Record serial traffic from real hardware for later replay +# This creates/updates .jsonl files with recorded device responses + +set -e + +DEVICE="/dev/ttyUSB0" + +# Check if device exists +if [ ! -e "$DEVICE" ]; then + echo "ERROR: Device $DEVICE does not exist" + exit 1 +fi + +# Check if device is readable and writable +if [ ! -r "$DEVICE" ] || [ ! -w "$DEVICE" ]; then + echo "ERROR: Device $DEVICE is not readable and writable by current user" + echo "Try: sudo chmod 666 $DEVICE" + exit 1 +fi + +uv run pytest tests/ --record -v "$@" \ No newline at end of file diff --git a/scripts/test-replay.sh b/scripts/test-replay.sh new file mode 100755 index 0000000..fae00fb --- /dev/null +++ b/scripts/test-replay.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Run tests using recorded serial traffic (no hardware needed) +# This uses the .jsonl files created by test-record.sh + +set -e + +# Check if any .jsonl files exist +if ! ls tests/*.jsonl 1> /dev/null 2>&1; then + echo "ERROR: No recorded traffic files found. Run ./scripts/test-record.sh first." + exit 1 +fi + +uv run pytest tests/ --replay -v "$@" \ No newline at end of file diff --git a/scripts/test-with-hardware.sh b/scripts/test-with-hardware.sh new file mode 100755 index 0000000..050c83f --- /dev/null +++ b/scripts/test-with-hardware.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Test with real hardware connected +# This runs tests against the actual EZCOO device without recording or replaying + +set -e + +DEVICE="/dev/ttyUSB0" + +# Check if device exists +if [ ! -e "$DEVICE" ]; then + echo "ERROR: Device $DEVICE does not exist" + exit 1 +fi + +# Check if device is readable and writable +if [ ! -r "$DEVICE" ] || [ ! -w "$DEVICE" ]; then + echo "ERROR: Device $DEVICE is not readable and writable by current user" + echo "Try: sudo chmod 666 $DEVICE" + exit 1 +fi + +uv run pytest tests/ -v "$@" \ No newline at end of file diff --git a/src/ezcoo_cli/__init__.py b/src/ezcoo_cli/__init__.py index 9c58050..9dd81be 100644 --- a/src/ezcoo_cli/__init__.py +++ b/src/ezcoo_cli/__init__.py @@ -1,10 +1,3 @@ -try: - from importlib.metadata import version, PackageNotFoundError # type: ignore -except ImportError: # pragma: no cover - from importlib_metadata import version, PackageNotFoundError # type: ignore +from importlib.metadata import version - -try: - __version__ = version(__name__) -except PackageNotFoundError: # pragma: no cover - __version__ = "unknown" \ No newline at end of file +__version__ = version(__name__) diff --git a/src/ezcoo_cli/cli.py b/src/ezcoo_cli/cli.py new file mode 100755 index 0000000..784a2f6 --- /dev/null +++ b/src/ezcoo_cli/cli.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python +import json +from dataclasses import asdict +from pathlib import Path + +import click + +from . import __version__ +from .kvm import KVM, KVMError +from .models import DiscoveredDevice + +device_option = click.option( + "-d", + "--device", + type=click.Path( + exists=True, + dir_okay=False, + writable=True, + readable=True, + path_type=Path, + ), + required=True, + default="/dev/ttyUSB0", +) + +address_option = click.option( + "-a", + "--address", + type=click.IntRange(0, 99), + default=0, + help="Device address (0-99). Use 0 for single device mode (default).", +) + +format_option = click.option( + "-f", + "--format", + type=click.Choice(["raw", "json", "pretty"], case_sensitive=False), + default="pretty", + help="Output format (default: pretty)", +) + + +@click.group() +def main() -> None: + """A tool to control EZCOO KVM switches via the serial interface.""" + pass + + +@main.command() +def version() -> None: + """Show the version and exit.""" + click.echo(__version__) + + +@main.command() +@device_option +@address_option +@format_option +def status(device: Path, address: int, format: str) -> None: + """Show global system status.""" + try: + kvm = KVM(device, address=address) + status_response = kvm.get_system_status() + + match format: + case "json": + click.echo(json.dumps(asdict(status_response), indent=2)) + case "pretty": + click.echo(f"System Address: {status_response.response.system_address:02d}") + click.echo(f"Firmware Version: {status_response.response.firmware_version}") + case _: # raw + click.echo("".join(status_response.raw_response), nl=False) + except KVMError as e: + click.echo(f"Error: {e}", err=True) + raise click.Abort() from e + + +@main.command() +@device_option +@address_option +@format_option +def help(device: Path, address: int, format: str) -> None: + """Get help information from the device.""" + try: + kvm = KVM(device, address=address) + help_response = kvm.get_help() + + match format: + case "json": + click.echo(json.dumps(asdict(help_response), indent=2)) + case "pretty": + click.echo("EZCOO Device Help Summary:") + click.echo("=" * 40) + + if help_response.response.firmware_version: + click.echo(f"Firmware Version: {help_response.response.firmware_version}") + + for cmd in help_response.response.commands: + click.echo(f" {cmd.command}: {cmd.description}") + + click.echo(f"\nTotal commands available: {help_response.response.total_commands}") + case _: # raw + click.echo("".join(help_response.raw_response), nl=False) + except KVMError as e: + click.echo(f"Error: {e}", err=True) + raise click.Abort() from e + + +@main.group() +def input() -> None: + """Commands for managing inputs.""" + pass + + +@input.command() +@device_option +@address_option +@click.argument("input", type=click.IntRange(1, 4), required=True) +@click.option( + "--output", + type=click.IntRange(1, 1), + default=1, + help="Output to switch (only output 1 supported)", +) +def switch(device: Path, address: int, input: int, output: int) -> None: + """Switch an input to the specified output. + + INPUT: Input number to switch (1-4) + """ + try: + kvm = KVM(device, address=address) + kvm.switch_input(input, output_num=output) + click.echo(f"Switched input {input} to output {output}") + except (KVMError, ValueError) as e: + click.echo(f"Error: {e}", err=True) + raise click.Abort() from e + + +@main.group() +def output() -> None: + """Commands for managing outputs.""" + pass + + +@output.command() +@device_option +@address_option +@click.option( + "--output", + type=click.IntRange(1, 1), + default=1, + help="Output to query (only output 1 supported)", +) +@format_option +def routing(device: Path, address: int, output: int, format: str) -> None: + """Get current output video routing.""" + try: + kvm = KVM(device, address=address) + routing_response = kvm.get_output_routing(output) + + match format: + case "json": + click.echo(json.dumps(asdict(routing_response), indent=2)) + case "pretty": + click.echo( + f"Output {routing_response.response.output} is connected to Input {routing_response.response.input}" + ) + case _: # raw + click.echo("".join(routing_response.raw_response), nl=False) + except (KVMError, ValueError) as e: + click.echo(f"Error: {e}", err=True) + raise click.Abort() from e + + +@output.command() +@device_option +@address_option +@click.option( + "--output", + type=click.IntRange(1, 1), + default=1, + help="Output to query (only output 1 supported)", +) +@format_option +def stream(device: Path, address: int, output: int, format: str) -> None: + """Get output stream status.""" + try: + kvm = KVM(device, address=address) + stream_response = kvm.get_stream_status(output) + + match format: + case "json": + click.echo(json.dumps(asdict(stream_response), indent=2)) + case "pretty": + click.echo(f"Output {stream_response.response.output} stream is {stream_response.response.status}") + case _: # raw + click.echo("".join(stream_response.raw_response), nl=False) + except (KVMError, ValueError) as e: + click.echo(f"Error: {e}", err=True) + raise click.Abort() from e + + +@main.group() +def system() -> None: + """System management commands.""" + pass + + +@system.command() +@device_option +@address_option +@click.argument("new_address", type=click.IntRange(0, 99), required=True) +@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt") +def set_address(device: Path, address: int, new_address: int, yes: bool) -> None: + """Set the device address. + + NEW_ADDRESS: New address to set (0-99) + + Warning: After changing the address, you must use --address option + to communicate with the device at its new address. + """ + try: + # Confirm the change unless --yes is used + if not yes: + click.echo(f"This will change the device address from {address} to {new_address}") + click.echo("After this change, you must use --address {new_address} to communicate with the device") + if not click.confirm("Do you want to continue?"): + click.echo("Address change cancelled") + return + + kvm = KVM(device, address=address) + kvm.set_device_address(new_address) + click.echo(f"Device address changed from {address} to {new_address}") + click.echo(f"\nTo communicate with the device now, use: --address {new_address}") + except (KVMError, ValueError) as e: + click.echo(f"Error: {e}", err=True) + raise click.Abort() from e + + +@system.command() +@device_option +@click.option("--start", type=click.IntRange(0, 99), default=0, help="Start address (default: 0)") +@click.option("--end", type=click.IntRange(0, 99), default=99, help="End address (default: 99)") +@format_option +def discover(device: Path, start: int, end: int, format: str) -> None: + """Discover devices by scanning address range. + + This command scans the specified address range to find all responding devices. + """ + if start > end: + click.echo("Error: Start address must be less than or equal to end address", err=True) + raise click.Abort() + + try: + if format != "json": + click.echo(f"Scanning addresses {start} to {end}...", err=True) + + found_devices: list[DiscoveredDevice] = [] + + for addr in range(start, end + 1): + try: + kvm = KVM(device, address=addr) + status = kvm.get_system_status() + found_devices.append( + DiscoveredDevice( + address=addr, + firmware=status.response.firmware_version, + system_address=status.response.system_address, + ) + ) + if format != "json": + click.echo(f"Found device at address {addr} (firmware: {status.response.firmware_version})") + except (KVMError, Exception): + # No device at this address + continue + + match format: + case "json": + click.echo(json.dumps([asdict(d) for d in found_devices], indent=2)) + case _: + if not found_devices: + click.echo("\nNo devices found in the specified range") + else: + click.echo(f"\nTotal devices found: {len(found_devices)}") + + except Exception as e: + click.echo(f"Error during discovery: {e}", err=True) + raise click.Abort() from e diff --git a/src/ezcoo_cli/console.py b/src/ezcoo_cli/console.py deleted file mode 100755 index 71ce601..0000000 --- a/src/ezcoo_cli/console.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -from pathlib import Path - -import click - -from . import __version__ -from .device import Device - -device_option = click.option( - "-d", - "--device", - type=click.Path( - exists=True, dir_okay=False, writable=True, readable=True, path_type=Path - ), - required=True, - default="/dev/ttyUSB0", -) - - -@click.group() -@click.version_option(version=__version__) -def main() -> None: - pass - - -@main.group() -def input() -> None: - pass - - -@input.command() -@device_option -@click.argument("input", type=click.IntRange(1, 8), required=True) -@click.option("--output", type=click.IntRange(1, 2), default=1, help="Output to switch") -def switch(device: Path, input: int, output: int) -> None: - with Device(device) as client: - client.write(f"EZS OUT{output} VS IN{input}") - - -@input.command() -@device_option -def edid(device: Path) -> None: - with Device(device) as client: - client.write("EZG IN0 EDID") - for line in client.readlines(): - print(line, end="") - - -@main.command() -@device_option -def help(device: Path) -> None: - with Device(device) as client: - client.write("EZH") - for line in client.readlines(): - print(line, end="") diff --git a/src/ezcoo_cli/device.py b/src/ezcoo_cli/device.py index 3154709..cd1857a 100644 --- a/src/ezcoo_cli/device.py +++ b/src/ezcoo_cli/device.py @@ -1,38 +1,124 @@ import contextlib from pathlib import Path from types import TracebackType -from typing import Generator, Type +from typing import Generator, Self import serial -class Device(contextlib.AbstractContextManager): - def __init__(self, path: Path) -> None: +class DeviceError(Exception): + """Base exception for device-related errors.""" + + pass + + +class DeviceConnectionError(DeviceError): + """Raised when device connection fails.""" + + pass + + +class Device(contextlib.AbstractContextManager["Device"]): + """A context manager for communicating with EZCOO KVM switches via serial interface. + + This class can be used both as a CLI tool and as a library component. + + Example: + >>> from ezcoo_cli.device import Device + >>> from pathlib import Path + >>> + >>> with Device(Path("/dev/ttyUSB0")) as device: + ... device.write("EZS OUT1 VS IN2") + """ + + def __init__(self, path: Path, baudrate: int = 115200, timeout: float = 1.0) -> None: + """Initialize the Device. + + Args: + path: Path to the serial device (e.g., /dev/ttyUSB0) + baudrate: Serial communication baud rate (default: 115200) + timeout: Read timeout in seconds (default: 1) + """ + self._path = path self._serial = serial.Serial() self._serial.port = str(path) - self._serial.baudrate = 115200 - self._serial.timeout = 1 + self._serial.baudrate = baudrate + self._serial.timeout = timeout - def __enter__(self) -> "Device": - self._serial.open() + def __enter__(self) -> Self: + try: + self._serial.open() + except serial.SerialException as e: + raise DeviceConnectionError(f"Failed to open device {self._path}: {e}") from e return self def __exit__( self, - exc_type: Type[BaseException] | None, + exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: - self._serial.close() + if self._serial.is_open: + self._serial.close() + + @staticmethod + def validate_command(cmd: str) -> None: + """Validate that a command only contains safe characters. + + Args: + cmd: Command string to validate + + Raises: + DeviceError: If command contains invalid characters + """ + # Only allow ASCII alphanumeric characters and regular spaces (not tabs, newlines, etc.) + if not all(c.isalnum() or c == " " for c in cmd): + raise DeviceError( + f"Command contains invalid characters. Only ASCII alphanumeric and spaces allowed: {cmd!r}" + ) def write(self, cmd: str) -> None: - buffer = (cmd + "\n").encode("ascii") - self._serial.write(buffer) + """Write a command to the device. + + Args: + cmd: Command string to send to the device + + Raises: + DeviceError: If writing to device fails or command contains invalid characters + """ + if not self._serial.is_open: + raise DeviceError("Device is not open") + + # Validate command before sending + self.validate_command(cmd) + + try: + buffer = (cmd + "\n").encode("ascii") + self._serial.write(buffer) + except UnicodeEncodeError as e: + raise DeviceError(f"Command contains non-ASCII characters: {e}") from e + except serial.SerialException as e: + raise DeviceError(f"Failed to write to device: {e}") from e def readlines(self) -> Generator[str, None, None]: - while True: - read = self._serial.read_until() - if not read: - break + """Read lines from the device until no more data is available. + + Yields: + str: Each line received from the device - yield read.decode("ascii") + Raises: + DeviceError: If reading from device fails + """ + if not self._serial.is_open: + raise DeviceError("Device is not open") + + while True: + try: + read = self._serial.read_until() + if not read: + break + yield read.decode("ascii") + except serial.SerialException as e: + raise DeviceError(f"Failed to read from device: {e}") from e + except UnicodeDecodeError as e: + raise DeviceError(f"Failed to decode device response: {e}") from e diff --git a/src/ezcoo_cli/kvm.py b/src/ezcoo_cli/kvm.py new file mode 100644 index 0000000..21c1a93 --- /dev/null +++ b/src/ezcoo_cli/kvm.py @@ -0,0 +1,364 @@ +"""High-level KVM switch interface.""" + +import re +from collections.abc import Callable +from pathlib import Path +from typing import TypeVar, overload + +from .device import Device +from .models import Command, HelpInfo, KVMResponse, OutputRouting, StreamState, StreamStatus, SystemStatus + +T = TypeVar("T") + + +class KVMError(Exception): + """Base exception for KVM-related errors.""" + + pass + + +class KVM: + """High-level interface for EZCOO KVM switches. + + This class provides a type-safe, structured interface to KVM functionality + that can be used both by the CLI and as a library. + + Example: + >>> from pathlib import Path + >>> from ezcoo_cli.kvm import KVM + >>> + >>> kvm = KVM(Path("/dev/ttyUSB0")) + >>> status = kvm.get_system_status() + >>> print(f"Firmware: {status.firmware_version}") + >>> kvm.switch_input(2) + >>> + >>> # For device at address 5 + >>> kvm = KVM(Path("/dev/ttyUSB0"), address=5) + >>> status = kvm.get_system_status() # Sends A05EZSTA + """ + + def __init__( + self, + device_path: Path, + baudrate: int = 115200, + timeout: float = 1.0, + address: int = 0, + ): + """Initialize the KVM interface. + + Args: + device_path: Path to the serial device (e.g., /dev/ttyUSB0) + baudrate: Serial communication baud rate (default: 115200) + timeout: Read timeout in seconds (default: 1.0) + address: Device address (0-99). Use 0 for single device (default). + For addresses 1-99, commands will be prefixed with Axx. + """ + self.device_path = device_path + self.baudrate = baudrate + self.timeout = timeout + self._address = address + + if not 0 <= address <= 99: + raise ValueError("Address must be between 0 and 99") + + @property + def address(self) -> int: + """Get the current address this KVM instance is configured to use.""" + return self._address + + @address.setter + def address(self, value: int) -> None: + """Set the address this KVM instance should use for communication. + + Args: + value: Address to use (0-99) + + Raises: + ValueError: If address is invalid + + Note: + This only changes which address this KVM instance uses for commands. + It does NOT change the device's actual address. Use set_device_address() + to change the device's address. + """ + if not 0 <= value <= 99: + raise ValueError("Address must be between 0 and 99") + + self._address = value + + def _build_command(self, command: str) -> str: + """Build a command with address prefix. + + Args: + command: The base command (e.g., "EZSTA", "EZH") + + Returns: + Command with address prefix if needed + """ + if self._address == 0: + return command + return f"A{self._address:02d}{command}" + + @overload + def _execute_command( + self, + command: str, + parser: Callable[[list[str]], T], + ) -> KVMResponse[T]: ... + + @overload + def _execute_command( + self, + command: str, + parser: None, + ) -> None: ... + + def _execute_command( + self, + command: str, + parser: Callable[[list[str]], T] | None = None, + ) -> KVMResponse[T] | None: + """Execute a command on the device and optionally parse the response. + + Args: + command: The full command to execute (with address prefix already applied) + parser: Optional function to parse the response lines into a typed result. + If None, no response is expected and None is returned. + + Returns: + KVMResponse[T] containing the parsed result if parser is provided, None otherwise + + Raises: + KVMError: If parser is provided but no response received, or if parsing fails + """ + with Device(self.device_path, self.baudrate, self.timeout) as device: + device.write(command) + if parser is None: + return None + lines = list(device.readlines()) + + if not lines: + raise KVMError("No response from device") + + parsed_response = parser(lines) + return KVMResponse(command=command, raw_response=lines, response=parsed_response) + + def _parse_status_output(self, lines: list[str]) -> SystemStatus: + """Parse EZSTA command output into SystemStatus. + + Expected format: + - "System Address = XX F/W Version : X.XX" + """ + # Pattern matches: System Address = F/W Version : + status_pattern = r"System\s+Address\s*=\s*(?P
\d+)\s+F/W\s+Version\s*:\s*(?P[\d.]+)" + + for line in lines: + line = line.strip() + + # Try to match system address and firmware version + match = re.search(status_pattern, line, re.IGNORECASE) + if not match: + continue + + system_address = int(match.group("address")) + firmware_version = match.group("version") + + return SystemStatus( + system_address=system_address, + firmware_version=firmware_version, + ) + + raise KVMError("Failed to parse system status") + + def _parse_help_output(self, lines: list[str]) -> HelpInfo: + """Parse EZH command output into HelpInfo. + + Expected format: + - "F/W Version : X.XX" + - "= COMMAND : Description" + """ + commands: list[Command] = [] + firmware_version: str | None = None + + # Pattern matches: F/W Version : + version_pattern = r"F/W\s+Version\s*:\s*(?P[\d.]+)" + # Pattern matches: = COMMAND_NAME : Description + # Captures everything from EZ to the colon (trimmed) + command_pattern = r"^=\s+(?PEZ[^:]+?)\s*:\s*(?P.+)$" + + for line in lines: + line = line.strip() + + # Try to match firmware version + version_match = re.search(version_pattern, line, re.IGNORECASE) + if version_match: + firmware_version = version_match.group("version") + + # Try to match command entries + cmd_match = re.match(command_pattern, line) + if cmd_match: + cmd_name = cmd_match.group("command") + cmd_desc = cmd_match.group("description").strip() + commands.append(Command(command=cmd_name, description=cmd_desc)) + + return HelpInfo( + firmware_version=firmware_version, + commands=commands, + total_commands=len(commands), + ) + + def _parse_routing_output(self, lines: list[str]) -> OutputRouting: + """Parse EZG OUTx VS command output into OutputRouting. + + Expected format: "OUTx VS y" where x is output number and y is input number. + """ + # Pattern matches: OUT VS + pattern = r"OUT(?P\d+)\s+VS\s+(?P\d+)" + + for line in lines: + match = re.search(pattern, line.strip()) + if not match: + continue + + output_num = int(match.group("output")) + input_num = int(match.group("input")) + + return OutputRouting( + output=output_num, + input=input_num, + ) + + raise KVMError("Failed to parse routing output") + + def _parse_stream_output(self, lines: list[str]) -> StreamStatus: + """Parse EZG OUTx STREAM command output into StreamStatus. + + Expected format: "OUT STREAM " where status is ON or OFF. + """ + # Pattern matches: OUT STREAM + pattern = r"OUT\s+(?P\d+)\s+STREAM\s+(?PON|OFF)" + + for line in lines: + match = re.search(pattern, line.strip(), re.IGNORECASE) + if not match: + continue + + output_num = int(match.group("output")) + status_upper = match.group("status").upper() + enabled = status_upper == "ON" + status = StreamState.ON if enabled else StreamState.OFF + + return StreamStatus( + output=output_num, + status=status, + enabled=enabled, + ) + + raise KVMError("Failed to parse stream output") + + def get_system_status(self) -> KVMResponse[SystemStatus]: + """Get system status information. + + Returns: + KVMResponse containing SystemStatus with device information + + Raises: + KVMError: If the command fails or response cannot be parsed + """ + command = self._build_command("EZSTA") + return self._execute_command(command, self._parse_status_output) + + def get_help(self) -> KVMResponse[HelpInfo]: + """Get device help information. + + Returns: + KVMResponse containing HelpInfo with available commands + + Raises: + KVMError: If the command fails or response cannot be parsed + """ + command = self._build_command("EZH") + return self._execute_command(command, self._parse_help_output) + + def switch_input(self, input_num: int, output_num: int = 1) -> None: + """Switch an input to the specified output. + + Args: + input_num: Input number to switch (1-4) + output_num: Output number (default: 1, only 1 supported) + + Raises: + KVMError: If the command fails + ValueError: If input/output numbers are invalid + """ + if not 1 <= input_num <= 4: + raise ValueError("Input number must be between 1 and 4") + + if output_num != 1: + raise ValueError("Only output 1 is supported") + + command = self._build_command(f"EZS OUT{output_num} VS IN{input_num}") + self._execute_command(command, parser=None) + + def get_output_routing(self, output_num: int = 1) -> KVMResponse[OutputRouting]: + """Get current output routing. + + Args: + output_num: Output number to query (default: 1, only 1 supported) + + Returns: + KVMResponse containing OutputRouting with current connection + + Raises: + KVMError: If the command fails or response cannot be parsed + ValueError: If output number is invalid + """ + if output_num != 1: + raise ValueError("Only output 1 is supported") + + command = self._build_command(f"EZG OUT{output_num} VS") + return self._execute_command(command, self._parse_routing_output) + + def get_stream_status(self, output_num: int = 1) -> KVMResponse[StreamStatus]: + """Get output stream status. + + Args: + output_num: Output number to query (default: 1, only 1 supported) + + Returns: + KVMResponse containing StreamStatus with current stream state + + Raises: + KVMError: If the command fails or response cannot be parsed + ValueError: If output number is invalid + """ + if output_num != 1: + raise ValueError("Only output 1 is supported") + + command = self._build_command(f"EZG OUT{output_num} STREAM") + return self._execute_command(command, self._parse_stream_output) + + def set_device_address(self, new_address: int) -> None: + """Set the device's address. + + Args: + new_address: New address to set (0-99) + + Raises: + ValueError: If address is invalid + KVMError: If the command fails + + Warning: + After changing the device's address, you must update this KVM instance's + address property to continue communicating with the device. + + Example: + >>> kvm = KVM(Path("/dev/ttyUSB0"), address=0) + >>> kvm.set_device_address(5) + >>> kvm.address = 5 # Update instance to use new address + """ + if not 0 <= new_address <= 99: + raise ValueError("Address must be between 0 and 99") + + command = self._build_command(f"EZS ADDR {new_address:02d}") + self._execute_command(command, parser=None) diff --git a/src/ezcoo_cli/models.py b/src/ezcoo_cli/models.py new file mode 100644 index 0000000..d648fdc --- /dev/null +++ b/src/ezcoo_cli/models.py @@ -0,0 +1,77 @@ +"""Data models for EZCOO KVM switch responses.""" + +from dataclasses import dataclass +from enum import Enum +from typing import Generic, TypeVar + +T = TypeVar("T") + + +class StreamState(str, Enum): + """Stream status state.""" + + ON = "on" + OFF = "off" + + +@dataclass +class KVMResponse(Generic[T]): + """Generic response wrapper for KVM commands. + + Contains the raw command, response lines, and parsed response. + """ + + command: str + raw_response: list[str] + response: T + + +@dataclass +class SystemStatus: + """System status information.""" + + system_address: int + firmware_version: str + + +@dataclass +class Command: + """Device command information.""" + + command: str + description: str + + +@dataclass +class HelpInfo: + """Device help information.""" + + firmware_version: str | None + commands: list[Command] + total_commands: int + + +@dataclass +class OutputRouting: + """Output routing information.""" + + output: int + input: int + + +@dataclass +class StreamStatus: + """Output stream status.""" + + output: int + status: StreamState + enabled: bool + + +@dataclass +class DiscoveredDevice: + """Information about a discovered device.""" + + address: int + firmware: str + system_address: int diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2c598c1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,23 @@ +"""Shared pytest fixtures and configuration for EZCOO CLI tests.""" + +from pathlib import Path + +import pytest + + +@pytest.fixture +def mock_device_path() -> Path: + """Mock device path for testing.""" + return Path("/dev/ttyUSB0") + + +@pytest.fixture +def test_baudrate() -> int: + """Standard baudrate for testing.""" + return 115200 + + +@pytest.fixture +def test_timeout() -> float: + """Standard timeout for testing.""" + return 1.0 diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..1071747 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,458 @@ +"""Tests for the CLI interface.""" + +import json +import tempfile +from collections.abc import Generator +from pathlib import Path +from unittest.mock import MagicMock, Mock, patch + +import pytest +from click.testing import CliRunner + +from ezcoo_cli.cli import main +from ezcoo_cli.models import Command, HelpInfo, KVMResponse, OutputRouting, StreamState, StreamStatus, SystemStatus + + +@pytest.fixture +def mock_device_file() -> Generator[str, None, None]: + """Create a temporary file to satisfy Click's exists=True validation.""" + with tempfile.NamedTemporaryFile(delete=False) as tmp: + device_path = tmp.name + yield device_path + Path(device_path).unlink(missing_ok=True) + + +# Basic CLI tests without device interaction + + +def test_main_help(): + """Test main command help.""" + runner = CliRunner() + result = runner.invoke(main, ["--help"]) + assert result.exit_code == 0 + assert "EZCOO KVM" in result.output or "control EZCOO" in result.output + + +def test_status_help(): + """Test status command help.""" + runner = CliRunner() + result = runner.invoke(main, ["status", "--help"]) + assert result.exit_code == 0 + assert "system status" in result.output + + +def test_help_command_help(): + """Test help command help.""" + runner = CliRunner() + result = runner.invoke(main, ["help", "--help"]) + assert result.exit_code == 0 + assert "help information" in result.output + + +def test_input_group_help(): + """Test input group help.""" + runner = CliRunner() + result = runner.invoke(main, ["input", "--help"]) + assert result.exit_code == 0 + assert "managing inputs" in result.output + + +def test_output_group_help(): + """Test output group help.""" + runner = CliRunner() + result = runner.invoke(main, ["output", "--help"]) + assert result.exit_code == 0 + assert "managing outputs" in result.output + + +def test_system_group_help(): + """Test system group help.""" + runner = CliRunner() + result = runner.invoke(main, ["system", "--help"]) + assert result.exit_code == 0 + assert "System management" in result.output + + +def test_missing_device_parameter(): + """Test commands fail without device parameter.""" + runner = CliRunner() + + # Test status command without device + result = runner.invoke(main, ["status"]) + # The command may succeed if default device exists, or fail if it doesn't + assert isinstance(result.exit_code, int) + + +def test_version_command(): + """Test version command.""" + runner = CliRunner() + result = runner.invoke(main, ["version"]) + assert result.exit_code == 0 + assert len(result.output.strip()) > 0 + + +# CLI integration tests (mock KVM, no device needed) + + +@patch("ezcoo_cli.cli.KVM") +def test_status_command_json(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test status command with JSON output.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + mock_status = KVMResponse( + command="EZSTA", + raw_response=["System Address : 00 F/W Version : 2.03\n"], + response=SystemStatus( + system_address=0, + firmware_version="2.03", + ), + ) + mock_kvm.get_system_status.return_value = mock_status + + runner = CliRunner() + result = runner.invoke(main, ["status", "--device", mock_device_file, "--format", "json"]) + + assert result.exit_code == 0 + data = json.loads(result.output) + assert data["response"]["system_address"] == 0 + assert data["response"]["firmware_version"] == "2.03" + + +@patch("ezcoo_cli.cli.KVM") +def test_help_command_json(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test help command with JSON output.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + mock_help = KVMResponse( + command="EZH", + raw_response=["F/W Version : 2.03\n", "= EZH : Help\n", "= EZSTA : Show Global System Status\n"], + response=HelpInfo( + firmware_version="2.03", + commands=[ + Command(command="EZH", description="Help"), + Command(command="EZSTA", description="Show Global System Status"), + ], + total_commands=2, + ), + ) + mock_kvm.get_help.return_value = mock_help + + runner = CliRunner() + result = runner.invoke(main, ["help", "--device", mock_device_file, "--format", "json"]) + + assert result.exit_code == 0 + data = json.loads(result.output) + assert data["response"]["total_commands"] == 2 + assert len(data["response"]["commands"]) == 2 + + +@patch("ezcoo_cli.cli.KVM") +def test_input_switch_command_valid_inputs(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test input switch command with valid input numbers.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + runner = CliRunner() + + for input_num in range(1, 5): + result = runner.invoke(main, ["input", "switch", "--device", mock_device_file, str(input_num)]) + assert result.exit_code == 0 + mock_kvm.switch_input.assert_called_with(input_num, output_num=1) + + +def test_input_switch_command_invalid_input(): + """Test input switch command with invalid input numbers.""" + runner = CliRunner() + + # Test input 0 + result = runner.invoke(main, ["input", "switch", "--device", "/dev/ttyUSB0", "0"]) + assert result.exit_code != 0 + + # Test input 5 + result = runner.invoke(main, ["input", "switch", "--device", "/dev/ttyUSB0", "5"]) + assert result.exit_code != 0 + + +@patch("ezcoo_cli.cli.KVM") +def test_output_routing_command_json(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test output routing command with JSON output.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + mock_routing = KVMResponse( + command="EZG OUT1 VS", + raw_response=["OUT1 VS IN2\n"], + response=OutputRouting( + output=1, + input=2, + ), + ) + mock_kvm.get_output_routing.return_value = mock_routing + + runner = CliRunner() + result = runner.invoke(main, ["output", "routing", "--device", mock_device_file, "--format", "json"]) + + assert result.exit_code == 0 + data = json.loads(result.output) + assert data["response"]["output"] == 1 + assert data["response"]["input"] == 2 + + +@patch("ezcoo_cli.cli.KVM") +def test_output_stream_command_json(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test output stream command with JSON output.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + mock_stream = KVMResponse( + command="EZG OUT1 STREAM", + raw_response=["OUT1 STREAM on\n"], + response=StreamStatus( + output=1, + status=StreamState.ON, + enabled=True, + ), + ) + mock_kvm.get_stream_status.return_value = mock_stream + + runner = CliRunner() + result = runner.invoke(main, ["output", "stream", "--device", mock_device_file, "--format", "json"]) + + assert result.exit_code == 0 + data = json.loads(result.output) + assert data["response"]["output"] == 1 + assert data["response"]["status"] == StreamState.ON.value + assert data["response"]["enabled"] is True + + +@patch("ezcoo_cli.cli.KVM") +def test_output_format_consistency(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test that output formats are consistent across commands.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + mock_kvm.get_system_status.return_value = KVMResponse( + command="EZSTA", + raw_response=["System Address : 00 F/W Version : 2.03\n"], + response=SystemStatus( + system_address=0, + firmware_version="2.03", + ), + ) + mock_kvm.get_help.return_value = KVMResponse( + command="EZH", + raw_response=["F/W Version : 2.03\n", "= EZH : Help\n"], + response=HelpInfo( + firmware_version="2.03", + commands=[Command(command="EZH", description="Help")], + total_commands=1, + ), + ) + mock_kvm.get_output_routing.return_value = KVMResponse( + command="EZG OUT1 VS", + raw_response=["OUT1 VS IN2\n"], + response=OutputRouting( + output=1, + input=2, + ), + ) + mock_kvm.get_stream_status.return_value = KVMResponse( + command="EZG OUT1 STREAM", + raw_response=["OUT1 STREAM on\n"], + response=StreamStatus( + output=1, + status=StreamState.ON, + enabled=True, + ), + ) + + runner = CliRunner() + device_args = ["--device", mock_device_file] + + commands_to_test = [["status"], ["help"], ["output", "routing"], ["output", "stream"]] + + for command_parts in commands_to_test: + # Test JSON format + result = runner.invoke(main, command_parts + device_args + ["--format", "json"]) + assert result.exit_code == 0 + json.loads(result.output) # Should not raise + + # Test pretty format + result = runner.invoke(main, command_parts + device_args + ["--format", "pretty"]) + assert result.exit_code == 0 + assert len(result.output.strip()) > 0 + + +@patch("ezcoo_cli.cli.KVM") +def test_input_switch_with_output_option(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test input switch command with output option.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + runner = CliRunner() + + # Test with default output (should work) + result = runner.invoke(main, ["input", "switch", "--device", mock_device_file, "1"]) + assert result.exit_code == 0 + mock_kvm.switch_input.assert_called_with(1, output_num=1) + + # Test with explicit output 1 (should work) + result = runner.invoke(main, ["input", "switch", "--device", mock_device_file, "--output", "1", "1"]) + assert result.exit_code == 0 + mock_kvm.switch_input.assert_called_with(1, output_num=1) + + +# System group command tests + + +@patch("ezcoo_cli.cli.KVM") +def test_system_set_address_with_yes_flag(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test system set-address command with --yes flag.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + runner = CliRunner() + result = runner.invoke(main, ["system", "set-address", "--device", mock_device_file, "--yes", "5"]) + + assert result.exit_code == 0 + mock_kvm.set_device_address.assert_called_once_with(5) + assert "Device address changed" in result.output + assert "from 0 to 5" in result.output + + +@patch("ezcoo_cli.cli.KVM") +def test_system_set_address_with_confirmation(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test system set-address command with user confirmation.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + runner = CliRunner() + + # Test with confirmation (yes) + result = runner.invoke(main, ["system", "set-address", "--device", mock_device_file, "10"], input="y\n") + assert result.exit_code == 0 + mock_kvm.set_device_address.assert_called_once_with(10) + assert "Device address changed" in result.output + + +@patch("ezcoo_cli.cli.KVM") +def test_system_set_address_cancelled(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test system set-address command cancelled by user.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + runner = CliRunner() + + # Test with cancellation (no) + result = runner.invoke(main, ["system", "set-address", "--device", mock_device_file, "10"], input="n\n") + assert result.exit_code == 0 + mock_kvm.set_device_address.assert_not_called() + assert "cancelled" in result.output + + +@patch("ezcoo_cli.cli.KVM") +def test_system_discover_command(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test system discover command.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + # Mock successful responses for addresses 0 and 5 + def mock_get_system_status_side_effect(): + # First call (address 0) + status_0 = KVMResponse( + command="EZSTA", + raw_response=["System Address : 00 F/W Version : 2.03\n"], + response=SystemStatus( + system_address=0, + firmware_version="2.03", + ), + ) + # Second call (address 5) + status_5 = KVMResponse( + command="A05EZSTA", + raw_response=["System Address : 05 F/W Version : 2.03\n"], + response=SystemStatus( + system_address=5, + firmware_version="2.03", + ), + ) + # Return different status based on which address was used + for status in [status_0, status_5]: + yield status + + mock_kvm.get_system_status.side_effect = mock_get_system_status_side_effect() + + runner = CliRunner() + result = runner.invoke(main, ["system", "discover", "--device", mock_device_file, "--start", "0", "--end", "5"]) + + assert result.exit_code == 0 + assert "Scanning addresses 0 to 5" in result.output + assert "Total devices found: 2" in result.output + + +@patch("ezcoo_cli.cli.KVM") +def test_system_discover_json_output(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test system discover command with JSON output.""" + # Setup mock + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + + mock_status = KVMResponse( + command="EZSTA", + raw_response=["System Address : 00 F/W Version : 2.03\n"], + response=SystemStatus( + system_address=0, + firmware_version="2.03", + ), + ) + mock_kvm.get_system_status.return_value = mock_status + + runner = CliRunner() + result = runner.invoke( + main, ["system", "discover", "--device", mock_device_file, "--start", "0", "--end", "0", "--format", "json"] + ) + + assert result.exit_code == 0 + data: list[dict[str, object]] = json.loads(result.output) + assert isinstance(data, list) + assert len(data) == 1 + assert data[0]["address"] == 0 + assert data[0]["firmware"] == "2.03" + assert data[0]["system_address"] == 0 + + +@patch("ezcoo_cli.cli.KVM") +def test_system_discover_no_devices(mock_kvm_class: MagicMock, mock_device_file: str) -> None: + """Test system discover command when no devices are found.""" + # Setup mock to always raise exception (no devices) + mock_kvm = Mock() + mock_kvm_class.return_value = mock_kvm + mock_kvm.get_system_status.side_effect = Exception("No device") + + runner = CliRunner() + result = runner.invoke(main, ["system", "discover", "--device", mock_device_file, "--start", "0", "--end", "2"]) + + assert result.exit_code == 0 + assert "No devices found" in result.output + + +def test_system_discover_invalid_range(mock_device_file: str) -> None: + """Test system discover command with invalid address range.""" + runner = CliRunner() + result = runner.invoke(main, ["system", "discover", "--device", mock_device_file, "--start", "10", "--end", "5"]) + + assert result.exit_code == 1 + assert "Start address must be less than or equal to end address" in result.output diff --git a/tests/test_device.jsonl b/tests/test_device.jsonl new file mode 100644 index 0000000..973aff7 --- /dev/null +++ b/tests/test_device.jsonl @@ -0,0 +1,11 @@ +{"test_device_context_manager_success": {"rx": "", "tx": ""}} +{"test_device_write_success": {"rx": "", "tx": "RVpTVEEK"}} +{"test_device_readlines_success": {"rx": "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKj0NCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCj0gICAgICAgICAgICAgICAgICAgICAgICBTeXN0ZW0gQWRkcmVzcyA9IDAwICAgICAgICAgICBGL1cgVmVyc2lvbiA6IDIuMDMgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0gICBTeXN0ZW0gU2V0dXAgU3RhdHVzICAgICAgICAgICA6ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0gICBSUzIzMiAgICAgICAgICAgICAgICAgICAgICAgICA6IEJhdWQgUmF0ZT0xMTUyMDBicHMsIERhdGE9OGJpdCwgUGFyaXR5PU5vbmUsIFN0b3A9MWJpdCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLT0NCj0qKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKj0NCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCj0qKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBTVEFUVVMqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKj0NCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCj0gICAgICAgICAgICAgICAgICAgICAgICBTeXN0ZW0gQWRkcmVzcyA9IDAwICAgICAgICAgICBGL1cgVmVyc2lvbiA6IDIuMDMgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0gICBTeXN0ZW0gU2V0dXAgU3RhdHVzICAgICAgICAgICA6ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0gICBSUzIzMiAgICAgICAgICAgICAgICAgICAgICAgICA6IEJhdWQgUmF0ZT0xMTUyMDBicHMsIERhdGE9OGJpdCwgUGFyaXR5PU5vbmUsIFN0b3A9MWJpdCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLT0NCj0qKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKj0NCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCg==", "tx": "RVpTVEEK"}} +{"test_system_status_command": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpTVEEK"}} +{"test_help_command": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBIRUxQKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEF6eiAgICAgICAgICAgICAgICAgICAgICAgICAgIDogIEFsbCBDb21tYW5kcyBzdGFydCBieSBQcmVmaXggU3lzdGVtIEFkZHJlc3MgenosIGlmIFswMS05OV0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPVN5c3RlbSBDb250cm9sIFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaSCAgICAgICAgICAgICAgICAgICAgICAgICAgIDogSGVscCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaU1RBICAgICAgICAgICAgICAgICAgICAgICAgIDogU2hvdyBHbG9iYWwgU3lzdGVtIFN0YXR1cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSU1QgICAgICAgICAgICAgICAgICAgICAgIDogUmVzZXQgdG8gRmFjdG9yeSBEZWZhdWx0cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSQlQgICAgICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3Rlcm0gdG8gUmVib290ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBRERSIHh4ICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3RlbSBBZGRyZXNzIHRvIHh4IHt4eD1bMDB+OTldKDAwPVNpbmdsZSl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBVVRPIE9OL09GRiAgICAgICAgICAgICAgIDogU2V0IEF1dG8gU3dpdGNoIE1vZGUgT24vT2ZmICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBRERSICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBBZGRyZXNzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBTVEEgICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBTeXN0ZW0gU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggU0lHIFNUQSAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggU2lnbmFsIFN0YXR1c3t4PVswfjRdKDA9QUxMKX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBVVRPIE1PREUgICAgICAgICAgICAgICAgIDogR2V0IEF1dG8gU3dpdGNoIE1vZGUgU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPU91dHB1dCBTZXR1cCBDb21tYW5kIDogKE5vdGU6b3V0cHV0IG51bWJlcih4KT1IRE1JKHgpLHg9MSkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFZTIElOeSAgICAgICAgICAgICAgIDogU2V0IE91dHB1dCB4IFRvIElucHV0IHkge3g9WzB+MV0oMD1BTEwpLCB5PVsxfjRdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFNUUkVBTSBPTi9PRkYgICAgICAgIDogU2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkZ7eD1bMH4xXSgwPUFMTCl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFZTICAgICAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFZpZGVvIFJvdXRle3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFNUUkVBTSAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkYgU3RhdHVze3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlucHV0IFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCB5ICAgICAgICAgICAgICAgIDogU2V0IElucHV0IHggRURJRHt4PVswXSgwPUFMTCksIHk9WzB+MjldfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDoxMDgwUF8yQ0ggICAgICAgICAgMToxMDgwUF82Q0ggICAgICAgICAgMjoxMDgwUF84Q0ggICAgICAgICAgMzoxMDgwUF8zRF8yQ0ggICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgNDoxMDgwUF8zRF82Q0ggICAgICAgNToxMDgwUF8zRF84Q0ggICAgICAgNjo0SzMwSFpfM0RfMkNIICAgICAgNzo0SzMwSFpfM0RfNkNIICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgODo0SzMwSFpfM0RfOENIICAgICAgOTo0SzYwSHpZNDIwXzNEXzJDSCAgMTA6NEs2MEh6WTQyMF8zRF82Q0ggMTE6NEs2MEh6WTQyMF8zRF84Q0ggICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTI6NEs2MEhaXzNEXzJDSCAgICAgMTM6NEs2MEhaXzNEXzZDSCAgICAgMTQ6NEs2MEhaXzNEXzhDSCAgICAgMTU6MTA4MFBfMkNIX0hEUiAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTY6MTA4MFBfNkNIX0hEUiAgICAgMTc6MTA4MFBfOENIX0hEUiAgICAgMTg6MTA4MFBfM0RfMkNIX0hEUiAgMTk6MTA4MFBfM0RfNkNIX0hEUiAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA6MTA4MFBfM0RfOENIX0hEUiAgMjE6NEszMEhaXzNEXzJDSF9IRFIgMjI6NEszMEhaXzNEXzZDSF9IRFIgMjM6NEszMEhaXzNEXzhDSF9IRFIgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjQ6NEs2MEh6WTQyMF8zRF8yQ0hfSERSICAgICAgMjU6NEs2MEh6WTQyMF8zRF82Q0hfSERSICAgICAgMjY6NEs2MEh6WTQyMF8zRF84Q0hfSERSICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjc6NEs2MEhaXzNEXzJDSF9IRFIgICAgICAgICAgMjg6NEs2MEhaXzNEXzZDSF9IRFIgICAgICAgICAgMjk6NEs2MEhaXzNEXzhDSF9IRFIgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCBDWSBPVVR5ICAgICAgICAgIDogQ29weSBPdXRwdXQgeSBFRElEIFRvIElucHV0IHgoVVNFUjEgQlVGKXt4PVswXSgwPUFMTCksIHk9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggRURJRCAgICAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggRURJRCAgSW5kZXh7eD1bMF0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlSIENvZGUgU2V0dXAgQ29tbWFuZDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBTWVMgeHgueXkgICAgICAgICAgICAgIDogU2V0IElSIEN1c3RvbSBDb2Rle3h4PVswMC1GRkhdLHl5PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IFVEIENPREUgeXkuenogICAgIDogU2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdLHl5PVswMC1GRkhdLHp6PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IElOeSBDT0RFIHp6ICAgICAgIDogU2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XSx6ej1bMDAtRkZIXX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBQT1cgeHggICAgICAgICAgICAgICAgIDogU2V0IElSIFBvd2VyIENvZGV7eHg9WzAwLUZGSF19ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBTWVMgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIEN1c3RvbSBDb2RlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IFVEIENPREUgICAgICAgICAgIDogR2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IElOeSBDT0RFICAgICAgICAgIDogR2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBQT1cgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIFBvd2VyIENvZGUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpICg=="}} +{"test_switch_input_commands": {"rx": "", "tx": "RVpTIE9VVDEgVlMgSU4xCkVaUyBPVVQxIFZTIElOMgpFWlMgT1VUMSBWUyBJTjMKRVpTIE9VVDEgVlMgSU40Cg=="}} +{"test_get_routing_command": {"rx": "VjA4Uw5PVVQxIFZTIDQNCg==", "tx": "RVpHIE9VVDEgVlMK"}} +{"test_get_stream_status_command": {"rx": "T1VUIDEgU1RSRUFNIE9ODQo=", "tx": "RVpHIE9VVDEgU1RSRUFNCg=="}} +{"test_command_sequence": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KT1VUMSBWUyAyDQpWMThTDg==", "tx": "RVpTVEEKRVpTIE9VVDEgVlMgSU4yCkVaRyBPVVQxIFZTCg=="}} +{"test_multiple_device_instances": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBIRUxQKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEF6eiAgICAgICAgICAgICAgICAgICAgICAgICAgIDogIEFsbCBDb21tYW5kcyBzdGFydCBieSBQcmVmaXggU3lzdGVtIEFkZHJlc3MgenosIGlmIFswMS05OV0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPVN5c3RlbSBDb250cm9sIFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaSCAgICAgICAgICAgICAgICAgICAgICAgICAgIDogSGVscCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaU1RBICAgICAgICAgICAgICAgICAgICAgICAgIDogU2hvdyBHbG9iYWwgU3lzdGVtIFN0YXR1cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSU1QgICAgICAgICAgICAgICAgICAgICAgIDogUmVzZXQgdG8gRmFjdG9yeSBEZWZhdWx0cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSQlQgICAgICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3Rlcm0gdG8gUmVib290ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBRERSIHh4ICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3RlbSBBZGRyZXNzIHRvIHh4IHt4eD1bMDB+OTldKDAwPVNpbmdsZSl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBVVRPIE9OL09GRiAgICAgICAgICAgICAgIDogU2V0IEF1dG8gU3dpdGNoIE1vZGUgT24vT2ZmICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBRERSICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBBZGRyZXNzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBTVEEgICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBTeXN0ZW0gU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggU0lHIFNUQSAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggU2lnbmFsIFN0YXR1c3t4PVswfjRdKDA9QUxMKX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBVVRPIE1PREUgICAgICAgICAgICAgICAgIDogR2V0IEF1dG8gU3dpdGNoIE1vZGUgU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPU91dHB1dCBTZXR1cCBDb21tYW5kIDogKE5vdGU6b3V0cHV0IG51bWJlcih4KT1IRE1JKHgpLHg9MSkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFZTIElOeSAgICAgICAgICAgICAgIDogU2V0IE91dHB1dCB4IFRvIElucHV0IHkge3g9WzB+MV0oMD1BTEwpLCB5PVsxfjRdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFNUUkVBTSBPTi9PRkYgICAgICAgIDogU2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkZ7eD1bMH4xXSgwPUFMTCl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFZTICAgICAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFZpZGVvIFJvdXRle3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFNUUkVBTSAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkYgU3RhdHVze3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlucHV0IFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCB5ICAgICAgICAgICAgICAgIDogU2V0IElucHV0IHggRURJRHt4PVswXSgwPUFMTCksIHk9WzB+MjldfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDoxMDgwUF8yQ0ggICAgICAgICAgMToxMDgwUF82Q0ggICAgICAgICAgMjoxMDgwUF84Q0ggICAgICAgICAgMzoxMDgwUF8zRF8yQ0ggICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgNDoxMDgwUF8zRF82Q0ggICAgICAgNToxMDgwUF8zRF84Q0ggICAgICAgNjo0SzMwSFpfM0RfMkNIICAgICAgNzo0SzMwSFpfM0RfNkNIICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgODo0SzMwSFpfM0RfOENIICAgICAgOTo0SzYwSHpZNDIwXzNEXzJDSCAgMTA6NEs2MEh6WTQyMF8zRF82Q0ggMTE6NEs2MEh6WTQyMF8zRF84Q0ggICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTI6NEs2MEhaXzNEXzJDSCAgICAgMTM6NEs2MEhaXzNEXzZDSCAgICAgMTQ6NEs2MEhaXzNEXzhDSCAgICAgMTU6MTA4MFBfMkNIX0hEUiAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTY6MTA4MFBfNkNIX0hEUiAgICAgMTc6MTA4MFBfOENIX0hEUiAgICAgMTg6MTA4MFBfM0RfMkNIX0hEUiAgMTk6MTA4MFBfM0RfNkNIX0hEUiAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA6MTA4MFBfM0RfOENIX0hEUiAgMjE6NEszMEhaXzNEXzJDSF9IRFIgMjI6NEszMEhaXzNEXzZDSF9IRFIgMjM6NEszMEhaXzNEXzhDSF9IRFIgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjQ6NEs2MEh6WTQyMF8zRF8yQ0hfSERSICAgICAgMjU6NEs2MEh6WTQyMF8zRF82Q0hfSERSICAgICAgMjY6NEs2MEh6WTQyMF8zRF84Q0hfSERSICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjc6NEs2MEhaXzNEXzJDSF9IRFIgICAgICAgICAgMjg6NEs2MEhaXzNEXzZDSF9IRFIgICAgICAgICAgMjk6NEs2MEhaXzNEXzhDSF9IRFIgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCBDWSBPVVR5ICAgICAgICAgIDogQ29weSBPdXRwdXQgeSBFRElEIFRvIElucHV0IHgoVVNFUjEgQlVGKXt4PVswXSgwPUFMTCksIHk9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggRURJRCAgICAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggRURJRCAgSW5kZXh7eD1bMF0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlSIENvZGUgU2V0dXAgQ29tbWFuZDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBTWVMgeHgueXkgICAgICAgICAgICAgIDogU2V0IElSIEN1c3RvbSBDb2Rle3h4PVswMC1GRkhdLHl5PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IFVEIENPREUgeXkuenogICAgIDogU2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdLHl5PVswMC1GRkhdLHp6PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IElOeSBDT0RFIHp6ICAgICAgIDogU2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XSx6ej1bMDAtRkZIXX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBQT1cgeHggICAgICAgICAgICAgICAgIDogU2V0IElSIFBvd2VyIENvZGV7eHg9WzAwLUZGSF19ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBTWVMgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIEN1c3RvbSBDb2RlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IFVEIENPREUgICAgICAgICAgIDogR2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IElOeSBDT0RFICAgICAgICAgIDogR2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBQT1cgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIFBvd2VyIENvZGUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpTVEEKRVpICg=="}} +{"test_ascii_encoding": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBIRUxQKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEF6eiAgICAgICAgICAgICAgICAgICAgICAgICAgIDogIEFsbCBDb21tYW5kcyBzdGFydCBieSBQcmVmaXggU3lzdGVtIEFkZHJlc3MgenosIGlmIFswMS05OV0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPVN5c3RlbSBDb250cm9sIFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaSCAgICAgICAgICAgICAgICAgICAgICAgICAgIDogSGVscCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaU1RBICAgICAgICAgICAgICAgICAgICAgICAgIDogU2hvdyBHbG9iYWwgU3lzdGVtIFN0YXR1cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSU1QgICAgICAgICAgICAgICAgICAgICAgIDogUmVzZXQgdG8gRmFjdG9yeSBEZWZhdWx0cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSQlQgICAgICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3Rlcm0gdG8gUmVib290ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBRERSIHh4ICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3RlbSBBZGRyZXNzIHRvIHh4IHt4eD1bMDB+OTldKDAwPVNpbmdsZSl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBVVRPIE9OL09GRiAgICAgICAgICAgICAgIDogU2V0IEF1dG8gU3dpdGNoIE1vZGUgT24vT2ZmICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBRERSICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBBZGRyZXNzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBTVEEgICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBTeXN0ZW0gU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggU0lHIFNUQSAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggU2lnbmFsIFN0YXR1c3t4PVswfjRdKDA9QUxMKX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBVVRPIE1PREUgICAgICAgICAgICAgICAgIDogR2V0IEF1dG8gU3dpdGNoIE1vZGUgU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPU91dHB1dCBTZXR1cCBDb21tYW5kIDogKE5vdGU6b3V0cHV0IG51bWJlcih4KT1IRE1JKHgpLHg9MSkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFZTIElOeSAgICAgICAgICAgICAgIDogU2V0IE91dHB1dCB4IFRvIElucHV0IHkge3g9WzB+MV0oMD1BTEwpLCB5PVsxfjRdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFNUUkVBTSBPTi9PRkYgICAgICAgIDogU2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkZ7eD1bMH4xXSgwPUFMTCl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFZTICAgICAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFZpZGVvIFJvdXRle3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFNUUkVBTSAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkYgU3RhdHVze3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlucHV0IFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCB5ICAgICAgICAgICAgICAgIDogU2V0IElucHV0IHggRURJRHt4PVswXSgwPUFMTCksIHk9WzB+MjldfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDoxMDgwUF8yQ0ggICAgICAgICAgMToxMDgwUF82Q0ggICAgICAgICAgMjoxMDgwUF84Q0ggICAgICAgICAgMzoxMDgwUF8zRF8yQ0ggICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgNDoxMDgwUF8zRF82Q0ggICAgICAgNToxMDgwUF8zRF84Q0ggICAgICAgNjo0SzMwSFpfM0RfMkNIICAgICAgNzo0SzMwSFpfM0RfNkNIICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgODo0SzMwSFpfM0RfOENIICAgICAgOTo0SzYwSHpZNDIwXzNEXzJDSCAgMTA6NEs2MEh6WTQyMF8zRF82Q0ggMTE6NEs2MEh6WTQyMF8zRF84Q0ggICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTI6NEs2MEhaXzNEXzJDSCAgICAgMTM6NEs2MEhaXzNEXzZDSCAgICAgMTQ6NEs2MEhaXzNEXzhDSCAgICAgMTU6MTA4MFBfMkNIX0hEUiAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTY6MTA4MFBfNkNIX0hEUiAgICAgMTc6MTA4MFBfOENIX0hEUiAgICAgMTg6MTA4MFBfM0RfMkNIX0hEUiAgMTk6MTA4MFBfM0RfNkNIX0hEUiAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA6MTA4MFBfM0RfOENIX0hEUiAgMjE6NEszMEhaXzNEXzJDSF9IRFIgMjI6NEszMEhaXzNEXzZDSF9IRFIgMjM6NEszMEhaXzNEXzhDSF9IRFIgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjQ6NEs2MEh6WTQyMF8zRF8yQ0hfSERSICAgICAgMjU6NEs2MEh6WTQyMF8zRF82Q0hfSERSICAgICAgMjY6NEs2MEh6WTQyMF8zRF84Q0hfSERSICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjc6NEs2MEhaXzNEXzJDSF9IRFIgICAgICAgICAgMjg6NEs2MEhaXzNEXzZDSF9IRFIgICAgICAgICAgMjk6NEs2MEhaXzNEXzhDSF9IRFIgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCBDWSBPVVR5ICAgICAgICAgIDogQ29weSBPdXRwdXQgeSBFRElEIFRvIElucHV0IHgoVVNFUjEgQlVGKXt4PVswXSgwPUFMTCksIHk9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggRURJRCAgICAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggRURJRCAgSW5kZXh7eD1bMF0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlSIENvZGUgU2V0dXAgQ29tbWFuZDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBTWVMgeHgueXkgICAgICAgICAgICAgIDogU2V0IElSIEN1c3RvbSBDb2Rle3h4PVswMC1GRkhdLHl5PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IFVEIENPREUgeXkuenogICAgIDogU2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdLHl5PVswMC1GRkhdLHp6PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IElOeSBDT0RFIHp6ICAgICAgIDogU2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XSx6ej1bMDAtRkZIXX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBQT1cgeHggICAgICAgICAgICAgICAgIDogU2V0IElSIFBvd2VyIENvZGV7eHg9WzAwLUZGSF19ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBTWVMgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIEN1c3RvbSBDb2RlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IFVEIENPREUgICAgICAgICAgIDogR2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IElOeSBDT0RFICAgICAgICAgIDogR2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBQT1cgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIFBvd2VyIENvZGUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KT1VUMSBWUyAxDQpWMENTDg==", "tx": "RVpTVEEKRVpICkVaUyBPVVQxIFZTIElOMQpFWkcgT1VUMSBWUwo="}} diff --git a/tests/test_device.py b/tests/test_device.py new file mode 100644 index 0000000..fe897fc --- /dev/null +++ b/tests/test_device.py @@ -0,0 +1,212 @@ +"""Tests for the low-level Device class using pytest-reserial.""" + +# pyright: reportPrivateUsage=false + +from pathlib import Path +from typing import Any + +import pytest + +from ezcoo_cli.device import Device, DeviceConnectionError, DeviceError + +# Device initialization tests + + +def test_device_init(mock_device_path: Path, test_baudrate: int, test_timeout: float) -> None: + """Test Device initialization.""" + device = Device(mock_device_path, test_baudrate, test_timeout) + assert device._path == mock_device_path + assert device._serial.port == str(mock_device_path) + assert device._serial.baudrate == test_baudrate + assert device._serial.timeout == test_timeout + + +def test_device_context_manager_failure() -> None: + """Test Device context manager with connection failure.""" + # Test without reserial fixture to simulate real connection failure + with pytest.raises(DeviceConnectionError): + with Device(Path("/dev/nonexistent")): + pass + + +def test_device_write_closed_device(mock_device_path: Path) -> None: + """Test writing to closed device raises error.""" + device = Device(mock_device_path) + with pytest.raises(DeviceError, match="Device is not open"): + device.write("EZSTA") + + +def test_device_readlines_closed_device(mock_device_path: Path) -> None: + """Test reading from closed device raises error.""" + device = Device(mock_device_path) + with pytest.raises(DeviceError, match="Device is not open"): + list(device.readlines()) + + +# Device interaction tests (uses reserial for record/replay) + + +def test_device_context_manager_success(reserial: Any, mock_device_path: Path) -> None: + """Test Device as context manager with successful connection.""" + with Device(mock_device_path) as device: + assert device._serial.is_open + + +def test_device_write_success(reserial: Any, mock_device_path: Path) -> None: + """Test successful command writing.""" + with Device(mock_device_path) as device: + # This should not raise an exception + device.write("EZSTA") + + +def test_device_readlines_success(reserial: Any, mock_device_path: Path) -> None: + """Test successful reading of lines.""" + with Device(mock_device_path) as device: + device.write("EZSTA") + lines = list(device.readlines()) + # The actual content will depend on recorded traffic + assert isinstance(lines, list) + + +def test_system_status_command(reserial: Any, mock_device_path: Path) -> None: + """Test EZSTA command with recorded traffic.""" + with Device(mock_device_path) as device: + device.write("EZSTA") + lines = list(device.readlines()) + + # Verify we got some response + assert len(lines) > 0 + + # Look for key indicators in the response + response_text = "".join(lines) + assert "System Address" in response_text or "F/W Version" in response_text + + +def test_help_command(reserial: Any, mock_device_path: Path) -> None: + """Test EZH command with recorded traffic.""" + with Device(mock_device_path) as device: + device.write("EZH") + lines = list(device.readlines()) + + # Verify we got some response + assert len(lines) > 0 + + # Look for help indicators + response_text = "".join(lines) + assert "Help" in response_text or "EZH" in response_text + + +def test_switch_input_commands(reserial: Any, mock_device_path: Path) -> None: + """Test input switching commands with recorded traffic.""" + with Device(mock_device_path) as device: + # Test switching to each input + for input_num in range(1, 5): + device.write(f"EZS OUT1 VS IN{input_num}") + # SET commands typically don't return responses + lines = list(device.readlines()) + # Verify we got a list (even if empty for SET commands) + assert isinstance(lines, list) + + +def test_get_routing_command(reserial: Any, mock_device_path: Path) -> None: + """Test EZG OUT1 VS command with recorded traffic.""" + with Device(mock_device_path) as device: + device.write("EZG OUT1 VS") + lines = list(device.readlines()) + + # Should get routing information + if lines: + response_text = "".join(lines) + assert "OUT1" in response_text and "VS" in response_text + + +def test_get_stream_status_command(reserial: Any, mock_device_path: Path) -> None: + """Test EZG OUT1 STREAM command with recorded traffic.""" + with Device(mock_device_path) as device: + device.write("EZG OUT1 STREAM") + lines = list(device.readlines()) + + # Should get stream status + if lines: + response_text = "".join(lines) + assert ("OUT1" in response_text or "OUT 1" in response_text) and "STREAM" in response_text + + +def test_command_sequence(reserial: Any, mock_device_path: Path) -> None: + """Test a sequence of commands to verify device state management.""" + with Device(mock_device_path) as device: + # Get initial status + device.write("EZSTA") + status_lines = list(device.readlines()) + assert len(status_lines) > 0 + + # Switch input + device.write("EZS OUT1 VS IN2") + switch_lines = list(device.readlines()) + + # Verify routing + device.write("EZG OUT1 VS") + routing_lines = list(device.readlines()) + + # All commands should execute without errors + assert isinstance(status_lines, list) + assert isinstance(switch_lines, list) + assert isinstance(routing_lines, list) + + +def test_multiple_device_instances(reserial: Any, mock_device_path: Path) -> None: + """Test that multiple device instances work correctly.""" + # First device instance + with Device(mock_device_path) as device1: + device1.write("EZSTA") + lines1 = list(device1.readlines()) + + # Second device instance + with Device(mock_device_path) as device2: + device2.write("EZH") + lines2 = list(device2.readlines()) + + # Both should work independently + assert isinstance(lines1, list) + assert isinstance(lines2, list) + + +def test_command_validation_valid() -> None: + """Test that valid commands with ASCII alphanumeric and spaces are accepted.""" + valid_commands = [ + "EZSTA", + "EZH", + "EZS OUT1 VS IN1", + "EZG OUT1 VS", + "A05EZSTA", + "EZS ADDR 05", + "EZS OUT1 STREAM ON", + ] + + for cmd in valid_commands: + # Should not raise any exception + Device.validate_command(cmd) + + +def test_command_validation_invalid() -> None: + """Test that commands with invalid characters are rejected.""" + invalid_commands = [ + "EZS@TA", # Special character @ + "EZH!", # Special character ! + "EZS\nOUT1", # Newline (embedded) + "EZS;DROP", # Semicolon + "EZS|OUT1", # Pipe + "EZS&OUT1", # Ampersand + "EZS$OUT1", # Dollar sign + "EZS#OUT1", # Hash + "EZS%OUT1", # Percent + "EZS*OUT1", # Asterisk + "EZS.OUT1", # Period + "EZS,OUT1", # Comma + "EZS:OUT1", # Colon + "EZS=OUT1", # Equals + ] + + for cmd in invalid_commands: + with pytest.raises(DeviceError, match="invalid characters"): + Device.validate_command(cmd) diff --git a/tests/test_integration.jsonl b/tests/test_integration.jsonl new file mode 100644 index 0000000..41bc907 --- /dev/null +++ b/tests/test_integration.jsonl @@ -0,0 +1,14 @@ +{"test_system_status": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpTVEEK"}} +{"test_help_command": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBIRUxQKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEF6eiAgICAgICAgICAgICAgICAgICAgICAgICAgIDogIEFsbCBDb21tYW5kcyBzdGFydCBieSBQcmVmaXggU3lzdGVtIEFkZHJlc3MgenosIGlmIFswMS05OV0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPVN5c3RlbSBDb250cm9sIFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaSCAgICAgICAgICAgICAgICAgICAgICAgICAgIDogSGVscCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaU1RBICAgICAgICAgICAgICAgICAgICAgICAgIDogU2hvdyBHbG9iYWwgU3lzdGVtIFN0YXR1cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSU1QgICAgICAgICAgICAgICAgICAgICAgIDogUmVzZXQgdG8gRmFjdG9yeSBEZWZhdWx0cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSQlQgICAgICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3Rlcm0gdG8gUmVib290ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBRERSIHh4ICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3RlbSBBZGRyZXNzIHRvIHh4IHt4eD1bMDB+OTldKDAwPVNpbmdsZSl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBVVRPIE9OL09GRiAgICAgICAgICAgICAgIDogU2V0IEF1dG8gU3dpdGNoIE1vZGUgT24vT2ZmICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBRERSICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBBZGRyZXNzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBTVEEgICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBTeXN0ZW0gU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggU0lHIFNUQSAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggU2lnbmFsIFN0YXR1c3t4PVswfjRdKDA9QUxMKX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBVVRPIE1PREUgICAgICAgICAgICAgICAgIDogR2V0IEF1dG8gU3dpdGNoIE1vZGUgU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPU91dHB1dCBTZXR1cCBDb21tYW5kIDogKE5vdGU6b3V0cHV0IG51bWJlcih4KT1IRE1JKHgpLHg9MSkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFZTIElOeSAgICAgICAgICAgICAgIDogU2V0IE91dHB1dCB4IFRvIElucHV0IHkge3g9WzB+MV0oMD1BTEwpLCB5PVsxfjRdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFNUUkVBTSBPTi9PRkYgICAgICAgIDogU2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkZ7eD1bMH4xXSgwPUFMTCl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFZTICAgICAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFZpZGVvIFJvdXRle3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFNUUkVBTSAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkYgU3RhdHVze3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlucHV0IFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCB5ICAgICAgICAgICAgICAgIDogU2V0IElucHV0IHggRURJRHt4PVswXSgwPUFMTCksIHk9WzB+MjldfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDoxMDgwUF8yQ0ggICAgICAgICAgMToxMDgwUF82Q0ggICAgICAgICAgMjoxMDgwUF84Q0ggICAgICAgICAgMzoxMDgwUF8zRF8yQ0ggICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgNDoxMDgwUF8zRF82Q0ggICAgICAgNToxMDgwUF8zRF84Q0ggICAgICAgNjo0SzMwSFpfM0RfMkNIICAgICAgNzo0SzMwSFpfM0RfNkNIICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgODo0SzMwSFpfM0RfOENIICAgICAgOTo0SzYwSHpZNDIwXzNEXzJDSCAgMTA6NEs2MEh6WTQyMF8zRF82Q0ggMTE6NEs2MEh6WTQyMF8zRF84Q0ggICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTI6NEs2MEhaXzNEXzJDSCAgICAgMTM6NEs2MEhaXzNEXzZDSCAgICAgMTQ6NEs2MEhaXzNEXzhDSCAgICAgMTU6MTA4MFBfMkNIX0hEUiAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTY6MTA4MFBfNkNIX0hEUiAgICAgMTc6MTA4MFBfOENIX0hEUiAgICAgMTg6MTA4MFBfM0RfMkNIX0hEUiAgMTk6MTA4MFBfM0RfNkNIX0hEUiAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA6MTA4MFBfM0RfOENIX0hEUiAgMjE6NEszMEhaXzNEXzJDSF9IRFIgMjI6NEszMEhaXzNEXzZDSF9IRFIgMjM6NEszMEhaXzNEXzhDSF9IRFIgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjQ6NEs2MEh6WTQyMF8zRF8yQ0hfSERSICAgICAgMjU6NEs2MEh6WTQyMF8zRF82Q0hfSERSICAgICAgMjY6NEs2MEh6WTQyMF8zRF84Q0hfSERSICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjc6NEs2MEhaXzNEXzJDSF9IRFIgICAgICAgICAgMjg6NEs2MEhaXzNEXzZDSF9IRFIgICAgICAgICAgMjk6NEs2MEhaXzNEXzhDSF9IRFIgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCBDWSBPVVR5ICAgICAgICAgIDogQ29weSBPdXRwdXQgeSBFRElEIFRvIElucHV0IHgoVVNFUjEgQlVGKXt4PVswXSgwPUFMTCksIHk9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggRURJRCAgICAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggRURJRCAgSW5kZXh7eD1bMF0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlSIENvZGUgU2V0dXAgQ29tbWFuZDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBTWVMgeHgueXkgICAgICAgICAgICAgIDogU2V0IElSIEN1c3RvbSBDb2Rle3h4PVswMC1GRkhdLHl5PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IFVEIENPREUgeXkuenogICAgIDogU2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdLHl5PVswMC1GRkhdLHp6PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IElOeSBDT0RFIHp6ICAgICAgIDogU2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XSx6ej1bMDAtRkZIXX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBQT1cgeHggICAgICAgICAgICAgICAgIDogU2V0IElSIFBvd2VyIENvZGV7eHg9WzAwLUZGSF19ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBTWVMgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIEN1c3RvbSBDb2RlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IFVEIENPREUgICAgICAgICAgIDogR2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IElOeSBDT0RFICAgICAgICAgIDogR2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBQT1cgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIFBvd2VyIENvZGUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpICg=="}} +{"test_input_switching": {"rx": "", "tx": "RVpTIE9VVDEgVlMgSU4xCkVaUyBPVVQxIFZTIElOMgpFWlMgT1VUMSBWUyBJTjMKRVpTIE9VVDEgVlMgSU40Cg=="}} +{"test_routing_query": {"rx": "T1VUMSBWUyA0DQo=", "tx": "RVpHIE9VVDEgVlMK"}} +{"test_stream_status": {"rx": "T1VUIDEgU1RSRUFNIE9ODQo=", "tx": "RVpHIE9VVDEgU1RSRUFNCg=="}} +{"test_complete_workflow": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBIRUxQKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEF6eiAgICAgICAgICAgICAgICAgICAgICAgICAgIDogIEFsbCBDb21tYW5kcyBzdGFydCBieSBQcmVmaXggU3lzdGVtIEFkZHJlc3MgenosIGlmIFswMS05OV0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPVN5c3RlbSBDb250cm9sIFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaSCAgICAgICAgICAgICAgICAgICAgICAgICAgIDogSGVscCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaU1RBICAgICAgICAgICAgICAgICAgICAgICAgIDogU2hvdyBHbG9iYWwgU3lzdGVtIFN0YXR1cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSU1QgICAgICAgICAgICAgICAgICAgICAgIDogUmVzZXQgdG8gRmFjdG9yeSBEZWZhdWx0cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSQlQgICAgICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3Rlcm0gdG8gUmVib290ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBRERSIHh4ICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3RlbSBBZGRyZXNzIHRvIHh4IHt4eD1bMDB+OTldKDAwPVNpbmdsZSl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBVVRPIE9OL09GRiAgICAgICAgICAgICAgIDogU2V0IEF1dG8gU3dpdGNoIE1vZGUgT24vT2ZmICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBRERSICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBBZGRyZXNzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBTVEEgICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBTeXN0ZW0gU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggU0lHIFNUQSAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggU2lnbmFsIFN0YXR1c3t4PVswfjRdKDA9QUxMKX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBVVRPIE1PREUgICAgICAgICAgICAgICAgIDogR2V0IEF1dG8gU3dpdGNoIE1vZGUgU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPU91dHB1dCBTZXR1cCBDb21tYW5kIDogKE5vdGU6b3V0cHV0IG51bWJlcih4KT1IRE1JKHgpLHg9MSkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFZTIElOeSAgICAgICAgICAgICAgIDogU2V0IE91dHB1dCB4IFRvIElucHV0IHkge3g9WzB+MV0oMD1BTEwpLCB5PVsxfjRdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFNUUkVBTSBPTi9PRkYgICAgICAgIDogU2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkZ7eD1bMH4xXSgwPUFMTCl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFZTICAgICAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFZpZGVvIFJvdXRle3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFNUUkVBTSAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkYgU3RhdHVze3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlucHV0IFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCB5ICAgICAgICAgICAgICAgIDogU2V0IElucHV0IHggRURJRHt4PVswXSgwPUFMTCksIHk9WzB+MjldfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDoxMDgwUF8yQ0ggICAgICAgICAgMToxMDgwUF82Q0ggICAgICAgICAgMjoxMDgwUF84Q0ggICAgICAgICAgMzoxMDgwUF8zRF8yQ0ggICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgNDoxMDgwUF8zRF82Q0ggICAgICAgNToxMDgwUF8zRF84Q0ggICAgICAgNjo0SzMwSFpfM0RfMkNIICAgICAgNzo0SzMwSFpfM0RfNkNIICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgODo0SzMwSFpfM0RfOENIICAgICAgOTo0SzYwSHpZNDIwXzNEXzJDSCAgMTA6NEs2MEh6WTQyMF8zRF82Q0ggMTE6NEs2MEh6WTQyMF8zRF84Q0ggICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTI6NEs2MEhaXzNEXzJDSCAgICAgMTM6NEs2MEhaXzNEXzZDSCAgICAgMTQ6NEs2MEhaXzNEXzhDSCAgICAgMTU6MTA4MFBfMkNIX0hEUiAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTY6MTA4MFBfNkNIX0hEUiAgICAgMTc6MTA4MFBfOENIX0hEUiAgICAgMTg6MTA4MFBfM0RfMkNIX0hEUiAgMTk6MTA4MFBfM0RfNkNIX0hEUiAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA6MTA4MFBfM0RfOENIX0hEUiAgMjE6NEszMEhaXzNEXzJDSF9IRFIgMjI6NEszMEhaXzNEXzZDSF9IRFIgMjM6NEszMEhaXzNEXzhDSF9IRFIgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjQ6NEs2MEh6WTQyMF8zRF8yQ0hfSERSICAgICAgMjU6NEs2MEh6WTQyMF8zRF82Q0hfSERSICAgICAgMjY6NEs2MEh6WTQyMF8zRF84Q0hfSERSICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjc6NEs2MEhaXzNEXzJDSF9IRFIgICAgICAgICAgMjg6NEs2MEhaXzNEXzZDSF9IRFIgICAgICAgICAgMjk6NEs2MEhaXzNEXzhDSF9IRFIgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCBDWSBPVVR5ICAgICAgICAgIDogQ29weSBPdXRwdXQgeSBFRElEIFRvIElucHV0IHgoVVNFUjEgQlVGKXt4PVswXSgwPUFMTCksIHk9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggRURJRCAgICAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggRURJRCAgSW5kZXh7eD1bMF0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlSIENvZGUgU2V0dXAgQ29tbWFuZDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBTWVMgeHgueXkgICAgICAgICAgICAgIDogU2V0IElSIEN1c3RvbSBDb2Rle3h4PVswMC1GRkhdLHl5PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IFVEIENPREUgeXkuenogICAgIDogU2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdLHl5PVswMC1GRkhdLHp6PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IElOeSBDT0RFIHp6ICAgICAgIDogU2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XSx6ej1bMDAtRkZIXX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBQT1cgeHggICAgICAgICAgICAgICAgIDogU2V0IElSIFBvd2VyIENvZGV7eHg9WzAwLUZGSF19ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBTWVMgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIEN1c3RvbSBDb2RlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IFVEIENPREUgICAgICAgICAgIDogR2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IElOeSBDT0RFICAgICAgICAgIDogR2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBQT1cgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIFBvd2VyIENvZGUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KT1VUMSBWUyAyDQpWMThTDk9VVCAxIFNUUkVBTSBPTg0K", "tx": "RVpTVEEKRVpICkVaUyBPVVQxIFZTIElOMgpFWkcgT1VUMSBWUwpFWkcgT1VUMSBTVFJFQU0K"}} +{"test_kvm_system_status": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpTVEEK"}} +{"test_kvm_help": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBIRUxQKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEF6eiAgICAgICAgICAgICAgICAgICAgICAgICAgIDogIEFsbCBDb21tYW5kcyBzdGFydCBieSBQcmVmaXggU3lzdGVtIEFkZHJlc3MgenosIGlmIFswMS05OV0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPVN5c3RlbSBDb250cm9sIFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaSCAgICAgICAgICAgICAgICAgICAgICAgICAgIDogSGVscCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaU1RBICAgICAgICAgICAgICAgICAgICAgICAgIDogU2hvdyBHbG9iYWwgU3lzdGVtIFN0YXR1cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSU1QgICAgICAgICAgICAgICAgICAgICAgIDogUmVzZXQgdG8gRmFjdG9yeSBEZWZhdWx0cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSQlQgICAgICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3Rlcm0gdG8gUmVib290ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBRERSIHh4ICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3RlbSBBZGRyZXNzIHRvIHh4IHt4eD1bMDB+OTldKDAwPVNpbmdsZSl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBVVRPIE9OL09GRiAgICAgICAgICAgICAgIDogU2V0IEF1dG8gU3dpdGNoIE1vZGUgT24vT2ZmICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBRERSICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBBZGRyZXNzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBTVEEgICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBTeXN0ZW0gU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggU0lHIFNUQSAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggU2lnbmFsIFN0YXR1c3t4PVswfjRdKDA9QUxMKX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBVVRPIE1PREUgICAgICAgICAgICAgICAgIDogR2V0IEF1dG8gU3dpdGNoIE1vZGUgU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPU91dHB1dCBTZXR1cCBDb21tYW5kIDogKE5vdGU6b3V0cHV0IG51bWJlcih4KT1IRE1JKHgpLHg9MSkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFZTIElOeSAgICAgICAgICAgICAgIDogU2V0IE91dHB1dCB4IFRvIElucHV0IHkge3g9WzB+MV0oMD1BTEwpLCB5PVsxfjRdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFNUUkVBTSBPTi9PRkYgICAgICAgIDogU2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkZ7eD1bMH4xXSgwPUFMTCl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFZTICAgICAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFZpZGVvIFJvdXRle3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFNUUkVBTSAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkYgU3RhdHVze3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlucHV0IFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCB5ICAgICAgICAgICAgICAgIDogU2V0IElucHV0IHggRURJRHt4PVswXSgwPUFMTCksIHk9WzB+MjldfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDoxMDgwUF8yQ0ggICAgICAgICAgMToxMDgwUF82Q0ggICAgICAgICAgMjoxMDgwUF84Q0ggICAgICAgICAgMzoxMDgwUF8zRF8yQ0ggICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgNDoxMDgwUF8zRF82Q0ggICAgICAgNToxMDgwUF8zRF84Q0ggICAgICAgNjo0SzMwSFpfM0RfMkNIICAgICAgNzo0SzMwSFpfM0RfNkNIICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgODo0SzMwSFpfM0RfOENIICAgICAgOTo0SzYwSHpZNDIwXzNEXzJDSCAgMTA6NEs2MEh6WTQyMF8zRF82Q0ggMTE6NEs2MEh6WTQyMF8zRF84Q0ggICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTI6NEs2MEhaXzNEXzJDSCAgICAgMTM6NEs2MEhaXzNEXzZDSCAgICAgMTQ6NEs2MEhaXzNEXzhDSCAgICAgMTU6MTA4MFBfMkNIX0hEUiAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTY6MTA4MFBfNkNIX0hEUiAgICAgMTc6MTA4MFBfOENIX0hEUiAgICAgMTg6MTA4MFBfM0RfMkNIX0hEUiAgMTk6MTA4MFBfM0RfNkNIX0hEUiAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA6MTA4MFBfM0RfOENIX0hEUiAgMjE6NEszMEhaXzNEXzJDSF9IRFIgMjI6NEszMEhaXzNEXzZDSF9IRFIgMjM6NEszMEhaXzNEXzhDSF9IRFIgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjQ6NEs2MEh6WTQyMF8zRF8yQ0hfSERSICAgICAgMjU6NEs2MEh6WTQyMF8zRF82Q0hfSERSICAgICAgMjY6NEs2MEh6WTQyMF8zRF84Q0hfSERSICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjc6NEs2MEhaXzNEXzJDSF9IRFIgICAgICAgICAgMjg6NEs2MEhaXzNEXzZDSF9IRFIgICAgICAgICAgMjk6NEs2MEhaXzNEXzhDSF9IRFIgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCBDWSBPVVR5ICAgICAgICAgIDogQ29weSBPdXRwdXQgeSBFRElEIFRvIElucHV0IHgoVVNFUjEgQlVGKXt4PVswXSgwPUFMTCksIHk9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggRURJRCAgICAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggRURJRCAgSW5kZXh7eD1bMF0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlSIENvZGUgU2V0dXAgQ29tbWFuZDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBTWVMgeHgueXkgICAgICAgICAgICAgIDogU2V0IElSIEN1c3RvbSBDb2Rle3h4PVswMC1GRkhdLHl5PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IFVEIENPREUgeXkuenogICAgIDogU2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdLHl5PVswMC1GRkhdLHp6PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IElOeSBDT0RFIHp6ICAgICAgIDogU2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XSx6ej1bMDAtRkZIXX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBQT1cgeHggICAgICAgICAgICAgICAgIDogU2V0IElSIFBvd2VyIENvZGV7eHg9WzAwLUZGSF19ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBTWVMgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIEN1c3RvbSBDb2RlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IFVEIENPREUgICAgICAgICAgIDogR2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IElOeSBDT0RFICAgICAgICAgIDogR2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBQT1cgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIFBvd2VyIENvZGUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpICg=="}} +{"test_kvm_switching": {"rx": "", "tx": "RVpTIE9VVDEgVlMgSU4xCkVaUyBPVVQxIFZTIElOMgpFWlMgT1VUMSBWUyBJTjMKRVpTIE9VVDEgVlMgSU40Cg=="}} +{"test_kvm_routing": {"rx": "T1VUMSBWUyA0DQpWMDhTDg==", "tx": "RVpHIE9VVDEgVlMK"}} +{"test_kvm_stream": {"rx": "T1VUIDEgU1RSRUFNIE9ODQo=", "tx": "RVpHIE9VVDEgU1RSRUFNCg=="}} +{"test_kvm_complete_workflow": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBIRUxQKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEF6eiAgICAgICAgICAgICAgICAgICAgICAgICAgIDogIEFsbCBDb21tYW5kcyBzdGFydCBieSBQcmVmaXggU3lzdGVtIEFkZHJlc3MgenosIGlmIFswMS05OV0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPVN5c3RlbSBDb250cm9sIFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaSCAgICAgICAgICAgICAgICAgICAgICAgICAgIDogSGVscCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaU1RBICAgICAgICAgICAgICAgICAgICAgICAgIDogU2hvdyBHbG9iYWwgU3lzdGVtIFN0YXR1cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSU1QgICAgICAgICAgICAgICAgICAgICAgIDogUmVzZXQgdG8gRmFjdG9yeSBEZWZhdWx0cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSQlQgICAgICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3Rlcm0gdG8gUmVib290ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBRERSIHh4ICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3RlbSBBZGRyZXNzIHRvIHh4IHt4eD1bMDB+OTldKDAwPVNpbmdsZSl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBVVRPIE9OL09GRiAgICAgICAgICAgICAgIDogU2V0IEF1dG8gU3dpdGNoIE1vZGUgT24vT2ZmICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBRERSICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBBZGRyZXNzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBTVEEgICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBTeXN0ZW0gU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggU0lHIFNUQSAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggU2lnbmFsIFN0YXR1c3t4PVswfjRdKDA9QUxMKX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBVVRPIE1PREUgICAgICAgICAgICAgICAgIDogR2V0IEF1dG8gU3dpdGNoIE1vZGUgU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPU91dHB1dCBTZXR1cCBDb21tYW5kIDogKE5vdGU6b3V0cHV0IG51bWJlcih4KT1IRE1JKHgpLHg9MSkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFZTIElOeSAgICAgICAgICAgICAgIDogU2V0IE91dHB1dCB4IFRvIElucHV0IHkge3g9WzB+MV0oMD1BTEwpLCB5PVsxfjRdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFNUUkVBTSBPTi9PRkYgICAgICAgIDogU2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkZ7eD1bMH4xXSgwPUFMTCl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFZTICAgICAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFZpZGVvIFJvdXRle3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFNUUkVBTSAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkYgU3RhdHVze3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlucHV0IFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCB5ICAgICAgICAgICAgICAgIDogU2V0IElucHV0IHggRURJRHt4PVswXSgwPUFMTCksIHk9WzB+MjldfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDoxMDgwUF8yQ0ggICAgICAgICAgMToxMDgwUF82Q0ggICAgICAgICAgMjoxMDgwUF84Q0ggICAgICAgICAgMzoxMDgwUF8zRF8yQ0ggICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgNDoxMDgwUF8zRF82Q0ggICAgICAgNToxMDgwUF8zRF84Q0ggICAgICAgNjo0SzMwSFpfM0RfMkNIICAgICAgNzo0SzMwSFpfM0RfNkNIICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgODo0SzMwSFpfM0RfOENIICAgICAgOTo0SzYwSHpZNDIwXzNEXzJDSCAgMTA6NEs2MEh6WTQyMF8zRF82Q0ggMTE6NEs2MEh6WTQyMF8zRF84Q0ggICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTI6NEs2MEhaXzNEXzJDSCAgICAgMTM6NEs2MEhaXzNEXzZDSCAgICAgMTQ6NEs2MEhaXzNEXzhDSCAgICAgMTU6MTA4MFBfMkNIX0hEUiAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTY6MTA4MFBfNkNIX0hEUiAgICAgMTc6MTA4MFBfOENIX0hEUiAgICAgMTg6MTA4MFBfM0RfMkNIX0hEUiAgMTk6MTA4MFBfM0RfNkNIX0hEUiAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA6MTA4MFBfM0RfOENIX0hEUiAgMjE6NEszMEhaXzNEXzJDSF9IRFIgMjI6NEszMEhaXzNEXzZDSF9IRFIgMjM6NEszMEhaXzNEXzhDSF9IRFIgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjQ6NEs2MEh6WTQyMF8zRF8yQ0hfSERSICAgICAgMjU6NEs2MEh6WTQyMF8zRF82Q0hfSERSICAgICAgMjY6NEs2MEh6WTQyMF8zRF84Q0hfSERSICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjc6NEs2MEhaXzNEXzJDSF9IRFIgICAgICAgICAgMjg6NEs2MEhaXzNEXzZDSF9IRFIgICAgICAgICAgMjk6NEs2MEhaXzNEXzhDSF9IRFIgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCBDWSBPVVR5ICAgICAgICAgIDogQ29weSBPdXRwdXQgeSBFRElEIFRvIElucHV0IHgoVVNFUjEgQlVGKXt4PVswXSgwPUFMTCksIHk9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggRURJRCAgICAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggRURJRCAgSW5kZXh7eD1bMF0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlSIENvZGUgU2V0dXAgQ29tbWFuZDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBTWVMgeHgueXkgICAgICAgICAgICAgIDogU2V0IElSIEN1c3RvbSBDb2Rle3h4PVswMC1GRkhdLHl5PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IFVEIENPREUgeXkuenogICAgIDogU2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdLHl5PVswMC1GRkhdLHp6PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IElOeSBDT0RFIHp6ICAgICAgIDogU2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XSx6ej1bMDAtRkZIXX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBQT1cgeHggICAgICAgICAgICAgICAgIDogU2V0IElSIFBvd2VyIENvZGV7eHg9WzAwLUZGSF19ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBTWVMgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIEN1c3RvbSBDb2RlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IFVEIENPREUgICAgICAgICAgIDogR2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IElOeSBDT0RFICAgICAgICAgIDogR2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBQT1cgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIFBvd2VyIENvZGUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KT1VUMSBWUyAxDQpWMENTDk9VVDEgVlMgMw0KVjVFUw5PVVQgMSBTVFJFQU0gT04NCg==", "tx": "RVpTVEEKRVpICkVaUyBPVVQxIFZTIElOMQpFWkcgT1VUMSBWUwpFWlMgT1VUMSBWUyBJTjMKRVpHIE9VVDEgVlMKRVpHIE9VVDEgU1RSRUFNCg=="}} +{"test_multiple_rapid_commands": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KT1VUMSBWUyAyDQpWMThTDj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCj0qKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBTVEFUVVMqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKj0NCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCj0gICAgICAgICAgICAgICAgICAgICAgICBTeXN0ZW0gQWRkcmVzcyA9IDAwICAgICAgICAgICBGL1cgVmVyc2lvbiA6IDIuMDMgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0gICBTeXN0ZW0gU2V0dXAgU3RhdHVzICAgICAgICAgICA6ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0gICBSUzIzMiAgICAgICAgICAgICAgICAgICAgICAgICA6IEJhdWQgUmF0ZT0xMTUyMDBicHMsIERhdGE9OGJpdCwgUGFyaXR5PU5vbmUsIFN0b3A9MWJpdCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLT0NCj0qKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKj0NCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCk9VVDEgVlMgMg0KVjE4Uw49PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQo9KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKlN5c3RlbXMgU1RBVFVTKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKio9DQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQo9ICAgICAgICAgICAgICAgICAgICAgICAgU3lzdGVtIEFkZHJlc3MgPSAwMCAgICAgICAgICAgRi9XIFZlcnNpb24gOiAyLjAzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA9DQo9ICAgU3lzdGVtIFNldHVwIFN0YXR1cyAgICAgICAgICAgOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA9DQo9ICAgUlMyMzIgICAgICAgICAgICAgICAgICAgICAgICAgOiBCYXVkIFJhdGU9MTE1MjAwYnBzLCBEYXRhPThiaXQsIFBhcml0eT1Ob25lLCBTdG9wPTFiaXQgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA9DQo9LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS09DQo9KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKio9DQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQpPVVQxIFZTIDINClYxOFMOPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KT1VUMSBWUyAyDQpWMThTDj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCj0qKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBTVEFUVVMqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKj0NCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCj0gICAgICAgICAgICAgICAgICAgICAgICBTeXN0ZW0gQWRkcmVzcyA9IDAwICAgICAgICAgICBGL1cgVmVyc2lvbiA6IDIuMDMgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0gICBTeXN0ZW0gU2V0dXAgU3RhdHVzICAgICAgICAgICA6ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0gICBSUzIzMiAgICAgICAgICAgICAgICAgICAgICAgICA6IEJhdWQgUmF0ZT0xMTUyMDBicHMsIERhdGE9OGJpdCwgUGFyaXR5PU5vbmUsIFN0b3A9MWJpdCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID0NCj0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLT0NCj0qKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKj0NCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCk9VVDEgVlMgMg0KVjE4Uw4=", "tx": "RVpTVEEKRVpTIE9VVDEgVlMgSU4xCkVaUyBPVVQxIFZTIElOMgpFWkcgT1VUMSBWUwpFWlNUQQpFWlMgT1VUMSBWUyBJTjEKRVpTIE9VVDEgVlMgSU4yCkVaRyBPVVQxIFZTCkVaU1RBCkVaUyBPVVQxIFZTIElOMQpFWlMgT1VUMSBWUyBJTjIKRVpHIE9VVDEgVlMKRVpTVEEKRVpTIE9VVDEgVlMgSU4xCkVaUyBPVVQxIFZTIElOMgpFWkcgT1VUMSBWUwpFWlNUQQpFWlMgT1VUMSBWUyBJTjEKRVpTIE9VVDEgVlMgSU4yCkVaRyBPVVQxIFZTCg=="}} +{"test_device_reuse": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpTVEEKRVpTVEEKRVpTVEEK"}} diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..0aae667 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,160 @@ +"""Integration tests demonstrating pytest-reserial recording and replay.""" + +from pathlib import Path +from typing import Any + +import pytest + +from ezcoo_cli.device import Device, DeviceConnectionError +from ezcoo_cli.kvm import KVM +from ezcoo_cli.models import StreamState + +# Integration tests (uses reserial for record/replay) + + +def test_system_status(reserial: Any, mock_device_path: Path) -> None: + """Test EZSTA command.""" + with Device(mock_device_path) as device: + device.write("EZSTA") + lines = list(device.readlines()) + + # Verify we got a response + assert len(lines) > 0 + + # Look for expected content + response_text = "".join(lines) + assert any(keyword in response_text for keyword in ["System Address", "F/W Version", "RS232"]) + + +def test_help_command(reserial: Any, mock_device_path: Path) -> None: + """Test EZH command.""" + with Device(mock_device_path) as device: + device.write("EZH") + lines = list(device.readlines()) + + # Verify we got a response + assert len(lines) > 0 + + # Look for help content + response_text = "".join(lines) + assert any(keyword in response_text for keyword in ["Help", "EZH", "EZSTA", "Commands"]) + + +def test_input_switching(reserial: Any, mock_device_path: Path) -> None: + """Test input switching commands.""" + with Device(mock_device_path) as device: + for input_num in range(1, 5): + device.write(f"EZS OUT1 VS IN{input_num}") + list(device.readlines()) + + +def test_routing_query(reserial: Any, mock_device_path: Path) -> None: + """Test routing query command.""" + with Device(mock_device_path) as device: + device.write("EZG OUT1 VS") + lines = list(device.readlines()) + + # Should get routing information + if lines: + response_text = "".join(lines) + assert "OUT1" in response_text + + +def test_stream_status(reserial: Any, mock_device_path: Path) -> None: + """Test stream status command.""" + with Device(mock_device_path) as device: + device.write("EZG OUT1 STREAM") + lines = list(device.readlines()) + + # Should get stream status + if lines: + response_text = "".join(lines) + assert "OUT1" in response_text or "OUT 1" in response_text + + +# KVM integration tests + + +def test_kvm_system_status(reserial: Any, mock_device_path: Path) -> None: + """Test KVM system status.""" + kvm = KVM(mock_device_path) + status_response = kvm.get_system_status() + + # Verify the parsed response + assert status_response.response.system_address is not None + assert status_response.response.firmware_version is not None + + +def test_kvm_help(reserial: Any, mock_device_path: Path) -> None: + """Test KVM help.""" + kvm = KVM(mock_device_path) + help_response = kvm.get_help() + + # Verify the parsed response + assert help_response.response.total_commands > 0 + assert len(help_response.response.commands) > 0 + + # Check for expected commands + command_names = [cmd.command for cmd in help_response.response.commands] + expected_commands = ["EZH", "EZSTA", "EZS OUTx VS INy"] + for expected in expected_commands: + assert any(expected in cmd for cmd in command_names) + + +def test_kvm_switching(reserial: Any, mock_device_path: Path) -> None: + """Test KVM input switching.""" + kvm = KVM(mock_device_path) + + # Test switching to each input + for input_num in range(1, 5): + kvm.switch_input(input_num) # Should not raise exception + + +def test_kvm_routing(reserial: Any, mock_device_path: Path) -> None: + """Test KVM routing query.""" + kvm = KVM(mock_device_path) + routing_response = kvm.get_output_routing() + + # Verify the parsed response + assert 1 <= routing_response.response.input <= 4 + assert routing_response.response.output == 1 + + +def test_kvm_stream(reserial: Any, mock_device_path: Path) -> None: + """Test KVM stream status.""" + kvm = KVM(mock_device_path) + stream_response = kvm.get_stream_status() + + # Verify the parsed response + assert stream_response.response.output == 1 + assert isinstance(stream_response.response.enabled, bool) + assert stream_response.response.status in [StreamState.ON, StreamState.OFF] + + +# Error scenario tests + + +def test_device_connection_error() -> None: + """Test device connection error.""" + with pytest.raises(DeviceConnectionError): + with Device(Path("/dev/nonexistent")): + pass + + +def test_kvm_error_handling(mock_device_path: Path) -> None: + """Test KVM error handling.""" + kvm = KVM(mock_device_path) + + # Test invalid input numbers + with pytest.raises(ValueError): + kvm.switch_input(0) + + with pytest.raises(ValueError): + kvm.switch_input(5) + + # Test invalid output numbers + with pytest.raises(ValueError): + kvm.get_output_routing(2) + + with pytest.raises(ValueError): + kvm.get_stream_status(2) diff --git a/tests/test_kvm.jsonl b/tests/test_kvm.jsonl new file mode 100644 index 0000000..896c303 --- /dev/null +++ b/tests/test_kvm.jsonl @@ -0,0 +1,8 @@ +{"test_get_system_status": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpTVEEK"}} +{"test_get_help": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBIRUxQKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEF6eiAgICAgICAgICAgICAgICAgICAgICAgICAgIDogIEFsbCBDb21tYW5kcyBzdGFydCBieSBQcmVmaXggU3lzdGVtIEFkZHJlc3MgenosIGlmIFswMS05OV0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPVN5c3RlbSBDb250cm9sIFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaSCAgICAgICAgICAgICAgICAgICAgICAgICAgIDogSGVscCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaU1RBICAgICAgICAgICAgICAgICAgICAgICAgIDogU2hvdyBHbG9iYWwgU3lzdGVtIFN0YXR1cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSU1QgICAgICAgICAgICAgICAgICAgICAgIDogUmVzZXQgdG8gRmFjdG9yeSBEZWZhdWx0cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSQlQgICAgICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3Rlcm0gdG8gUmVib290ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBRERSIHh4ICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3RlbSBBZGRyZXNzIHRvIHh4IHt4eD1bMDB+OTldKDAwPVNpbmdsZSl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBVVRPIE9OL09GRiAgICAgICAgICAgICAgIDogU2V0IEF1dG8gU3dpdGNoIE1vZGUgT24vT2ZmICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBRERSICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBBZGRyZXNzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBTVEEgICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBTeXN0ZW0gU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggU0lHIFNUQSAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggU2lnbmFsIFN0YXR1c3t4PVswfjRdKDA9QUxMKX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBVVRPIE1PREUgICAgICAgICAgICAgICAgIDogR2V0IEF1dG8gU3dpdGNoIE1vZGUgU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPU91dHB1dCBTZXR1cCBDb21tYW5kIDogKE5vdGU6b3V0cHV0IG51bWJlcih4KT1IRE1JKHgpLHg9MSkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFZTIElOeSAgICAgICAgICAgICAgIDogU2V0IE91dHB1dCB4IFRvIElucHV0IHkge3g9WzB+MV0oMD1BTEwpLCB5PVsxfjRdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFNUUkVBTSBPTi9PRkYgICAgICAgIDogU2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkZ7eD1bMH4xXSgwPUFMTCl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFZTICAgICAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFZpZGVvIFJvdXRle3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFNUUkVBTSAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkYgU3RhdHVze3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlucHV0IFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCB5ICAgICAgICAgICAgICAgIDogU2V0IElucHV0IHggRURJRHt4PVswXSgwPUFMTCksIHk9WzB+MjldfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDoxMDgwUF8yQ0ggICAgICAgICAgMToxMDgwUF82Q0ggICAgICAgICAgMjoxMDgwUF84Q0ggICAgICAgICAgMzoxMDgwUF8zRF8yQ0ggICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgNDoxMDgwUF8zRF82Q0ggICAgICAgNToxMDgwUF8zRF84Q0ggICAgICAgNjo0SzMwSFpfM0RfMkNIICAgICAgNzo0SzMwSFpfM0RfNkNIICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgODo0SzMwSFpfM0RfOENIICAgICAgOTo0SzYwSHpZNDIwXzNEXzJDSCAgMTA6NEs2MEh6WTQyMF8zRF82Q0ggMTE6NEs2MEh6WTQyMF8zRF84Q0ggICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTI6NEs2MEhaXzNEXzJDSCAgICAgMTM6NEs2MEhaXzNEXzZDSCAgICAgMTQ6NEs2MEhaXzNEXzhDSCAgICAgMTU6MTA4MFBfMkNIX0hEUiAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTY6MTA4MFBfNkNIX0hEUiAgICAgMTc6MTA4MFBfOENIX0hEUiAgICAgMTg6MTA4MFBfM0RfMkNIX0hEUiAgMTk6MTA4MFBfM0RfNkNIX0hEUiAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA6MTA4MFBfM0RfOENIX0hEUiAgMjE6NEszMEhaXzNEXzJDSF9IRFIgMjI6NEszMEhaXzNEXzZDSF9IRFIgMjM6NEszMEhaXzNEXzhDSF9IRFIgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjQ6NEs2MEh6WTQyMF8zRF8yQ0hfSERSICAgICAgMjU6NEs2MEh6WTQyMF8zRF82Q0hfSERSICAgICAgMjY6NEs2MEh6WTQyMF8zRF84Q0hfSERSICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjc6NEs2MEhaXzNEXzJDSF9IRFIgICAgICAgICAgMjg6NEs2MEhaXzNEXzZDSF9IRFIgICAgICAgICAgMjk6NEs2MEhaXzNEXzhDSF9IRFIgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCBDWSBPVVR5ICAgICAgICAgIDogQ29weSBPdXRwdXQgeSBFRElEIFRvIElucHV0IHgoVVNFUjEgQlVGKXt4PVswXSgwPUFMTCksIHk9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggRURJRCAgICAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggRURJRCAgSW5kZXh7eD1bMF0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlSIENvZGUgU2V0dXAgQ29tbWFuZDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBTWVMgeHgueXkgICAgICAgICAgICAgIDogU2V0IElSIEN1c3RvbSBDb2Rle3h4PVswMC1GRkhdLHl5PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IFVEIENPREUgeXkuenogICAgIDogU2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdLHl5PVswMC1GRkhdLHp6PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IElOeSBDT0RFIHp6ICAgICAgIDogU2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XSx6ej1bMDAtRkZIXX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBQT1cgeHggICAgICAgICAgICAgICAgIDogU2V0IElSIFBvd2VyIENvZGV7eHg9WzAwLUZGSF19ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBTWVMgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIEN1c3RvbSBDb2RlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IFVEIENPREUgICAgICAgICAgIDogR2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IElOeSBDT0RFICAgICAgICAgIDogR2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBQT1cgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIFBvd2VyIENvZGUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpICg=="}} +{"test_switch_input_valid": {"rx": "", "tx": "RVpTIE9VVDEgVlMgSU4xCkVaUyBPVVQxIFZTIElOMgpFWlMgT1VUMSBWUyBJTjMKRVpTIE9VVDEgVlMgSU40Cg=="}} +{"test_get_output_routing": {"rx": "T1VUMSBWUyA0DQpWMDhTDg==", "tx": "RVpHIE9VVDEgVlMK"}} +{"test_get_stream_status": {"rx": "T1VUIDEgU1RSRUFNIE9ODQo=", "tx": "RVpHIE9VVDEgU1RSRUFNCg=="}} +{"test_complete_workflow": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqU3lzdGVtcyBIRUxQKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEF6eiAgICAgICAgICAgICAgICAgICAgICAgICAgIDogIEFsbCBDb21tYW5kcyBzdGFydCBieSBQcmVmaXggU3lzdGVtIEFkZHJlc3MgenosIGlmIFswMS05OV0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPVN5c3RlbSBDb250cm9sIFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaSCAgICAgICAgICAgICAgICAgICAgICAgICAgIDogSGVscCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaU1RBICAgICAgICAgICAgICAgICAgICAgICAgIDogU2hvdyBHbG9iYWwgU3lzdGVtIFN0YXR1cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSU1QgICAgICAgICAgICAgICAgICAgICAgIDogUmVzZXQgdG8gRmFjdG9yeSBEZWZhdWx0cyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBSQlQgICAgICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3Rlcm0gdG8gUmVib290ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBRERSIHh4ICAgICAgICAgICAgICAgICAgIDogU2V0IFN5c3RlbSBBZGRyZXNzIHRvIHh4IHt4eD1bMDB+OTldKDAwPVNpbmdsZSl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBBVVRPIE9OL09GRiAgICAgICAgICAgICAgIDogU2V0IEF1dG8gU3dpdGNoIE1vZGUgT24vT2ZmICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBRERSICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBBZGRyZXNzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBTVEEgICAgICAgICAgICAgICAgICAgICAgIDogR2V0IFN5c3RlbSBTeXN0ZW0gU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggU0lHIFNUQSAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggU2lnbmFsIFN0YXR1c3t4PVswfjRdKDA9QUxMKX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBBVVRPIE1PREUgICAgICAgICAgICAgICAgIDogR2V0IEF1dG8gU3dpdGNoIE1vZGUgU3RhdHVzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPU91dHB1dCBTZXR1cCBDb21tYW5kIDogKE5vdGU6b3V0cHV0IG51bWJlcih4KT1IRE1JKHgpLHg9MSkgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFZTIElOeSAgICAgICAgICAgICAgIDogU2V0IE91dHB1dCB4IFRvIElucHV0IHkge3g9WzB+MV0oMD1BTEwpLCB5PVsxfjRdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBPVVR4IFNUUkVBTSBPTi9PRkYgICAgICAgIDogU2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkZ7eD1bMH4xXSgwPUFMTCl9ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFZTICAgICAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFZpZGVvIFJvdXRle3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBPVVR4IFNUUkVBTSAgICAgICAgICAgICAgIDogR2V0IE91dHB1dCB4IFN0cmVhbSBPTi9PRkYgU3RhdHVze3g9WzB+MV0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlucHV0IFNldHVwIENvbW1hbmRzOiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCB5ICAgICAgICAgICAgICAgIDogU2V0IElucHV0IHggRURJRHt4PVswXSgwPUFMTCksIHk9WzB+MjldfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDoxMDgwUF8yQ0ggICAgICAgICAgMToxMDgwUF82Q0ggICAgICAgICAgMjoxMDgwUF84Q0ggICAgICAgICAgMzoxMDgwUF8zRF8yQ0ggICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgNDoxMDgwUF8zRF82Q0ggICAgICAgNToxMDgwUF8zRF84Q0ggICAgICAgNjo0SzMwSFpfM0RfMkNIICAgICAgNzo0SzMwSFpfM0RfNkNIICAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgODo0SzMwSFpfM0RfOENIICAgICAgOTo0SzYwSHpZNDIwXzNEXzJDSCAgMTA6NEs2MEh6WTQyMF8zRF82Q0ggMTE6NEs2MEh6WTQyMF8zRF84Q0ggICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTI6NEs2MEhaXzNEXzJDSCAgICAgMTM6NEs2MEhaXzNEXzZDSCAgICAgMTQ6NEs2MEhaXzNEXzhDSCAgICAgMTU6MTA4MFBfMkNIX0hEUiAgICAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMTY6MTA4MFBfNkNIX0hEUiAgICAgMTc6MTA4MFBfOENIX0hEUiAgICAgMTg6MTA4MFBfM0RfMkNIX0hEUiAgMTk6MTA4MFBfM0RfNkNIX0hEUiAgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA6MTA4MFBfM0RfOENIX0hEUiAgMjE6NEszMEhaXzNEXzJDSF9IRFIgMjI6NEszMEhaXzNEXzZDSF9IRFIgMjM6NEszMEhaXzNEXzhDSF9IRFIgICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjQ6NEs2MEh6WTQyMF8zRF8yQ0hfSERSICAgICAgMjU6NEs2MEh6WTQyMF8zRF82Q0hfSERSICAgICAgMjY6NEs2MEh6WTQyMF8zRF84Q0hfSERSICAgICAgPQ0KPSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjc6NEs2MEhaXzNEXzJDSF9IRFIgICAgICAgICAgMjg6NEs2MEhaXzNEXzZDSF9IRFIgICAgICAgICAgMjk6NEs2MEhaXzNEXzhDSF9IRFIgICAgICAgICAgPQ0KPSAgIEVaUyBJTnggRURJRCBDWSBPVVR5ICAgICAgICAgIDogQ29weSBPdXRwdXQgeSBFRElEIFRvIElucHV0IHgoVVNFUjEgQlVGKXt4PVswXSgwPUFMTCksIHk9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJTnggRURJRCAgICAgICAgICAgICAgICAgIDogR2V0IElucHV0IHggRURJRCAgSW5kZXh7eD1bMF0oMD1BTEwpfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPUlSIENvZGUgU2V0dXAgQ29tbWFuZDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBTWVMgeHgueXkgICAgICAgICAgICAgIDogU2V0IElSIEN1c3RvbSBDb2Rle3h4PVswMC1GRkhdLHl5PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IFVEIENPREUgeXkuenogICAgIDogU2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdLHl5PVswMC1GRkhdLHp6PVswMC1GRkhdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBPVVR4IElOeSBDT0RFIHp6ICAgICAgIDogU2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XSx6ej1bMDAtRkZIXX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaUyBJUiBQT1cgeHggICAgICAgICAgICAgICAgIDogU2V0IElSIFBvd2VyIENvZGV7eHg9WzAwLUZGSF19ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBTWVMgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIEN1c3RvbSBDb2RlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IFVEIENPREUgICAgICAgICAgIDogR2V0IElSIE9VVHggVXAvRG93biBDb2Rle3g9WzFdfSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBPVVR4IElOeSBDT0RFICAgICAgICAgIDogR2V0IElSIE9VVHggSU55IENvZGV7eD1bMV0seT1bMX40XX0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIEVaRyBJUiBQT1cgICAgICAgICAgICAgICAgICAgIDogR2V0IElSIFBvd2VyIENvZGUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KT1VUMSBWUyAyDQpWMThTDk9VVCAxIFNUUkVBTSBPTg0K", "tx": "RVpTVEEKRVpICkVaUyBPVVQxIFZTIElOMgpFWkcgT1VUMSBWUwpFWkcgT1VUMSBTVFJFQU0K"}} +{"test_set_device_address": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpTVEEKRVpTIEFERFIgMDUKQTA1RVpTIEFERFIgMDAK"}} +{"test_address_in_status_command": {"rx": "PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipTeXN0ZW1zIFNUQVRVUyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KPSAgICAgICAgICAgICAgICAgICAgICAgIFN5c3RlbSBBZGRyZXNzID0gMDAgICAgICAgICAgIEYvVyBWZXJzaW9uIDogMi4wMyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFN5c3RlbSBTZXR1cCBTdGF0dXMgICAgICAgICAgIDogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPSAgIFJTMjMyICAgICAgICAgICAgICAgICAgICAgICAgIDogQmF1ZCBSYXRlPTExNTIwMGJwcywgRGF0YT04Yml0LCBQYXJpdHk9Tm9uZSwgU3RvcD0xYml0ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPQ0KPS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPQ0KPSoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqPQ0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0K", "tx": "RVpTVEEKQTA1RVpTVEEK"}} diff --git a/tests/test_kvm.py b/tests/test_kvm.py new file mode 100644 index 0000000..83a10d1 --- /dev/null +++ b/tests/test_kvm.py @@ -0,0 +1,236 @@ +"""Tests for the high-level KVM class using pytest-reserial.""" + +# pyright: reportPrivateUsage=false + +from pathlib import Path +from typing import Any +from unittest.mock import patch + +import pytest + +from ezcoo_cli.kvm import KVM +from ezcoo_cli.models import StreamState + +# KVM initialization tests + + +def test_kvm_init(mock_device_path: Path, test_baudrate: int, test_timeout: float) -> None: + """Test KVM initialization.""" + kvm = KVM(mock_device_path, test_baudrate, test_timeout) + assert kvm.device_path == mock_device_path + assert kvm.baudrate == test_baudrate + assert kvm.timeout == test_timeout + + +def test_kvm_init_defaults(mock_device_path: Path) -> None: + """Test KVM initialization with default parameters.""" + kvm = KVM(mock_device_path) + assert kvm.device_path == mock_device_path + assert kvm.baudrate == 115200 + assert kvm.timeout == 1.0 + + +# Validation tests + + +def test_switch_input_invalid_input(mock_device_path: Path) -> None: + """Test switch_input with invalid input numbers.""" + kvm = KVM(mock_device_path) + + with pytest.raises(ValueError, match="Input number must be between 1 and 4"): + kvm.switch_input(0) + + with pytest.raises(ValueError, match="Input number must be between 1 and 4"): + kvm.switch_input(5) + + +def test_switch_input_invalid_output(mock_device_path: Path) -> None: + """Test switch_input with invalid output numbers.""" + kvm = KVM(mock_device_path) + + with pytest.raises(ValueError, match="Only output 1 is supported"): + kvm.switch_input(1, output_num=2) + + +def test_get_output_routing_invalid_output(mock_device_path: Path) -> None: + """Test get_output_routing with invalid output number.""" + kvm = KVM(mock_device_path) + + with pytest.raises(ValueError, match="Only output 1 is supported"): + kvm.get_output_routing(output_num=2) + + +def test_get_stream_status_invalid_output(mock_device_path: Path) -> None: + """Test get_stream_status with invalid output number.""" + kvm = KVM(mock_device_path) + + with pytest.raises(ValueError, match="Only output 1 is supported"): + kvm.get_stream_status(output_num=2) + + +# Device interaction tests (uses reserial for record/replay) + + +def test_get_system_status(reserial: Any, mock_device_path: Path) -> None: + """Test get_system_status with recorded traffic.""" + kvm = KVM(mock_device_path) + status_response = kvm.get_system_status() + + assert status_response.response.system_address is not None + assert status_response.response.firmware_version is not None + + +def test_get_help(reserial: Any, mock_device_path: Path) -> None: + """Test get_help with recorded traffic.""" + kvm = KVM(mock_device_path) + help_response = kvm.get_help() + + assert len(help_response.response.commands) > 0 + assert help_response.response.total_commands > 0 + + +def test_switch_input_valid(reserial: Any, mock_device_path: Path) -> None: + """Test switch_input with valid inputs.""" + kvm = KVM(mock_device_path) + + # Test switching to each valid input + for input_num in range(1, 5): + kvm.switch_input(input_num) # Should not raise exception + + +def test_get_output_routing(reserial: Any, mock_device_path: Path) -> None: + """Test get_output_routing with recorded traffic.""" + kvm = KVM(mock_device_path) + routing_response = kvm.get_output_routing() + + assert 1 <= routing_response.response.input <= 4 + assert routing_response.response.output == 1 + + +def test_get_stream_status(reserial: Any, mock_device_path: Path) -> None: + """Test get_stream_status with recorded traffic.""" + kvm = KVM(mock_device_path) + stream_response = kvm.get_stream_status() + + assert stream_response.response.output == 1 + assert isinstance(stream_response.response.enabled, bool) + assert stream_response.response.status in [StreamState.ON, StreamState.OFF] + + +# Address management tests + + +def test_kvm_init_with_address(mock_device_path: Path) -> None: + """Test KVM initialization with custom address.""" + kvm = KVM(mock_device_path, address=5) + assert kvm.address == 5 + + +def test_kvm_init_default_address(mock_device_path: Path) -> None: + """Test KVM initialization with default address.""" + kvm = KVM(mock_device_path) + assert kvm.address == 0 + + +def test_kvm_address_validation(mock_device_path: Path) -> None: + """Test address validation during initialization.""" + # Valid addresses + for addr in [0, 1, 50, 99]: + kvm = KVM(mock_device_path, address=addr) + assert kvm.address == addr + + # Invalid addresses + with pytest.raises(ValueError, match="Address must be between 0 and 99"): + KVM(mock_device_path, address=-1) + + with pytest.raises(ValueError, match="Address must be between 0 and 99"): + KVM(mock_device_path, address=100) + + +def test_address_property_setter(mock_device_path: Path) -> None: + """Test address property setter.""" + kvm = KVM(mock_device_path, address=0) + + # Valid address changes + kvm.address = 5 + assert kvm.address == 5 + + kvm.address = 99 + assert kvm.address == 99 + + # Invalid addresses + with pytest.raises(ValueError, match="Address must be between 0 and 99"): + kvm.address = -1 + + with pytest.raises(ValueError, match="Address must be between 0 and 99"): + kvm.address = 100 + + +def test_set_device_address_validation(mock_device_path: Path) -> None: + """Test set_device_address validation.""" + kvm = KVM(mock_device_path) + + # Invalid addresses + with pytest.raises(ValueError, match="Address must be between 0 and 99"): + kvm.set_device_address(-1) + + with pytest.raises(ValueError, match="Address must be between 0 and 99"): + kvm.set_device_address(100) + + +def test_set_device_address(reserial: Any, mock_device_path: Path) -> None: + """Test set_device_address with recorded traffic. + + This test changes the device address and must restore it even if the test fails. + """ + kvm = KVM(mock_device_path) + + # Read the original address from the device + status_response = kvm.get_system_status() + original_address = status_response.response.system_address + new_address = 5 + + try: + # Change address to 5 + kvm.set_device_address(new_address) + + # Note: The address property is NOT automatically updated + # User must manually update it after changing device address + assert kvm.address == original_address # Still at old address + finally: + # Always restore the original address, even if test fails + # Update KVM instance to use new address to send reset command + kvm.address = new_address + kvm.set_device_address(original_address) + + +def test_address_in_commands(mock_device_path: Path) -> None: + """Test that address prefix is included in commands.""" + # Mock Device to avoid actual serial communication + with patch("ezcoo_cli.kvm.Device") as mock_device_class: + mock_device = mock_device_class.return_value.__enter__.return_value + + # Test with address 0 (no prefix) + kvm = KVM(mock_device_path, address=0) + kvm.switch_input(2) + mock_device.write.assert_called_with("EZS OUT1 VS IN2") + + # Reset mock + mock_device.write.reset_mock() + + # Test with address 5 (A05 prefix) + kvm = KVM(mock_device_path, address=5) + kvm.switch_input(2) + mock_device.write.assert_called_with("A05EZS OUT1 VS IN2") + + +def test_address_prefix_in_command(mock_device_path: Path) -> None: + """Test that address prefix is correctly included in commands.""" + # Mock Device to verify command format without actual device communication + with patch("ezcoo_cli.kvm.Device") as mock_device_class: + mock_device = mock_device_class.return_value.__enter__.return_value + + # Test with address 5 - should include A05 prefix + kvm = KVM(mock_device_path, address=5) + kvm.switch_input(2) + mock_device.write.assert_called_with("A05EZS OUT1 VS IN2") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a50348c --- /dev/null +++ b/uv.lock @@ -0,0 +1,335 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "ezcoo-cli" +version = "0.2.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "pyserial" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-reserial" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.1.3" }, + { name = "pyserial", specifier = ">=3.5" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-reserial", specifier = ">=0.4.2" }, + { name = "ruff", specifier = ">=0.13.2" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyserial" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-reserial" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyserial" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/17/d5498646909b70e866b20e07567cddbec84585feb7745dbb93bb2683ff45/pytest_reserial-0.4.3.tar.gz", hash = "sha256:f097776c62b53aab11e599ba8f70ffae542fe7cd11ce7a2041c57989f41f00d4", size = 12625, upload-time = "2024-12-22T10:55:18.092Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/d4/b4664e36a841b294707302ab6f4b3cd98028c20f8170fd0710ac53af8cc2/pytest_reserial-0.4.3-py3-none-any.whl", hash = "sha256:6521b514f35434555450c27ddc9638af5f203480058655c972090a4f6093ca23", size = 8193, upload-time = "2024-12-22T10:55:13.023Z" }, +] + +[[package]] +name = "ruff" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/df/8d7d8c515d33adfc540e2edf6c6021ea1c5a58a678d8cfce9fae59aabcab/ruff-0.13.2.tar.gz", hash = "sha256:cb12fffd32fb16d32cef4ed16d8c7cdc27ed7c944eaa98d99d01ab7ab0b710ff", size = 5416417, upload-time = "2025-09-25T14:54:09.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/84/5716a7fa4758e41bf70e603e13637c42cfb9dbf7ceb07180211b9bbf75ef/ruff-0.13.2-py3-none-linux_armv6l.whl", hash = "sha256:3796345842b55f033a78285e4f1641078f902020d8450cade03aad01bffd81c3", size = 12343254, upload-time = "2025-09-25T14:53:27.784Z" }, + { url = "https://files.pythonhosted.org/packages/9b/77/c7042582401bb9ac8eff25360e9335e901d7a1c0749a2b28ba4ecb239991/ruff-0.13.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ff7e4dda12e683e9709ac89e2dd436abf31a4d8a8fc3d89656231ed808e231d2", size = 13040891, upload-time = "2025-09-25T14:53:31.38Z" }, + { url = "https://files.pythonhosted.org/packages/c6/15/125a7f76eb295cb34d19c6778e3a82ace33730ad4e6f28d3427e134a02e0/ruff-0.13.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c75e9d2a2fafd1fdd895d0e7e24b44355984affdde1c412a6f6d3f6e16b22d46", size = 12243588, upload-time = "2025-09-25T14:53:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/9e/eb/0093ae04a70f81f8be7fd7ed6456e926b65d238fc122311293d033fdf91e/ruff-0.13.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cceac74e7bbc53ed7d15d1042ffe7b6577bf294611ad90393bf9b2a0f0ec7cb6", size = 12491359, upload-time = "2025-09-25T14:53:35.892Z" }, + { url = "https://files.pythonhosted.org/packages/43/fe/72b525948a6956f07dad4a6f122336b6a05f2e3fd27471cea612349fedb9/ruff-0.13.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae3f469b5465ba6d9721383ae9d49310c19b452a161b57507764d7ef15f4b07", size = 12162486, upload-time = "2025-09-25T14:53:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e3/0fac422bbbfb2ea838023e0d9fcf1f30183d83ab2482800e2cb892d02dfe/ruff-0.13.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f8f9e3cd6714358238cd6626b9d43026ed19c0c018376ac1ef3c3a04ffb42d8", size = 13871203, upload-time = "2025-09-25T14:53:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/6b/82/b721c8e3ec5df6d83ba0e45dcf00892c4f98b325256c42c38ef136496cbf/ruff-0.13.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed79584a8f6cbe2e5d7dbacf7cc1ee29cbdb5df1172e77fbdadc8bb85a1f89", size = 14929635, upload-time = "2025-09-25T14:53:43.953Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/ad56faf6daa507b83079a1ad7a11694b87d61e6bf01c66bd82b466f21821/ruff-0.13.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aed130b2fde049cea2019f55deb939103123cdd191105f97a0599a3e753d61b0", size = 14338783, upload-time = "2025-09-25T14:53:46.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/77/ad1d9156db8f99cd01ee7e29d74b34050e8075a8438e589121fcd25c4b08/ruff-0.13.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1887c230c2c9d65ed1b4e4cfe4d255577ea28b718ae226c348ae68df958191aa", size = 13355322, upload-time = "2025-09-25T14:53:48.164Z" }, + { url = "https://files.pythonhosted.org/packages/64/8b/e87cfca2be6f8b9f41f0bb12dc48c6455e2d66df46fe61bb441a226f1089/ruff-0.13.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bcb10276b69b3cfea3a102ca119ffe5c6ba3901e20e60cf9efb53fa417633c3", size = 13354427, upload-time = "2025-09-25T14:53:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/7f/df/bf382f3fbead082a575edb860897287f42b1b3c694bafa16bc9904c11ed3/ruff-0.13.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:afa721017aa55a555b2ff7944816587f1cb813c2c0a882d158f59b832da1660d", size = 13537637, upload-time = "2025-09-25T14:53:52.887Z" }, + { url = "https://files.pythonhosted.org/packages/51/70/1fb7a7c8a6fc8bd15636288a46e209e81913b87988f26e1913d0851e54f4/ruff-0.13.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dbc875cf3720c64b3990fef8939334e74cb0ca65b8dbc61d1f439201a38101b", size = 12340025, upload-time = "2025-09-25T14:53:54.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/27/1e5b3f1c23ca5dd4106d9d580e5c13d9acb70288bff614b3d7b638378cc9/ruff-0.13.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939a1b2a960e9742e9a347e5bbc9b3c3d2c716f86c6ae273d9cbd64f193f22", size = 12133449, upload-time = "2025-09-25T14:53:57.089Z" }, + { url = "https://files.pythonhosted.org/packages/2d/09/b92a5ccee289f11ab128df57d5911224197d8d55ef3bd2043534ff72ca54/ruff-0.13.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:50e2d52acb8de3804fc5f6e2fa3ae9bdc6812410a9e46837e673ad1f90a18736", size = 13051369, upload-time = "2025-09-25T14:53:59.124Z" }, + { url = "https://files.pythonhosted.org/packages/89/99/26c9d1c7d8150f45e346dc045cc49f23e961efceb4a70c47dea0960dea9a/ruff-0.13.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3196bc13ab2110c176b9a4ae5ff7ab676faaa1964b330a1383ba20e1e19645f2", size = 13523644, upload-time = "2025-09-25T14:54:01.622Z" }, + { url = "https://files.pythonhosted.org/packages/f7/00/e7f1501e81e8ec290e79527827af1d88f541d8d26151751b46108978dade/ruff-0.13.2-py3-none-win32.whl", hash = "sha256:7c2a0b7c1e87795fec3404a485096bcd790216c7c146a922d121d8b9c8f1aaac", size = 12245990, upload-time = "2025-09-25T14:54:03.647Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bd/d9f33a73de84fafd0146c6fba4f497c4565fe8fa8b46874b8e438869abc2/ruff-0.13.2-py3-none-win_amd64.whl", hash = "sha256:17d95fb32218357c89355f6f6f9a804133e404fc1f65694372e02a557edf8585", size = 13324004, upload-time = "2025-09-25T14:54:06.05Z" }, + { url = "https://files.pythonhosted.org/packages/c3/12/28fa2f597a605884deb0f65c1b1ae05111051b2a7030f5d8a4ff7f4599ba/ruff-0.13.2-py3-none-win_arm64.whl", hash = "sha256:da711b14c530412c827219312b7d7fbb4877fb31150083add7e8c5336549cea7", size = 12484437, upload-time = "2025-09-25T14:54:08.022Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +]