diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index 8c4b5bcbe..99bfa4e96 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -17,7 +17,7 @@ jobs:
permissions: {}
name: Package
runs-on: ubuntu-22.04
- timeout-minutes: 5
+ timeout-minutes: 10
steps:
- name: Checkout source
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index d6deb1ab0..2aaa55b25 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -10,7 +10,7 @@ jobs:
testrun_baseline:
permissions: {}
name: Baseline
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- name: Checkout source
@@ -29,7 +29,7 @@ jobs:
testrun_api:
permissions: {}
name: API
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- name: Checkout source
@@ -58,7 +58,7 @@ jobs:
testrun_unit:
permissions: {}
name: Unit
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- name: Checkout source
@@ -73,9 +73,40 @@ jobs:
shell: bash {0}
run: cmd/build
timeout-minutes: 10
- - name: Run tests
+ - name: Run tests for conn module
+ shell: bash {0}
+ run: bash testing/unit/run_test_module.sh conn captures ethtool output
+ - name: Run tests for dns module
+ shell: bash {0}
+ run: bash testing/unit/run_test_module.sh dns captures reports output
+ - name: Run tests for ntp module
+ shell: bash {0}
+ run: bash testing/unit/run_test_module.sh ntp captures reports output
+ - name: Run tests for protocol module
+ shell: bash {0}
+ run: bash testing/unit/run_test_module.sh protocol captures output
+ - name: Run tests for services module
shell: bash {0}
- run: bash testing/unit/run.sh
+ run: bash testing/unit/run_test_module.sh services reports results output
+ - name: Run tests for tls module
+ shell: bash {0}
+ run: bash testing/unit/run_test_module.sh tls captures certAuth certs reports root_certs output
+ - name: Run tests for risk profiles
+ shell: bash {0}
+ run: bash testing/unit/run_report_test.sh testing/unit/risk_profile/risk_profile_test.py
+ - name: Run tests for reports
+ shell: bash {0}
+ run: bash testing/unit/run_report_test.sh testing/unit/report/report_test.py
+ - name: Archive HTML reports for modules
+ if: ${{ always() }}
+ run: sudo tar --exclude-vcs -czf html_reports.tgz testing/unit/report/output/
+ - name: Upload HTML reports
+ uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
+ if: ${{ always() }}
+ with:
+ if-no-files-found: error
+ name: html-reports_${{ github.run_id }}
+ path: html_reports.tgz
pylint:
permissions: {}
@@ -98,7 +129,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
with:
- node-version: 18.18.0
+ node-version: 18.19.0
- name: Install Chromium Browser
run: sudo apt install chromium-browser
- name: Install dependencies
@@ -121,7 +152,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
with:
- node-version: 18.18.0
+ node-version: 18.19.0
- name: Install dependencies
run: npm install && npm ci
working-directory: ./modules/ui
diff --git a/README.md b/README.md
index 23fd843ca..ce9602637 100644
--- a/README.md
+++ b/README.md
@@ -4,84 +4,93 @@
[![CodeQL](https://github.com/google/testrun/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)](https://github.com/google/testrun/actions/workflows/github-code-scanning/codeql)
[![Testrun test suite](https://github.com/google/testrun/actions/workflows/testing.yml/badge.svg?branch=main&event=push)](https://github.com/google/testrun/actions/workflows/testing.yml)
-## Introduction :wave:
-Testrun automates specific test cases to verify network and security functionality in IoT devices. It is an open source tool which allows manufacturers of IP capable devices to test their devices for the purposes of Device Qualification within the BOS program.
+# Introduction :wave:
-## Motivation :bulb:
-Without tools like Testrun, test labs and engineers may need to maintain a large and complex network coupled with dynamic configuration files and constant software updates. The major issues which can and should be solved are:
- 1) The complexity of managing a testing network
- 2) The time required to perform testing of network functionality
- 3) The accuracy and consistency of testing network functionality
+Testrun automates specific test cases to verify network and security functionality in IoT devices. It's an open-source tool that manufacturers use to test their IP-capable devices for the purpose of device qualification within Google's Building Operating System (BOS) program.
-## How it works :triangular_ruler:
-Testrun creates an isolated and controlled network environment on a linux machine. This removes the necessity for complex hardware, advanced knowledge and networking experience whilst enabling test engineers to validate device behaviour against Google’s Building Operating System requirements.
+# Motivation :bulb:
-Two modes are supported by Testrun:
+Test labs and engineers often need to maintain a large and complex network coupled with dynamic configuration files and constant software updates. Testrun helps address major issues like:
-
-
- Automated testing
-
+- The complexity of managing a testing network
+- The time required to perform testing of network functionality
+- The accuracy and consistency of testing network functionality
-Once the device has become operational (steady state), automated testing of the DUT (device under test) will begin. Containerized test modules will then execute against the device, one module at a time. Once all test modules have been executed, a report will be produced - presenting the results.
-
+# How it works :triangular_ruler:
-
+Testrun creates an isolated and controlled network environment on a Linux machine. This removes the necessity for complex hardware, advanced knowledge, and networking experience while enabling test engineers to validate device behavior against Google's BOS requirements.
-
- Lab network
-
+Testrun supports two modes: automated testing and lab network.
-When manual testing or configuration changes are required, Testrun will provide the network and some tools to assist an engineer performing the additional testing. This reduces the need to maintain a separate but identical lab network. Testrun will take care of packet captures and logs for each network service for further debugging.
+## Automated testing
-
+Automated testing of the device under test (DUT) begins once the device is operational (steady state). Containerized test modules execute against the device one module at a time. Testrun produces a report with the results after all modules are executed.
-## Minimum requirements :computer:
-### Hardware
- - PC running Ubuntu LTS 20.04, 22.04 or 24.04 (laptop or desktop)
- - 2x USB ethernet adapter (One may be built in ethernet)
- - Internet connection
-### Software
-- Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository)
-### Device under test (DUT)
- - DHCP client - The device must be able to obtain an IP address via DHCP
+## Lab network
-## Get started ▶️
-Once you have met the hardware and software requirements, you can get started with Testrun by following the [Get started guide](docs/get_started.md). Further docs are available in the [docs directory](docs)
+Testrun provides the network and assistive tools for engineers when manual testing or configuration changes are required, reducing the need to maintain a separate but identical lab network. Testrun handles packet captures and logs for each network service for further debugging.
-## Roadmap :chart_with_upwards_trend:
-Testrun will constantly evolve to further support end-users by automating device network behaviour against industry standards. For further information on upcoming features, check out the [Roadmap](docs/roadmap.pdf).
+# Minimum requirements :computer:
-## Accessibility :busts_in_silhouette:
-We are proud of our tool and strive to provide an enjoyable experience for all of our users. Testrun goes through rigorous accessibility testing at each release. You can read more about [Google and Accessibility here](https://www.google.co.uk/accessibility). You are welcome to submit a new issue and provide feedback on our implementations. To find out how Testrun implements accessibility features, you can view a [short video here](docs/ui/accessibility.mp4).
+## Hardware
-## Issue reporting :triangular_flag_on_post:
-If the application has come across a problem at any point during setup or use, please raise an issue under the [issues tab](https://github.com/google/testrun/issues). Issue templates exist for both bug reports and feature requests. If neither of these are appropriate for your issue, raise a blank issue instead.
+- PC running Ubuntu LTS 20.04, 22.04, or 24.04 (laptop or desktop)
+- 2x USB Ethernet adapter (one may be built-in Ethernet)
+- Internet connection
-## Contributing :keyboard:
-The contributing requirements can be found in [CONTRIBUTING.md](CONTRIBUTING.md). In short, checkout the [Google CLA](https://cla.developers.google.com/) site to get started. After that, check out our [developer documentation](docs/dev/README.md).
+## Software
-## FAQ :raising_hand:
-1) I have an issue whilst installing/upgrading Testrun, what do I do?
+Testrun requires Docker. Refer to the [installation guide](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) for more information.
- Sometimes, issues may arise when installing or upgrading Testrun - this may happen due to one of many reasons due to the nature of the application. However, most of the time, it can be resolved by following a full Testrun re-install by using these commands:
- - ```sudo docker system prune -a```
- - ```sudo apt install ./testrun-*.deb```
+## Device under test (DUT)
-2) What device networking functionality is validated by Testrun?
+The DUT must be able to obtain an IP address via DHCP.
- Best practices and requirements for IoT devices are constantly changing due to technological advances and discovery of vulnerabilities.
- The current expectations for IoT devices on Google deployments can be found in the [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements).
- Testrun aims to automate as much of the Application Security Requirements as possible.
+# Get started :arrow_forward:
-3) What services are provided on the virtual network?
+Once you meet the hardware and software requirements, follow the Testrun [Get started guide](/docs/get_started.md). Additional guidance is available in the [docs directory](/docs).
- The following are network services that are containerized and accessible to the device under test though are likely to change over time:
- - DHCP in failover configuration with internet connectivity
- - IPv6 SLAAC
- - DNS
- - NTPv4
+# Roadmap :chart_with_upwards_trend:
-4) Can I run Testrun on a virtual machine?
+Testrun continually evolves to further support end users by automating device network behavior against industry standards. For information on upcoming features, check out the [Roadmap](/docs/roadmap.pdf).
- Testrun can be virtualized if the 2x ethernet adapters are passed through to a VirtualBox VM as a USB device rather than managed network adapters. You can view the guide to working on a [virtual machine here](docs/virtual_machine.md).
+# Accessibility :busts_in_silhouette:
+
+We're proud of our tool and strive to provide an enjoyable experience for everyone. Testrun goes through rigorous accessibility testing at each release. Download the [Testrun: Accessible features](https://github.com/google/testrun/raw/refs/heads/main/docs/ui/accessibility.mp4) video to learn more.You're welcome to [submit a new issue](https://github.com/google/testrun/issues) and provide feedback on our implementations. To learn more about Google's [Belonging initiative](https://www.google.co.uk/accessibility) and their approach to accessibility, visit their site.
+
+# Issue reporting :triangular_flag_on_post:
+
+If you encounter a problem during setup or use, raise an issue under the [Issues tab](https://github.com/google/testrun/issues). Issue templates exist for both bug reports and feature requests. If neither of these apply, raise a blank issue instead.
+
+# Contributing :keyboard:
+
+We strongly encourage contributions from the community. Review the requirements on the ["How to Contribute" page](CONTRIBUTING.md), then follow the [developer guidelines](/docs/dev/README.md).
+
+# FAQ :raising_hand:
+
+#### 1. What should I do if I have an issue while installing or upgrading Testrun?
+
+ You can resolve most issues by reinstalling Testrun using these commands:
+- `sudo docker system prune -a`
+- `sudo apt install ./testrun-*.deb`
+
+If this doesn't resolve the problem, [raise an issue](https://github.com/google/testrun/issues).
+
+#### 2. What device networking functionality does Testrun validate?
+
+Best practices and requirements for IoT devices change often due to technological advances and discovery of vulnerabilities. You can find the current expectations for IoT devices on Google deployments in the [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements). Testrun aims to automate as much of the Application Security Requirements as possible.
+
+#### 3. What services are provided on the virtual network?
+
+The following network services are containerized and accessible to the DUT:
+
+- DHCP in failover configuration with internet connectivity
+- IPv6 SLAAC
+- DNS
+- NTPv4
+
+Note that this list is likely to change over time.
+
+#### 4. Can I run Testrun on a virtual machine?
+
+Testrun can be virtualized if the 2x Ethernet adapters are passed through to a VirtualBox VM as a USB device rather than managed network adapters. Visit the [virtual machine guide](/docs/virtual_machine.md) for additional details.
\ No newline at end of file
diff --git a/cmd/build b/cmd/build
index d3294a681..8ecccb5ef 100755
--- a/cmd/build
+++ b/cmd/build
@@ -60,11 +60,10 @@ fi
# Build network modules
echo Building network modules
-mkdir -p build/network
for dir in modules/network/* ; do
module=$(basename $dir)
echo Building network module $module...
- if docker build -f modules/network/$module/$module.Dockerfile -t test-run/$module . ; then
+ if docker build -f modules/network/$module/$module.Dockerfile -t testrun/$module . ; then
echo Successfully built container for network $module
else
echo An error occured whilst building container for network module $module
@@ -74,11 +73,10 @@ done
# Build validators
echo Building network validators
-mkdir -p build/devices
for dir in modules/devices/* ; do
module=$(basename $dir)
echo Building validator module $module...
- if docker build -f modules/devices/$module/$module.Dockerfile -t test-run/$module . ; then
+ if docker build -f modules/devices/$module/$module.Dockerfile -t testrun/$module . ; then
echo Successfully built container for device module $module
else
echo An error occured whilst building container for device module $module
@@ -88,11 +86,10 @@ done
# Build test modules
echo Building test modules
-mkdir -p build/test
for dir in modules/test/* ; do
module=$(basename $dir)
echo Building test module $module...
- if docker build -f modules/test/$module/$module.Dockerfile -t test-run/$module-test . ; then
+ if docker build -f modules/test/$module/$module.Dockerfile -t testrun/$module-test . ; then
echo Successfully built container for test module $module
else
echo An error occured whilst building container for test module $module
diff --git a/cmd/install b/cmd/install
index c350a969f..906550abf 100755
--- a/cmd/install
+++ b/cmd/install
@@ -68,15 +68,13 @@ deactivate
cmd/build
# Create local folders
-mkdir -p local/devices
-mkdir -p local/root_certs
-mkdir -p local/risk_profiles
+mkdir -p local/{devices,root_certs,risk_profiles}
# Set file permissions on local
# This does not work on GitHub actions
if logname ; then
USER_NAME=$(logname)
- sudo chown -R "$USER_NAME" local
+ sudo chown -R "$USER_NAME" local || true
fi
echo Finished installing Testrun
diff --git a/cmd/prune b/cmd/prune
index 9f471897d..4c2796460 100755
--- a/cmd/prune
+++ b/cmd/prune
@@ -25,17 +25,16 @@ fi
# Remove docker images
echo Removing docker images
-docker_images=$(sudo docker images --filter=reference="test-run/*" -q)
+docker_images=$(sudo docker images --filter=reference="testrun/*" -q)
if [ -z "$docker_images" ]; then
echo No docker images to delete
else
- sudo docker rmi $docker_images > /dev/null
+ sudo docker rmi $docker_images
fi
# Remove docker networks
echo Removing docker networks
-sudo docker network rm endev0 > /dev/null
-# Private network not used, add cleanup
-# back in if/when implemented
-#sudo docker network rm tr-private-net > /dev/null
\ No newline at end of file
+sudo docker network rm endev0 || true
+
+echo Successfully pruned Testrun resources
diff --git a/docs/README.md b/docs/README.md
index 5f055dbb9..efea0d413 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,21 +1,19 @@
+# Contents
-## Contents
+- [Get started](/docs/get_started.md)
+ - [Run on a virtual machine](/docs/virtual_machine.md)
+- [Network](/docs/network/README.md)
+ - [Network addresses](/docs/network/addresses.md)
+ - [Add a new network service](/docs/network/add_new_service.md)
+- [Testing](/docs/test/README.md)
+ - [Test modules](/docs/test/modules.md)
+ - [Test results](/docs/test/statuses.md)
+- [Developer guidelines](/docs/dev/README.md)
+- [Accessibility](/docs/ui/accessibility.md)
+- [Roadmap](/docs/roadmap.pdf)
- - [Get Started](get_started.md)
- - [Network](network/README.md)
- - [Network Overview](network/README.md)
- - [How to identify network interfaces](network/identify_interfaces.md)
- - [Addresses](network/addresses.md)
- - [Add a new network service](network/add_new_service.md)
- - [Testing](test/README.md)
- - [Test modules](test/modules.md)
- - [Test statuses](test/statuses.md)
- - [Development](dev/README.md)
- - [Running on a virtual machine](virtual_machine.md)
- - [Accessibility](ui/accessibility.mp4)
- - [Roadmap](roadmap.pdf)
+# Something missing?
-## Something missing?
-If you feel there is some documentation that you would find useful, or have found an issue with existing documentation, please raise an issue on GitHub by navigating [here](https://github.com/google/testrun/issues/new/choose)
\ No newline at end of file
+To request additional documentation or report an issue with existing resources, visit [the Issues tab](https://github.com/google/testrun/issues/new/choose).
diff --git a/docs/configure_device.md b/docs/configure_device.md
deleted file mode 100644
index 1db7155be..000000000
--- a/docs/configure_device.md
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-## Device Configuration (Deprecated)
-
-The device configuration file allows you to customize the testing behavior for a specific device. This file is located at `local/devices/{Device Name}/device_config.json`. Below is an overview of how to configure the device tests.
-
-## Device Information
-
-The device information section includes the manufacturer, model, and MAC address of the device. These details help identify the specific device being tested.
-
-## Test Modules
-
-Test modules are groups of tests that can be enabled or disabled as needed. You can choose which test modules to run on your device.
-
-### Enabling and Disabling Test Modules
-
-To enable or disable a test module, modify the `enabled` field within the respective module. Setting it to `true` enables the module, while setting it to `false` disables the module.
-
-## Customizing the Device Configuration
-
-To customize the device configuration for your specific device, follow these steps:
-
-1. Copy the default configuration file provided in the `resources/devices/template` folder.
- - Create a new folder for your device under `local/devices` directory.
- - Copy the `device_config.json` file from `resources/devices/template` to the newly created device folder.
-
-This ensures that you have a copy of the default configuration file, which you can then modify for your specific device.
-
-> Note: Ensure that the device configuration file is properly formatted, and the changes made align with the intended test behavior. Incorrect settings or syntax may lead to unexpected results during testing.
-
-If you encounter any issues or need assistance with the device configuration, refer to the Testrun documentation or ask a question on the Issues page.
diff --git a/docs/dev/README.md b/docs/dev/README.md
index f11b1b092..076cb827c 100644
--- a/docs/dev/README.md
+++ b/docs/dev/README.md
@@ -1,25 +1,35 @@
-## Developer docs
+# Developer guidelines
-## Table of Contents
-1) General guidelines (this page)
-2) [Code quality](code_quality.md)
+## How to contribute
-## General guidelines
-As an open source project, we absolutely encourage contributions from the community to help Testrun remain an expanding but stable product. However, before contributing there are a number of things to take into consideration.
+As an open source project, we encourage contributions from the community to help Testrun remain an expanding but stable product. To contribute, follow the steps below:
-1) [Sign the Google CLA](https://cla.developers.google.com/): Whether you are an individual or contributing on behalf of your organisation, you must be covered by a Google CLA.
+1. Sign the [Google Contributor License Agreement (CLA)](https://cla.developers.google.com/).
+ - Whether you're an individual or contributing on behalf of your organization, you must be covered by a Google CLA.
-2) Determine the scope of your contribution
+1. Determine the scope of your contribution.
+ - Keep it simple. Your contribution is more likely to be accepted if you change fewer files.
+ - Ensure your pull request addresses one thing, such as a bug fix, dependency issue, or new framework capability.
- - Your contribution is more likely to be accepted if fewer files are changed (keep it simple)
- - Are you going to be fixing a bug, dependency issue or a new framework capability? Whatever it is, ensure your pull request fixes or changes just one thing.
+1. Reach out to the core maintainers at [testrun-team@googlegroups.com](mailto:testrun-team@googlegroups.com).
+ - They can provide confirmation that your proposed changes meet our objectives and align with Testrun principles, making them more likely to be accepted.
-3) Get in touch to discuss whether your proposed changes are likely to be accepted
+1. Fork Testrun and get developing.
+ - We aim to provide thorough and clear developer documentation to help you contribute successfully.
- - It is best to get the opinion from the core maintainers whether your proposed changes meet our objectives and align with Testrun principles.
+## Code quality
-4) Fork Testrun and get developing
+To ensure code quality, use the appropriate style guide when developing code for Testrun:
- - We aim to provide thorough and easy to ready developer documentation to help you contribute successfully.
\ No newline at end of file
+- [Python](https://google.github.io/styleguide/pyguide.html)
+- [Angular](https://google.github.io/styleguide/angularjs-google-style.html)
+- [Shell](https://google.github.io/styleguide/shellguide.html)
+- [HTML/CSS](https://google.github.io/styleguide/htmlcssguide.html)
+- [JSON](https://google.github.io/styleguide/jsoncstyleguide.xml)
+- [Markdown](https://google.github.io/styleguide/docguide/style.html)
+
+## Automated actions
+
+The current code base has zero code lint issues. To maintain this, all lint checks are enforced on pull requests to dev and main. You should ensure these lint checks pass before marking your pull requests as Ready for review.
diff --git a/docs/dev/code_quality.md b/docs/dev/code_quality.md
deleted file mode 100644
index 47eabcf95..000000000
--- a/docs/dev/code_quality.md
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-## Code quality
-
-Whilst developing code for Testrun, there are some style guides that you should follow.
-
- - Python: https://google.github.io/styleguide/pyguide.html
- - Angular: https://google.github.io/styleguide/angularjs-google-style.html
- - Shell: https://google.github.io/styleguide/shellguide.html
- - HTML/CSS: https://google.github.io/styleguide/htmlcssguide.html
- - JSON: https://google.github.io/styleguide/jsoncstyleguide.xml
- - Markdown: https://google.github.io/styleguide/docguide/style.html
-
-### Automated actions
-
-The current code base has been able to achieve 0 code lint issues. To maintain this, all lint checks are enforced on pull requests to dev and main. Please ensure that these lint checks are passing before marking your pull requests as 'Ready for review'.
\ No newline at end of file
diff --git a/docs/get_started.md b/docs/get_started.md
index f9a2aa2f8..dbe5eab43 100644
--- a/docs/get_started.md
+++ b/docs/get_started.md
@@ -1,123 +1,121 @@
+# Get started
-## Getting Started
+This page covers the following topics:
-It is recommended that you run Testrun on a standalone machine running a fresh install of Ubuntu 20.04, 22.04 or 24.04 LTS (laptop or desktop).
+- [Prerequisites](#prerequisites)
+- [Installation](#installation)
+- [Testing](#testing)
+- [Troubleshooting](#troubleshooting)
+- [Review the report](#review-the-report)
+- [Uninstall](#uninstall)
-## Prerequisites
+# Prerequisites
-### Hardware
+We recommend that you run Testrun on a stand-alone machine that has a fresh install of Ubuntu 20.04, 22.04, or 24.04 LTS (laptop or desktop).
-Before starting with Testrun, ensure you have the following hardware:
+## Hardware
-- PC running Ubuntu LTS (laptop or desktop)
-- 2x USB Ethernet adapter (one may be a built-in Ethernet port)
-- Internet connection
+Before you start, ensure you have the following hardware:
-![Visual representation of setup](setup/visual.png)
+- PC running Ubuntu LTS (laptop or desktop)
+- 2x USB Ethernet adapter (one may be a built-in Ethernet port)
+- Internet connection
-**NOTE: Running in a virtual machine? Checkout the virtual machine documentation [here](/docs/virtual_machine.md).**
+![Required hardware for Testrun](/docs/ui/getstarted--2dn8vrzsspe.png)
-### Software
+Note: If you're using Testrun in a virtual machine, follow the steps on the [Virtual machine page](/docs/virtual_machine.md).
-Ensure the following software is installed on your Ubuntu LTS PC:
-- Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository)
-- System dependencies (These will be installed automatically when installing Testrun if not already installed):
- - Python3-dev
- - Python3-venv
- - Openvswitch Common
- - Openvswitch Switch
- - Build Essential
- - Net Tools
- - Ethtool
+## Software
-### Device
-Any device with an ethernet connection, and support for IPv4 DHCP can be tested.
+Install Docker on your Ubuntu LTS PC using [the installation guide](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository). The following system dependencies install automatically when you install Testrun:
-However, to achieve a compliant test outcome, your device must be configured correctly and implement the required security features. These standards are outlined in the [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements). but further detail is available in [documentation for each test module](/docs/test/modules.md).
+- Python3-dev
+- Python3-venv
+- Openvswitch Common
+- Openvswitch Switch
+- Build Essential
+- Net Tools
+- Ethtool
-## Installation
+## Device
-1. Download the latest version of the Testrun installer from the [releases page](https://github.com/google/testrun/releases)
+You can test any device with an Ethernet connection and support for IPv4 DHCP. However, to achieve a compliant test outcome, you must configure your device correctly and implement the required security features. The [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements) outlines these standards. Additional details are available on the [Test modules page](/docs/test/modules.md).
-2. Open a terminal and navigate to location of the Testrun installer (most likely your downloads folder)
+# Installation
-3. Install the package using ``sudo apt install ./testrun*.deb``
+Follow these steps to install Testrun:
- - Testrun will be installed under the /usr/local/testrun directory.
- - Testing data will be available in the ``local/devices/{device}/reports`` folders
+1. Download the latest version of the Testrun installer from the [Releases page](https://github.com/google/testrun/releases).
+2. Open a terminal and navigate to the location of the Testrun installer (most likely your Downloads folder).
+3. Install the package using `sudo apt install ./testrun*.deb`
- **NOTE: Local CA certificates should be uploaded within Testrun to run TLS server testing**
+Testrun installs under the `/usr/local/testrun` directory. Testing data is available in the `local/devices/{device}/reports` folders.
- ![Installing Testrun in terminal window](setup/install.gif)
+Note: Local CA certificates should be uploaded within Testrun to run TLS server testing.
-## Start Testrun
-
-1. Attach network interfaces:
- - Connect one USB Ethernet adapter to the internet source (e.g., router or switch) using an ethernet cable.
- - Connect the other USB Ethernet adapter directly to the IoT device you want to test using an ethernet cable.
-
- Some things to remember:
- - Both adapters should be disabled in the host system (IPv4, IPv6 and general). You can do this by going to Settings > Network
- - The device under test should be powered off until prompted
- - Struggling to identify the correct interfaces? See [this guide](network/identify_interfaces.md).
-
-2. Start Testrun.
-
-Start Testrun with the command `sudo testrun`
-
- - To run Testrun in network-only mode (without running any tests), use the `--net-only` option.
-
- - To run Testrun with just one interface (connected to the device), use the ``--single-intf`` option.
+![Terminal during install](/docs/setup/install.gif)
-## Test Your Device
+# Testing
-1. Once Testrun has started, open your browser to http://localhost:8080.
-
-2. Configure your network interfaces under the settings menu - located in the top right corner of the application. Settings can be changed at any time.
-
- ![](/docs/ui/settings_icon.png)
-
-3. Navigate to the device repository icon to add a new device for testing.
+## Start Testrun
- ![](/docs/ui/device_icon.png)
+Follow these steps to start Testrun:
+1. Attach the network interfaces.
+ - Connect one USB Ethernet adapter to the internet source (e.g., router or switch) using an Ethernet cable.
+ - Connect the other USB Ethernet adapter directly to the IoT device you want to test using an Ethernet cable.
-4. Click the button 'Add Device'.
+Notes:
-5. Enter the MAC address, manufacturer name and model number.
+- Disable both adapters in the host system (IPv4, IPv6, and general) by opening **Settings**, then **Network**.
+- Keep the DUT powered off until prompted.
-6. Select the test modules you wish to enable for this device (Hint: All are required for qualification purposes) and click save.
+1. Start Testrun with the command `sudo testrun`
+ - To run Testrun in network-only mode (without running any tests), use the `--net-only` option.
+ - To run Testrun with just one interface (connected to the device), use the `--single-intf` option.
-7. Navigate to the Testrun progress icon and click the button 'Start New Testrun'.
- ![](/docs/ui/progress_icon.png)
+## Test your device
-8. Select the device you would like to test.
+Follow these steps to test your IoT device:
-9. Enter the version number of the firmware running on the device.
+1. Open Testrun by navigating to [http://localhost:8080](http://localhost:8080/) in your browser.
+2. Select the **Settings** menu in the top-right corner, then select your network interfaces. You can change the settings at any time.
+ ![Settings menu button](/docs/ui/getstarted--7cfvdpdnc5o.png)
-10. Click 'Start Testrun'
+3. Select the **device repository** icon on the left panel to add a new device for testing.
+ ![Device repository button](/docs/ui/getstarted--q5uw26tfod.png)
- - During testing, if you would like to stop Testrun, click 'Stop' next to the test name.
+4. Select the **Add Device** button.
+5. Enter the MAC address, manufacturer name, and model number.
+6. Select the test modules you want to enable for this device.
+Note: For qualification purposes, you must select all.
+7. Select **Save**.
+8. Select the Testrun progress icon, then select the **Testing** button.![Testing button](/docs/ui/getstarted--w09wecsry3.png)
-11. Once the notification 'Waiting for Device' appears, power on the device under test.
+9. Select the device you want to test.
+10. Enter the version number of the firmware running on the device.
+11. Select **Start Testrun**.
+- If you need to stop Testrun during testing, select **Stop** next to the test name.
+12. Once the Waiting for Device notification appears, power on the device under test. A report appears under the Reports icon once the test sequence is complete.
+ ![Reports button](/docs/ui/getstarted--m4si1otdu5d.png)
-12. On completion of the test sequence, a report will appear under the history icon.
+# Troubleshooting
- ![](/docs/ui/history_icon.png)
+If you encounter any issues, try the following:
-# Troubleshooting
+- Ensure that your computer meets all hardware and software prerequisites.
+- Verify that the network interfaces are connected correctly.
+- Check the configuration settings.
+- Refer to the [Testrun documentation](/docs).
-If you encounter any issues or need assistance, consider the following:
+If you still need assistance, ask a question on the [Issues page](https://github.com/google/testrun/issues).
-- Ensure that all hardware and software prerequisites are met.
-- Verify that the network interfaces are connected correctly.
-- Check the configuration settings.
-- Refer to the Testrun documentation or ask for assistance in the issues page: https://github.com/google/testrun/issues
+# Review the report
-# Reviewing
-Once you have completed a test attempt, you may want to review the test report provided by Testrun. For more information about what Testrun looks for when testing, and what the output means, take a look at the testing documentation: [Testing](/docs/test/README.md).
+Once you complete a test attempt, you can review the test report provided by Testrun. For more information on Testrun requirements and outputs, refer to the [Testing documentation](/docs/test/README.md).
# Uninstall
-To uninstall Testrun, use the built-in dpkg uninstall command to remove Testrun correctly. For Testrun, this would be: ```sudo apt-get remove testrun```.
+
+To uninstall Testrun correctly, use the built-in dpkg uninstall command: `sudo apt-get remove testrun`
\ No newline at end of file
diff --git a/docs/network/README.md b/docs/network/README.md
index 0f97ecd7b..0efb5e3ff 100644
--- a/docs/network/README.md
+++ b/docs/network/README.md
@@ -1,44 +1,41 @@
-## Network Overview
+# Network
-## Table of Contents
-1) Network overview (this page)
-2) [How to identify network interfaces](identify_interfaces.md)
-3) [Addresses](addresses.md)
-4) [Add a new network service](add_new_service.md)
+This page provides an overview of Testrun's network services. Visit these pages for additional information:
-Testrun provides several built-in network services that can be utilized for testing purposes. These services are already available and can be used without any additional configuration.
+- [Network addresses](/docs/network/addresses.md)
+- [Add a new network service](/docs/network/add_new_service.md)
-The following network services are provided:
+Testrun provides several built-in network services you can use for testing purposes. These services don't require any additional configuration. Below is a list and brief description of the network services provided.
-### Internet Connectivity (Gateway Service)
+# Internet connectivity (gateway service)
The gateway service provides internet connectivity to the test network. It allows devices in the network to access external resources and communicate with the internet.
-### DHCPv4 Service
+# DHCPv4 service
The DHCPv4 service provides Dynamic Host Configuration Protocol (DHCP) functionality for IPv4 addressing. It includes the following components:
-- Primary DHCP Server: A primary DHCP server is available to assign IPv4 addresses to DHCP clients in the network.
-- Secondary DHCP Server (Failover Configuration): A secondary DHCP server operates in failover configuration with the primary server to provide high availability and redundancy.
+- Primary DHCP server: Assigns IPv4 addresses to DHCP clients in the network.
+- Secondary DHCP server (failover configuration): Operates in failover configuration with the primary server to provide high availability and redundancy.
-#### Configuration
+## Configuration
-The configuration of the DHCPv4 service can be modified using the provided GRPC (gRPC Remote Procedure Call) service.
+You can modify the configuration of the DHCPv4 service using the provided Remote Procedure Call (GRPC) service.
-### IPv6 SLAAC Addressing
+# IPv6 SLAAC addressing
-The primary DHCP server also provides IPv6 Stateless Address Autoconfiguration (SLAAC) addressing for devices in the network. IPv6 addresses are automatically assigned to devices using SLAAC where test devices support it.
+The primary DHCP server provides IPv6 Stateless Address Autoconfiguration (SLAAC) addressing for devices on the network. It automatically assigns IPv6 addresses to devices using SLAAC where test devices support it.
-### NTP Service
+# NTP service
-The Network Time Protocol (NTP) service provides time synchronization for devices in the network. It ensures that all devices have accurate and synchronized time information.
+The Network Time Protocol (NTP) service provides time synchronization for devices on the network. It ensures that all devices have accurate and synchronized time information.
-### DNS Service
+# DNS service
-The DNS (Domain Name System) service resolves domain names to their corresponding IP addresses. It allows devices in the network to access external resources using domain names.
+The Domain Name System (DNS) service resolves domain names to their corresponding IP addresses. It allows devices on the network to access external resources using domain names.
-### 802.1x Authentication (Radius Module)
+# 802.1x authentication (radius module)
-The radius module provides 802.1x authentication for devices in the network. It ensures secure and authenticated access to the network. The issuing CA (Certificate Authority) certificate can be specified by the user if required.
\ No newline at end of file
+The radius module provides 802.1x authentication for devices on the network. It ensures secure and authenticated access to the network. The user can specify the issuing Certificate Authority (CA) certificate if required.
\ No newline at end of file
diff --git a/docs/network/add_new_service.md b/docs/network/add_new_service.md
index b3fa22514..e6b01e102 100644
--- a/docs/network/add_new_service.md
+++ b/docs/network/add_new_service.md
@@ -1,21 +1,44 @@
-## Adding a New Network Service
+# Add a new network service
-The Testrun framework allows users to add their own network services with ease. A template network service can be used to get started quickly, this can be found at [modules/network/template](../../modules/network/template). Otherwise, see below for details of the requirements for new network services.
+The Testrun framework allows you to easily add your own network services. You can use the template network service at [modules/network/template](/modules/network/template). To add a new network service, follow these steps:
-To add a new network service to Testrun, follow the procedure below:
+1. Create a folder under `modules/network/` with the name of the network service in lowercase using only alphanumeric characters and hyphens (-).
+1. Include the following items in the created folder:
+ - `{module}.Dockerfile`: Dockerfile builds the network service image. Replace `{module}` with the name of the module.
+ - `conf/`: Folder containing the module configuration files.
+ - `bin/`: Folder containing the start-up script for the network service.
+ - Place any additional application code in its own folder.
-1. Create a folder under `modules/network/` with the name of the network service in lowercase, using only alphanumeric characters and hyphens (`-`).
-2. Inside the created folder, include the following files and folders:
- - `{module}.Dockerfile`: Dockerfile for building the network service image. Replace `{module}` with the name of the module.
- - `conf/`: Folder containing the module configuration files.
- - `bin/`: Folder containing the startup script for the network service.
- - Any additional application code can be placed in its own folder.
+Here are some examples:
-### Example `module_config.json`
+## {module}.Dockerfile
-```json
+```
+# Image name: test-run/{module}
+FROM test-run/base:latest
+
+ARG MODULE_NAME={module}
+ARG MODULE_DIR=modules/network/$MODULE_NAME
+
+# Install network service dependencies
+# ...
+
+# Copy over all configuration files
+COPY $MODULE_DIR/conf /testrun/conf
+
+# Copy over all binary files
+COPY $MODULE_DIR/bin /testrun/bin
+
+# Copy over all python files
+COPY $MODULE_DIR/python /testrun/python
+
+# Do not specify a CMD or Entrypoint as Testrun will automatically start your service as required by calling the start_network_service script in the bin folder
+```
+
+## module_config.json
+```
{
"config": {
"meta": {
@@ -42,35 +65,11 @@ To add a new network service to Testrun, follow the procedure below:
}
}
}
-```
-
-### Example of {module}.Dockerfile
-```Dockerfile
-# Image name: test-run/{module}
-FROM test-run/base:latest
-
-ARG MODULE_NAME={module}
-ARG MODULE_DIR=modules/network/$MODULE_NAME
-
-# Install network service dependencies
-# ...
-
-# Copy over all configuration files
-COPY $MODULE_DIR/conf /testrun/conf
-
-# Copy over all binary files
-COPY $MODULE_DIR/bin /testrun/bin
-
-# Copy over all python files
-COPY $MODULE_DIR/python /testrun/python
-
-# Do not specify a CMD or Entrypoint as Testrun will automatically start your service as required
```
-### Example of start_network_service script
-
-```bash
+## start_network_service script
+```
#!/bin/bash
CONFIG_FILE=/etc/network_service/config.conf
@@ -89,8 +88,4 @@ echo "Starting Network Service..."
# Restart the network service when the config changes
# ...
-```
-
-
-
-
+```
\ No newline at end of file
diff --git a/docs/network/addresses.md b/docs/network/addresses.md
index 7fa71d716..261242687 100644
--- a/docs/network/addresses.md
+++ b/docs/network/addresses.md
@@ -1,20 +1,19 @@
-## Network Addresses
+# Network addresses
-Each network service is configured with an IPv4 and IPv6 address. For IPv4 addressing, the last number in the IPv4 address is fixed (ensuring the IP is unique). See below for a table of network addresses:
+Each network service is configured with an IPv4 and IPv6 address. For IPv4 addressing, the last number in the IPv4 address is fixed, ensuring the IP is unique. The table below lists network addresses you might need.
-| Name | Mac address | IPv4 address | IPv6 address |
-|---------------------|----------------------|--------------|------------------------------|
-| Internet gateway | 9a:02:57:1e:8f:01 | 10.10.10.1 | fd10:77be:4186::1 |
-| DHCP primary | 9a:02:57:1e:8f:02 | 10.10.10.2 | fd10:77be:4186::2 |
-| DHCP secondary | 9a:02:57:1e:8f:03 | 10.10.10.3 | fd10:77be:4186::3 |
-| DNS server | 9a:02:57:1e:8f:04 | 10.10.10.4 | fd10:77be:4186::4 |
-| NTP server | 9a:02:57:1e:8f:05 | 10.10.10.5 | fd10:77be:4186::5 |
-| Radius authenticator| 9a:02:57:1e:8f:07 | 10.10.10.7 | fd10:77be:4186::7 |
-| Active test module | 9a:02:57:1e:8f:09 | 10.10.10.9 | fd10:77be:4186::9 |
+| Name | MAC address | IPv4 address | IPv6 address |
+| ----------------- | ----------------- | ------------- | ------------------- |
+| Internet gateway | 9a\:02\:57\:1e\:8f\:01 | 10.10.10.1 | fd10\:77be\:4186\:\:1 |
+| DHCP primary | 9a\:02\:57\:1e\:8f\:02 | 10.10.10.2 | fd10\:77be\:4186\:\:2 |
+| DHCP secondary | 9a\:02\:57\:1e\:8f\:03 | 10.10.10.3 | fd10\:77be\:4186\:\:3 |
+| DNS server | 9a\:02\:57\:1e\:8f\:04 | 10.10.10.4 | fd10\:77be\:4186\:\:4 |
+| NTP server | 9a\:02\:57\:1e\:8f\:05 | 10.10.10.5 | fd10\:77be\:4186\:\:5 |
+| Radius authenticator | 9a\:02\:57\:1e\:8f\:07 | 10.10.10.7 | fd10\:77be\:4186\:\:7 |
+| Active test module | 9a\:02\:57\:1e\:8f\:09 | 10.10.10.9 | fd10\:77be\:4186\:\:9 |
+The default network range is 10.10.10.0/24 and devices are assigned addresses in that range via DHCP. The range may change when requested by a test module. In that case, network services restart and are accessible on the new range with the same final host ID. The default IPv6 network is fd10:77be:4186::/64 and addresses are assigned to devices on the network using IPv6 SLAAC.
-The default network range is 10.10.10.0/24 and devices will be assigned addresses in that range via DHCP. The range may change when requested by a test module. In which case, network services will be restarted and accessible on the new range, with the same final host ID. The default IPv6 network is fd10:77be:4186::/64 and addresses will be assigned to devices on the network using IPv6 SLAAC.
-
-When creating a new network module, please ensure that the ip_index value in the module_config.json is unique otherwise unexpected behaviour will occur.
\ No newline at end of file
+When creating a new network module, ensure that the ip_index value in the module_config.json is unique to prevent unexpected behavior.
\ No newline at end of file
diff --git a/docs/network/identify_interfaces.md b/docs/network/identify_interfaces.md
deleted file mode 100644
index 50e62acd3..000000000
--- a/docs/network/identify_interfaces.md
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-## Identifying network interfaces
-
-For Testrun to operate correctly, you must select the correct network interfaces within the settings panel of the user interface. There are 2 methods to identify the correct network interfaces:
-
-A) Find the printed MAC address on your interface
-
-Some USB network interfaces will have the MAC address printed on the interface itself. This will look something like: ```00:e0:4c:02:0f:a8```.
-
-Compare this printed MAC address against the MAC address provided in the settings panel in the user interface.
-
-B) Connect your interfaces one at a time
-
- 1) Ensure both interfaces are disconnected from your PC and open the settings panel in the user interface.
-
- 2) Connect your internet interface to your PC and refresh the settings panel. One interface, which was not previously present, should now be visibile.
-
- 3) Repeat the previous step for the devices interface.
-
diff --git a/docs/roadmap.pdf b/docs/roadmap.pdf
index 9f4599ee1..0566598e0 100644
Binary files a/docs/roadmap.pdf and b/docs/roadmap.pdf differ
diff --git a/docs/test/README.md b/docs/test/README.md
index 3163b4c84..19fcf3fd5 100644
--- a/docs/test/README.md
+++ b/docs/test/README.md
@@ -1,7 +1,5 @@
+# Testing
-## Testing
-The test requirements that are investigated by Testrun can be found in the [test modules documentation](/docs/test/modules.md).
-
-To understand the testing results, various definitions of test results and requirements are specified in the [statuses documentation](/docs/test/statuses.md).
\ No newline at end of file
+Testrun provides modules for you to test your own device. You can learn more about the requirements for each on the [Test modules page](/docs/test/modules.md). The [Test results page](/docs/test/statuses.md) outlines possible results and how to interpret them.
diff --git a/docs/test/modules.md b/docs/test/modules.md
index 2fe5983b1..ff6bb5c53 100644
--- a/docs/test/modules.md
+++ b/docs/test/modules.md
@@ -1,16 +1,26 @@
-## Test Modules
+# Test modules
-Testrun provides some pre-built test modules for you to use when testing your own device. These test modules are listed below:
+Testrun provides some pre-built modules you can use when testing your own device. The table below lists the test module, its purpose, and a link to additional information.
-| Name | Description | Read more |
-|---|---|---|
-| Base | Template for all test modules | [Base module](/modules/test/base/README.md) |
-| Baseline | A sample test module | [Baseline module](/modules/test/baseline/README.md) |
-| Connection | Verify IP and DHCP based behavior | [Connection module](/modules/test/conn/README.md) |
-| DNS | Verify DNS functionality | [DNS module](/modules/test/dns/README.md) |
-| Services | Ensure unsecure services are disabled | [Services module](/modules/test/services/README.md) |
-| NTP | Verify NTP functionality | [NTP module](/modules/test/ntp/README.md) |
-| Protocol | Inspect BMS protocol implementation | [Protocol Module](/modules/test/protocol/README.md) |
-| TLS | Determine TLS client and server behavior | [TLS module](/modules/test/tls/README.md) |
+| Module name | Purpose | Additional documentation |
+| ------------ | ---------------------------- | ----------------------------- |
+| Base | Template for all test modules | [Base module] |
+| Baseline | Sample test module | [Baseline module] |
+| Connection | Verify IP and DHCP-based behavior | [Connection module] |
+| DNS | Verify DNS functionality | [DNS module] |
+| Services | Ensure unsecure services are disabled | [Services module] |
+| NTP | Verify NTP functionality | [NTP module] |
+| Protocol | Inspect BMS protocol implementation | [Protocol Module] |
+| TLS | Determine TLS client and server behavior | [TLS module] |
+
+
+[Base module]: /modules/test/base/README.md
+[Baseline module]: /modules/test/baseline/README.md
+[Connection module]: /modules/test/conn/README.md
+[DNS module]: /modules/test/dns/README.md
+[Services module]: /modules/test/services/README.md
+[NTP module]: /modules/test/ntp/README.md
+[Protocol Module]: /modules/test/protocol/README.md
+[TLS module]: /modules/test/tls/README.md
diff --git a/docs/test/statuses.md b/docs/test/statuses.md
index d196fa4af..25c3d77b2 100644
--- a/docs/test/statuses.md
+++ b/docs/test/statuses.md
@@ -1,33 +1,32 @@
-## Test Statuses
-Testrun will output the result and description of each automated test. The test results will be one of the following:
+# Test results
-| Name | Description | What next? |
-|---|---|---|
-| Compliant | The device implements the required feature correctly | Nothing |
-| Non-Compliant | The device does not support the specified requirements for the test | Modify or implement the required functionality on the device |
-| Feature Not Detected | The device does not implement a feature covered by the test | You may implement the functionality (not required) |
-| Error | An error occured whilst running the test | Create a bug report requesting additional support to diagnose the issue |
+Testrun outputs the result and a description of each automated test. The table below includes the result name, its description, and what your next step should be.
-## Test Requirement
-Testrun also determines whether each test is required for the device to receive an overall compliant result. These rules are:
+| Result name | Description | What next? |
+| --------------------- | ------------------------ | ------------------------ |
+| Compliant | The device implements the required feature correctly. | Nothing. |
+| Non-Compliant | The device doesn’t support the specified requirements for the test. | Modify or implement the required functionality on the device. |
+| Informational | Extra information about the device under test | Nothing. |
+| Feature Not Detected | The device doesn’t implement a feature covered by the test. | You may implement the functionality but it’s not required. |
+| Error | An error occurred while running the test. | Create a bug report requesting additional support to diagnose the issue. |
-| Name | Description |
-|---|---|
-| Required | The device must implement the feature |
-| Recommended | The device should implement the feature, but will not receive an overall Non-Compliant if not implemented |
-| Roadmap | The device should implement this feature in the future, but is not required at the moment |
-| Required If Applicable | If the device implements this feature, it must be implemented correctly (as per the test requirements) |
-## Testrun Statuses
-Once testing is completed, an overall result for the test attempt will be produced. This is calculated by comparing the result of all tests, and whether they are required or not required.
+# Test requirements
-### Compliant
-All required tests are implemented correctly, and all required if applicable tests are implemented correctly (where the feature has been implemented).
+Testrun determines whether the device needs each test to receive an overall compliant result. Here are the rules and what they mean:
-### Non-Compliant
-One or more of the required tests (or required if applicable tests) have produced a non-compliant result.
+- Required: The device must implement the feature.
+- Recommended: The device should implement the feature but won't receive an overall Non-Compliant if it's not implemented.
+- Roadmap: The device should implement this feature in the future, but it's not required at the moment.
+- Required If Applicable: If the device implements this feature, it must be implemented correctly (per the test requirements).
-### Error
-One of more of the required tests (or required if applicable tests) have not executed correctly. This does not necessarily indicate that the device is compliant or non-compliant.
+# Testrun statuses
+
+Once testing is complete, the program produces an overall status for the test attempt. It's calculated by comparing the results of all tests and whether they're required or not. The possible statuses are:
+
+- Compliant: All required tests are implemented correctly, and all required if applicable tests are implemented correctly (where the feature is implemented).
+- Non-Compliant: One or more of the required tests (or Required If Applicable tests) produced a Non-Compliant result.
+- Error: One or more of the required tests (or Required If Applicable tests) didn't execute correctly. This doesn't necessarily indicate that the device is Compliant or Non-Compliant.
+- Cancelled: Either the device was disconnected during testing or the user requested to cancel the test attempt.
\ No newline at end of file
diff --git a/docs/ui/accessibility.md b/docs/ui/accessibility.md
new file mode 100644
index 000000000..58d948522
--- /dev/null
+++ b/docs/ui/accessibility.md
@@ -0,0 +1,12 @@
+
+
+# Accessibility
+
+We designed Testrun with accessibility at its core. The application provides full support for:
+
+- Screen readers
+- Keyboard navigation
+- Responsive resizing and scaling
+- [Helperbird](https://www.helperbird.com/)
+
+For a more thorough explanation of these features, download the [accessibility video](https://github.com/google/testrun/raw/main/docs/ui/accessibility.mp4). If you require further accommodations when using Testrun, please [raise an issue](https://github.com/google/testrun/issues/new/choose).
\ No newline at end of file
diff --git a/docs/ui/device_icon.png b/docs/ui/device_icon.png
deleted file mode 100644
index 2472f7da2..000000000
Binary files a/docs/ui/device_icon.png and /dev/null differ
diff --git a/docs/ui/getstarted--2dn8vrzsspe.png b/docs/ui/getstarted--2dn8vrzsspe.png
new file mode 100644
index 000000000..302e55302
Binary files /dev/null and b/docs/ui/getstarted--2dn8vrzsspe.png differ
diff --git a/docs/ui/getstarted--3d9k3si3ul1.png b/docs/ui/getstarted--3d9k3si3ul1.png
new file mode 100644
index 000000000..5c762156d
Binary files /dev/null and b/docs/ui/getstarted--3d9k3si3ul1.png differ
diff --git a/docs/ui/getstarted--7cfvdpdnc5o.png b/docs/ui/getstarted--7cfvdpdnc5o.png
new file mode 100644
index 000000000..288e5f876
Binary files /dev/null and b/docs/ui/getstarted--7cfvdpdnc5o.png differ
diff --git a/docs/ui/getstarted--m4si1otdu5d.png b/docs/ui/getstarted--m4si1otdu5d.png
new file mode 100644
index 000000000..f6b7eddc9
Binary files /dev/null and b/docs/ui/getstarted--m4si1otdu5d.png differ
diff --git a/docs/ui/getstarted--q5uw26tfod.png b/docs/ui/getstarted--q5uw26tfod.png
new file mode 100644
index 000000000..4b8b2e847
Binary files /dev/null and b/docs/ui/getstarted--q5uw26tfod.png differ
diff --git a/docs/ui/getstarted--w09wecsry3.png b/docs/ui/getstarted--w09wecsry3.png
new file mode 100644
index 000000000..e379040b5
Binary files /dev/null and b/docs/ui/getstarted--w09wecsry3.png differ
diff --git a/docs/ui/history_icon.png b/docs/ui/history_icon.png
deleted file mode 100644
index eb95a8663..000000000
Binary files a/docs/ui/history_icon.png and /dev/null differ
diff --git a/docs/ui/progress_icon.png b/docs/ui/progress_icon.png
deleted file mode 100644
index c326d185e..000000000
Binary files a/docs/ui/progress_icon.png and /dev/null differ
diff --git a/docs/ui/settings_icon.png b/docs/ui/settings_icon.png
deleted file mode 100644
index 8fc83b9bb..000000000
Binary files a/docs/ui/settings_icon.png and /dev/null differ
diff --git a/docs/ui/settings_menu.png b/docs/ui/settings_menu.png
deleted file mode 100644
index 046526b25..000000000
Binary files a/docs/ui/settings_menu.png and /dev/null differ
diff --git a/docs/ui/test_name.png b/docs/ui/test_name.png
deleted file mode 100644
index 3d18df19d..000000000
Binary files a/docs/ui/test_name.png and /dev/null differ
diff --git a/docs/virtual_machine.md b/docs/virtual_machine.md
index 2f029b296..579a40c93 100644
--- a/docs/virtual_machine.md
+++ b/docs/virtual_machine.md
@@ -1,38 +1,41 @@
-## Virtual Machine
+# Run on a virtual machine
-This guide will provide steps to use Testrun within a virtual machine in virtual Box (VMWare and other providers have not yet been tested). You should use this guide alongside the [Get Started guide](/docs/get_started.md) - only differences will be outlined in this guide.
+This page provides steps to use Testrun within a virtual machine in VirtualBox. VMWare and other providers haven't been tested yet. You should use these instructions alongside the [Get started guide](/docs/get_started.md).
-## Prerequisites
+# Prerequisites
-### Hardware
+## Hardware
-Before starting with Testrun, ensure you have the following hardware:
-- PC running any OS that supports Virtual Box
-- 2x USB Ethernet adapter (built in ethernet connections are not supported)
-- Internet connection
+Before you start with Testrun, ensure you have the following hardware:
-### Software
+- PC running any OS that supports VirtualBox
+- 2x USB Ethernet adapter (built-in Ethernet connections aren't supported)
+- Internet connection
-Ensure the following software is installed on the host PC:
- - Virtual Box
+## Software
-Ensure the following software is installed on your virtual machine:
-- Ubuntu LTS (22.04 or 20.04)
-- Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository)
+Ensure you have VirtualBox installed on the host PC. Then, install the following software on your virtual machine:
-## Installation
+- Ubuntu LTS (22.04 or 20.04)
+- Docker
+ - Refer to the [installation guide](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) as needed.
-In addition to the install steps provided in the Get Started guide, the default user must be added to the sudo group.
-1. Open a terminal and run ```sudo su``` to login as root (you will be prompted for your password).
-2. Add the default user to the sudo group by running ```adduser {username} sudo```.
-3. Restart the virtual machine.
-4. Continue the installation as per the Get Started guide.
+# Installation
-## Start Testrun
+As part of installation, you must add the default user to the sudo group:
+
+1. Open a terminal and run `sudo su` to log in as root.
+1. Enter your password when prompted.
+1. Add the default user to the sudo group by running `adduser {username} sudo`.
+1. Restart the virtual machine.
+1. Follow the steps in the [Get started guide](/docs/get_started.md) to complete the installation.
+
+# Start Testrun
+
+Follow these steps to start Testrun. Keep in mind that attaching USB Ethernet adapters is different when working in a virtual machine.
-Attaching USB ethernet adapters is different when working in a Virtual Machine.
1. Ensure the 2x adapters are attached to the host PC.
-2. With the virtual machine running, right click the USB icon in the bottom right of the window.
-3. Select the 2x ethernet adapter names and check that these two adapters have now appeared in the virtual machine.
\ No newline at end of file
+1. With the virtual machine running, right-click the **USB** icon in the bottom-right of the window.
+1. Select the 2x Ethernet adapter names. The two adapters should now appear in the virtual machine.
\ No newline at end of file
diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py
index e8e87465d..0d633987c 100644
--- a/framework/python/src/api/api.py
+++ b/framework/python/src/api/api.py
@@ -26,8 +26,10 @@
import uvicorn
from urllib.parse import urlparse
-from common import logger, tasks
+from core import tasks
+from common import logger
from common.device import Device
+from common.statuses import TestrunStatus
LOGGER = logger.get_logger("api")
@@ -35,8 +37,16 @@
DEVICE_MANUFACTURER_KEY = "manufacturer"
DEVICE_MODEL_KEY = "model"
DEVICE_TEST_MODULES_KEY = "test_modules"
+DEVICE_TEST_PACK_KEY = "test_pack"
+DEVICE_TYPE_KEY = "type"
+DEVICE_TECH_KEY = "technology"
+DEVICE_ADDITIONAL_INFO_KEY = "additional_info"
+
DEVICES_PATH = "local/devices"
-DEFAULT_DEVICE_INTF = "enx123456789123"
+
+RESOURCES_PATH = "resources"
+DEVICE_FOLDER_PATH = "devices"
+DEVICE_QUESTIONS_FILE_NAME = "device_profile.json"
LATEST_RELEASE_CHECK = ("https://api.github.com/repos/google/" +
"testrun/releases/latest")
@@ -45,32 +55,45 @@
class Api:
"""Provide REST endpoints to manage Testrun"""
- def __init__(self, test_run):
+ def __init__(self, testrun):
- self._test_run = test_run
+ self._testrun = testrun
self._name = "Testrun API"
self._router = APIRouter()
- self._session = self._test_run.get_session()
+ # Load static JSON resources
+ device_resources = os.path.join(self._testrun.get_root_dir(),
+ RESOURCES_PATH,
+ DEVICE_FOLDER_PATH)
+
+ # Load device profile questions
+ self._device_profile = self._load_json(device_resources,
+ DEVICE_QUESTIONS_FILE_NAME)
+ # Fetch Testrun session
+ self._session = self._testrun.get_session()
+
+ # System endpoints
self._router.add_api_route("/system/interfaces", self.get_sys_interfaces)
self._router.add_api_route("/system/config",
self.post_sys_config,
methods=["POST"])
self._router.add_api_route("/system/config", self.get_sys_config)
self._router.add_api_route("/system/start",
- self.start_test_run,
+ self.start_testrun,
methods=["POST"])
self._router.add_api_route("/system/stop",
- self.stop_test_run,
+ self.stop_testrun,
methods=["POST"])
self._router.add_api_route("/system/status", self.get_status)
self._router.add_api_route("/system/shutdown",
self.shutdown,
methods=["POST"])
-
self._router.add_api_route("/system/version", self.get_version)
+ self._router.add_api_route("/system/modules", self.get_test_modules)
+ self._router.add_api_route("/system/testpacks", self.get_test_packs)
+ # Report endpoints
self._router.add_api_route("/reports", self.get_reports)
self._router.add_api_route("/report",
self.delete_report,
@@ -81,6 +104,7 @@ def __init__(self, test_run):
self.get_results,
methods=["POST"])
+ # Device endpoints
self._router.add_api_route("/devices", self.get_devices)
self._router.add_api_route("/device",
self.delete_device,
@@ -89,10 +113,9 @@ def __init__(self, test_run):
self._router.add_api_route("/device/edit",
self.edit_device,
methods=["POST"])
+ self._router.add_api_route("/devices/format", self.get_devices_profile)
- # Load modules
- self._router.add_api_route("/system/modules", self.get_test_modules)
-
+ # Certificate endpoints
self._router.add_api_route("/system/config/certs", self.get_certs)
self._router.add_api_route("/system/config/certs",
self.upload_cert,
@@ -115,10 +138,15 @@ def __init__(self, test_run):
origins = ["*"]
# Scheduler for background periodic tasks
- self._scheduler = tasks.PeriodicTasks(self._test_run)
+ self._scheduler = tasks.PeriodicTasks(self._testrun)
+ # Init FastAPI
self._app = FastAPI(lifespan=self._scheduler.start)
+
+ # Attach router to FastAPI
self._app.include_router(self._router)
+
+ # Attach CORS middleware
self._app.add_middleware(
CORSMiddleware,
allow_origins=origins,
@@ -127,10 +155,27 @@ def __init__(self, test_run):
allow_headers=["*"],
)
+ # Use separate thread for API
self._api_thread = threading.Thread(target=self._start,
name="Testrun API",
daemon=True)
+ def _load_json(self, directory, file_name):
+ """Utility method to load json files' """
+ # Construct the base path relative to the main folder
+ root_dir = self._testrun.get_root_dir()
+
+ # Construct the full file path
+ file_path = os.path.join(root_dir, directory, file_name)
+
+ # Open the file in read mode
+ with open(file_path, "r", encoding="utf-8") as file:
+ # Return the file content
+ return json.load(file)
+
+ def _get_testrun(self):
+ return self._testrun
+
def start(self):
LOGGER.info("Starting API")
self._api_thread.start()
@@ -191,9 +236,12 @@ async def get_sys_config(self):
return self._session.get_config()
async def get_devices(self):
- return self._session.get_device_repository()
+ devices = []
+ for device in self._session.get_device_repository():
+ devices.append(device.to_dict())
+ return devices
- async def start_test_run(self, request: Request, response: Response):
+ async def start_testrun(self, request: Request, response: Response):
LOGGER.debug("Received start command")
@@ -214,9 +262,22 @@ async def start_test_run(self, request: Request, response: Response):
device = self._session.get_device(body_json["device"]["mac_addr"])
+ # Check if requested device is known in the device repository
+ if device is None:
+ response.status_code = status.HTTP_404_NOT_FOUND
+ return self._generate_msg(
+ False, "A device with that MAC address could not be found")
+
+ # Check if device is fully configured
+ if device.status != "Valid":
+ response.status_code = status.HTTP_400_BAD_REQUEST
+ return self._generate_msg(False, "Device configuration is not complete")
+
# Check Testrun is not already running
- if self._test_run.get_session().get_status() in [
- "In Progress", "Waiting for Device", "Monitoring"
+ if self._testrun.get_session().get_status() in [
+ TestrunStatus.IN_PROGRESS,
+ TestrunStatus.WAITING_FOR_DEVICE,
+ TestrunStatus.MONITORING
]:
LOGGER.debug("Testrun is already running. Cannot start another instance")
response.status_code = status.HTTP_409_CONFLICT
@@ -224,23 +285,17 @@ async def start_test_run(self, request: Request, response: Response):
False, "Testrun cannot be started " +
"whilst a test is running on another device")
- # Check if requested device is known in the device repository
- if device is None:
- response.status_code = status.HTTP_404_NOT_FOUND
- return self._generate_msg(
- False, "A device with that MAC address could not be found")
-
device.firmware = body_json["device"]["firmware"]
# Check if config has been updated (device interface not default)
- if (self._test_run.get_session().get_device_interface() ==
- DEFAULT_DEVICE_INTF):
+ if (self._testrun.get_session().get_device_interface() ==
+ ""):
response.status_code = status.HTTP_400_BAD_REQUEST
return self._generate_msg(
False, "Testrun configuration has not yet " + "been completed.")
# Check Testrun is able to start
- if self._test_run.get_net_orc().check_config() is False:
+ if self._testrun.get_net_orc().check_config() is False:
response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
return self._generate_msg(
False, "Configured interfaces are not " +
@@ -260,12 +315,12 @@ async def start_test_run(self, request: Request, response: Response):
f"{device.manufacturer} {device.model} with " +
f"MAC address {device.mac_addr}")
- thread = threading.Thread(target=self._start_test_run, name="Testrun")
+ thread = threading.Thread(target=self._start_testrun, name="Testrun")
thread.start()
- self._test_run.get_session().set_target_device(device)
+ self._testrun.get_session().set_target_device(device)
- return self._test_run.get_session().to_json()
+ return self._testrun.get_session().to_json()
def _generate_msg(self, success, message):
msg_type = "success"
@@ -273,24 +328,26 @@ def _generate_msg(self, success, message):
msg_type = "error"
return json.loads('{"' + msg_type + '": "' + message + '"}')
- def _start_test_run(self):
- self._test_run.start()
+ def _start_testrun(self):
+ self._testrun.start()
- async def stop_test_run(self, response: Response):
+ async def stop_testrun(self, response: Response):
LOGGER.debug("Received stop command")
# Check if Testrun is running
- if (self._test_run.get_session().get_status()
- not in ["In Progress", "Waiting for Device", "Monitoring"]):
+ if (self._testrun.get_session().get_status()
+ not in [TestrunStatus.IN_PROGRESS,
+ TestrunStatus.WAITING_FOR_DEVICE,
+ TestrunStatus.MONITORING]):
response.status_code = 404
return self._generate_msg(False, "Testrun is not currently running")
- self._test_run.stop()
+ self._testrun.stop()
return self._generate_msg(True, "Testrun stopped")
async def get_status(self):
- return self._test_run.get_session().to_json()
+ return self._testrun.get_session().to_json()
def shutdown(self, response: Response):
@@ -298,20 +355,24 @@ def shutdown(self, response: Response):
# Check that Testrun is not currently running
if (self._session.get_status()
- not in ["Cancelled", "Compliant", "Non-Compliant", "Idle"]):
+ not in [TestrunStatus.CANCELLED,
+ TestrunStatus.COMPLIANT,
+ TestrunStatus.NON_COMPLIANT,
+ TestrunStatus.IDLE
+ ]):
LOGGER.debug("Unable to shutdown Testrun as Testrun is in progress")
response.status_code = 400
return self._generate_msg(
False, "Unable to shutdown. A test is currently in progress.")
- self._test_run.shutdown()
+ self._testrun.shutdown()
os.kill(os.getpid(), signal.SIGTERM)
async def get_version(self, response: Response):
# Add defaults
json_response = {}
- json_response["installed_version"] = "v" + self._test_run.get_version()
+ json_response["installed_version"] = "v" + self._testrun.get_version()
json_response["update_available"] = False
json_response["latest_version"] = None
json_response["latest_version_url"] = (
@@ -383,7 +444,7 @@ async def delete_report(self, request: Request, response: Response):
if len(body_raw) == 0:
response.status_code = 400
- return self._generate_msg(False, "Invalid request received")
+ return self._generate_msg(False, "Invalid request received, missing body")
try:
body_json = json.loads(body_raw)
@@ -395,12 +456,18 @@ async def delete_report(self, request: Request, response: Response):
if "mac_addr" not in body_json or "timestamp" not in body_json:
response.status_code = 400
- return self._generate_msg(False, "Invalid request received")
+ return self._generate_msg(False, "Missing mac address or timestamp")
mac_addr = body_json.get("mac_addr").lower()
timestamp = body_json.get("timestamp")
- parsed_timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
- timestamp_formatted = parsed_timestamp.strftime("%Y-%m-%dT%H:%M:%S")
+
+ try:
+ parsed_timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
+ timestamp_formatted = parsed_timestamp.strftime("%Y-%m-%dT%H:%M:%S")
+
+ except ValueError:
+ response.status_code = 400
+ return self._generate_msg(False, "Incorrect timestamp format")
# Get device from MAC address
device = self._session.get_device(mac_addr)
@@ -409,7 +476,15 @@ async def delete_report(self, request: Request, response: Response):
response.status_code = 404
return self._generate_msg(False, "Could not find device")
- if self._test_run.delete_report(device, timestamp_formatted):
+ # Assign the reports folder path from testrun
+ reports_folder = self._testrun.get_reports_folder(device)
+
+ # Check if reports folder exists
+ if not os.path.exists(reports_folder):
+ response.status_code = 404
+ return self._generate_msg(False, "Report not found")
+
+ if self._testrun.delete_report(device, timestamp_formatted):
return self._generate_msg(True, "Deleted report")
response.status_code = 500
@@ -433,7 +508,7 @@ async def delete_device(self, request: Request, response: Response):
mac_addr = device_json.get("mac_addr").lower()
# Check that device exists
- device = self._test_run.get_session().get_device(mac_addr)
+ device = self._testrun.get_session().get_device(mac_addr)
if device is None:
response.status_code = 404
@@ -442,13 +517,16 @@ async def delete_device(self, request: Request, response: Response):
# Check that Testrun is not currently running against this device
if (self._session.get_target_device() == device
and self._session.get_status()
- not in ["Cancelled", "Compliant", "Non-Compliant"]):
+ not in [TestrunStatus.CANCELLED,
+ TestrunStatus.COMPLIANT,
+ TestrunStatus.NON_COMPLIANT
+ ]):
response.status_code = 403
return self._generate_msg(
False, "Cannot delete this device whilst " + "it is being tested")
# Delete device
- self._test_run.delete_device(device)
+ self._testrun.delete_device(device)
# Return success response
response.status_code = 200
@@ -488,7 +566,7 @@ async def save_device(self, request: Request, response: Response):
)
# Check if device folder exists
- device_folder = os.path.join(self._test_run.get_root_dir(),
+ device_folder = os.path.join(self._testrun.get_root_dir(),
DEVICES_PATH,
device_json.get(DEVICE_MANUFACTURER_KEY) +
" " +
@@ -507,10 +585,15 @@ async def save_device(self, request: Request, response: Response):
device.mac_addr = device_json.get(DEVICE_MAC_ADDR_KEY).lower()
device.manufacturer = device_json.get(DEVICE_MANUFACTURER_KEY)
device.model = device_json.get(DEVICE_MODEL_KEY)
+ device.test_pack = device_json.get(DEVICE_TEST_PACK_KEY)
+ device.type = device_json.get(DEVICE_TYPE_KEY)
+ device.technology = device_json.get(DEVICE_TECH_KEY)
+ device.additional_info = device_json.get(DEVICE_ADDITIONAL_INFO_KEY)
+
device.device_folder = device.manufacturer + " " + device.model
device.test_modules = device_json.get(DEVICE_TEST_MODULES_KEY)
- self._test_run.create_device(device)
+ self._testrun.create_device(device)
response.status_code = status.HTTP_201_CREATED
else:
@@ -553,14 +636,17 @@ async def edit_device(self, request: Request, response: Response):
if device is None:
response.status_code = status.HTTP_404_NOT_FOUND
return self._generate_msg(
- False, "A device with that MAC " + "address could not be found")
+ False, "A device with that MAC address could not be found")
if (self._session.get_target_device() == device
and self._session.get_status()
- not in ["Cancelled", "Compliant", "Non-Compliant"]):
+ not in [TestrunStatus.CANCELLED,
+ TestrunStatus.COMPLIANT,
+ TestrunStatus.NON_COMPLIANT
+ ]):
response.status_code = 403
return self._generate_msg(
- False, "Cannot edit this device whilst " + "it is being tested")
+ False, "Cannot edit this device whilst it is being tested")
# Check if a device exists with the new MAC address
check_new_device = self._session.get_device(
@@ -570,15 +656,22 @@ async def edit_device(self, request: Request, response: Response):
!= check_new_device.mac_addr):
response.status_code = status.HTTP_409_CONFLICT
return self._generate_msg(
- False, "A device with that MAC address " + "already exists")
+ False, "A device with that MAC address already exists")
# Update the device
device.mac_addr = device_json.get(DEVICE_MAC_ADDR_KEY).lower()
device.manufacturer = device_json.get(DEVICE_MANUFACTURER_KEY)
device.model = device_json.get(DEVICE_MODEL_KEY)
+ device.test_pack = device_json.get(DEVICE_TEST_PACK_KEY)
+ device.type = device_json.get(DEVICE_TYPE_KEY)
+ device.technology = device_json.get(DEVICE_TECH_KEY)
+ device.additional_info = device_json.get(DEVICE_ADDITIONAL_INFO_KEY)
device.test_modules = device_json.get(DEVICE_TEST_MODULES_KEY)
- self._test_run.save_device(device, device_json)
+ # Update device status to valid now that configuration is complete
+ device.status = "Valid"
+
+ self._testrun.save_device(device)
response.status_code = status.HTTP_200_OK
return device.to_config_json()
@@ -591,6 +684,12 @@ async def edit_device(self, request: Request, response: Response):
async def get_report(self, response: Response, device_name, timestamp):
device = self._session.get_device_by_name(device_name)
+ # If the device not found
+ if device is None:
+ LOGGER.info("Device not found, returning 404")
+ response.status_code = 404
+ return self._generate_msg(False, "Device not found")
+
# 1.3 file path
file_path = os.path.join(
DEVICES_PATH,
@@ -644,47 +743,77 @@ async def get_results(self, request: Request, response: Response, device_name,
return self._generate_msg(False,
"A device with that name could not be found")
- file_path = self._get_test_run().get_test_orc().zip_results(
+ # Check if report exists (1.3 file path)
+ report_file_path = os.path.join(
+ DEVICES_PATH,
+ device_name,
+ "reports",
+ timestamp,"test",
+ device.mac_addr.replace(":",""))
+
+ if not os.path.isdir(report_file_path):
+ # pre 1.3 file path
+ report_file_path = os.path.join(DEVICES_PATH, device_name, "reports",
+ timestamp)
+
+ if not os.path.isdir(report_file_path):
+ LOGGER.info("Report could not be found, returning 404")
+ response.status_code = 404
+ return self._generate_msg(False, "Report could not be found")
+
+ zip_file_path = self._get_testrun().get_test_orc().zip_results(
device, timestamp, profile)
- if file_path is None:
+ if zip_file_path is None:
response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
return self._generate_msg(
False, "An error occurred whilst archiving test results")
- if os.path.isfile(file_path):
- return FileResponse(file_path)
+ if os.path.isfile(zip_file_path):
+ return FileResponse(zip_file_path)
else:
LOGGER.info("Test results could not be found, returning 404")
response.status_code = 404
return self._generate_msg(False, "Test results could not be found")
+ async def get_devices_profile(self):
+ """Device profile questions"""
+ return self._device_profile
+
def _validate_device_json(self, json_obj):
# Check all required properties are present
- if not (DEVICE_MAC_ADDR_KEY in json_obj and DEVICE_MANUFACTURER_KEY
- in json_obj and DEVICE_MODEL_KEY in json_obj):
- return False
+ for string in [
+ DEVICE_MAC_ADDR_KEY,
+ DEVICE_MANUFACTURER_KEY,
+ DEVICE_MODEL_KEY,
+ DEVICE_TYPE_KEY,
+ DEVICE_TECH_KEY,
+ DEVICE_ADDITIONAL_INFO_KEY
+ ]:
+ if string not in json_obj:
+ LOGGER.error(f"Missing required key {string} in device configuration")
+ return False
# Check length of strings
if len(json_obj.get(DEVICE_MANUFACTURER_KEY)) > 28 or len(
json_obj.get(DEVICE_MODEL_KEY)) > 28:
+ LOGGER.error("Device manufacturer or model are longer than 28 characters")
return False
disallowed_chars = ["/", "\\", "\'", "\"", ";"]
for char in json_obj.get(DEVICE_MANUFACTURER_KEY):
if char in disallowed_chars:
+ LOGGER.error("Disallowed character in device manufacturer")
return False
for char in json_obj.get(DEVICE_MODEL_KEY):
if char in disallowed_chars:
+ LOGGER.error("Disallowed character in device model")
return False
return True
- def _get_test_run(self):
- return self._test_run
-
# Profiles
def get_profiles_format(self, response: Response):
@@ -706,6 +835,12 @@ async def update_profile(self, request: Request, response: Response):
LOGGER.debug("Received profile update request")
+ # Check if the profiles format was loaded correctly
+ if self.get_session().get_profiles_format() is None:
+ response.status_code = status.HTTP_501_NOT_IMPLEMENTED
+ return self._generate_msg(False,
+ "Risk profiles are not available right now")
+
try:
req_raw = (await request.body()).decode("UTF-8")
req_json = json.loads(req_raw)
@@ -726,6 +861,7 @@ async def update_profile(self, request: Request, response: Response):
profile = self.get_session().get_profile(profile_name)
if profile is None:
+
# Create new profile
profile = self.get_session().update_profile(req_json)
@@ -886,7 +1022,13 @@ async def delete_cert(self, request: Request, response: Response):
def get_test_modules(self):
modules = []
- for module in self._test_run.get_test_orc().get_test_modules():
+ for module in self._testrun.get_test_orc().get_test_modules():
if module.enabled and module.enable_container:
modules.append(module.display_name)
return modules
+
+ def get_test_packs(self):
+ test_packs: list[str] = []
+ for test_pack in self._testrun.get_test_orc().get_test_packs():
+ test_packs.append(test_pack.name)
+ return test_packs
diff --git a/framework/python/src/common/device.py b/framework/python/src/common/device.py
index c6a289d2c..d90720d90 100644
--- a/framework/python/src/common/device.py
+++ b/framework/python/src/common/device.py
@@ -14,7 +14,7 @@
"""Track device object information."""
-from typing import Dict, List
+from typing import List, Dict
from dataclasses import dataclass, field
from common.testreport import TestReport
from datetime import datetime
@@ -23,17 +23,21 @@
class Device():
"""Represents a physical device and it's configuration."""
+ status: str = 'Valid'
folder_url: str = None
mac_addr: str = None
manufacturer: str = None
model: str = None
+ type: str = None
+ technology: str = None
+ test_pack: str = 'Device Qualification'
+ additional_info: List[dict] = field(default_factory=list)
test_modules: Dict = field(default_factory=dict)
ip_addr: str = None
firmware: str = None
device_folder: str = None
reports: List[TestReport] = field(default_factory=list)
max_device_reports: int = None
- reports: List[TestReport] = field(default_factory=list)
def add_report(self, report):
self.reports.append(report)
@@ -54,11 +58,18 @@ def to_dict(self):
"""Returns the device as a python dictionary. This is used for the
system status API endpoint and in the report."""
device_json = {}
+ device_json['status'] = self.status
device_json['mac_addr'] = self.mac_addr
device_json['manufacturer'] = self.manufacturer
device_json['model'] = self.model
+ device_json['type'] = self.type
+ device_json['technology'] = self.technology
+ device_json['test_pack'] = self.test_pack
+ device_json['additional_info'] = self.additional_info
+
if self.firmware is not None:
device_json['firmware'] = self.firmware
+
device_json['test_modules'] = self.test_modules
return device_json
@@ -69,5 +80,9 @@ def to_config_json(self):
device_json['mac_addr'] = self.mac_addr
device_json['manufacturer'] = self.manufacturer
device_json['model'] = self.model
+ device_json['type'] = self.type
+ device_json['technology'] = self.technology
+ device_json['test_pack'] = self.test_pack
device_json['test_modules'] = self.test_modules
+ device_json['additional_info'] = self.additional_info
return device_json
diff --git a/framework/python/src/common/docker_util.py b/framework/python/src/common/docker_util.py
new file mode 100644
index 000000000..06b030419
--- /dev/null
+++ b/framework/python/src/common/docker_util.py
@@ -0,0 +1,35 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+"""Utility for common docker methods"""
+import docker
+
+def create_private_net(network_name):
+ client = docker.from_env()
+ try:
+ network = client.networks.get(network_name)
+ network.remove()
+ except docker.errors.NotFound:
+ pass
+
+ # TODO: These should be made into variables
+ ipam_pool = docker.types.IPAMPool(subnet='100.100.0.0/16',
+ iprange='100.100.100.0/24')
+
+ ipam_config = docker.types.IPAMConfig(pool_configs=[ipam_pool])
+
+ client.networks.create(network_name,
+ ipam=ipam_config,
+ internal=True,
+ check_duplicate=True,
+ driver='macvlan')
diff --git a/framework/python/src/common/mqtt.py b/framework/python/src/common/mqtt.py
index c58d24d3f..fc0458e7d 100644
--- a/framework/python/src/common/mqtt.py
+++ b/framework/python/src/common/mqtt.py
@@ -28,26 +28,25 @@ def __init__(self, message: str) -> None:
class MQTT:
- """ MQTT client class
- """
+ """ MQTT client class"""
def __init__(self) -> None:
self._host = WEBSOCKETS_HOST
self._client = mqtt_client.Client(mqtt_client.CallbackAPIVersion.VERSION2)
- LOGGER.setLevel(logger.logging.INFO)
self._client.enable_logger(LOGGER)
+ LOGGER.setLevel(logger.logging.INFO)
def _connect(self):
- """Establish connection to Mosquitto server
-
- Raises:
- MQTTException: Raises exception on connection error
- """
+ """Establish connection to MQTT broker"""
if not self._client.is_connected():
try:
self._client.connect(self._host, WEBSOCKETS_PORT, 60)
- except (ValueError, ConnectionRefusedError) as e:
- LOGGER.error("Can't connect to host")
- raise MQTTException("Connection to the Mosquitto server failed") from e
+ except (ValueError, ConnectionRefusedError):
+ LOGGER.error("Cannot connect to MQTT broker")
+
+ def disconnect(self):
+ """Disconnect the local client from the MQTT broker"""
+ if self._client.is_connected():
+ self._client.disconnect()
def send_message(self, topic: str, message: t.Union[str, dict]) -> None:
"""Send message to specific topic
diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py
index f50dffdde..eeae44db7 100644
--- a/framework/python/src/common/risk_profile.py
+++ b/framework/python/src/common/risk_profile.py
@@ -20,10 +20,15 @@
from common import logger
import json
import os
+from jinja2 import Template
+from copy import deepcopy
PROFILES_PATH = 'local/risk_profiles'
LOGGER = logger.get_logger('risk_profile')
RESOURCES_DIR = 'resources/report'
+TEMPLATE_FILE = 'risk_report_template.html'
+TEMPLATE_STYLES = 'risk_report_styles.css'
+DEVICE_FORMAT_PATH = 'resources/devices/device_profile.json'
# Locate parent directory
current_dir = os.path.dirname(os.path.realpath(__file__))
@@ -43,6 +48,34 @@ class RiskProfile():
def __init__(self, profile_json=None, profile_format=None):
+ # Jinja template
+ with open(os.path.join(report_resource_dir, TEMPLATE_FILE),
+ 'r',
+ encoding='UTF-8'
+ ) as template_file:
+ self._template = Template(template_file.read())
+ with open(os.path.join(report_resource_dir,
+ TEMPLATE_STYLES),
+ 'r',
+ encoding='UTF-8'
+ ) as style_file:
+ self._template_styles = style_file.read()
+
+ # Device profile format
+ self._device_format = []
+ try:
+ with open(os.path.join(root_dir, DEVICE_FORMAT_PATH),
+ 'r',
+ encoding='utf-8') as device_format_file:
+ device_format_json = json.load(device_format_file)
+ for step in device_format_json:
+ self._device_format.extend(step['questions'])
+ except (IOError, ValueError) as e:
+ LOGGER.error(
+ 'An error occurred whilst loading the device profile format')
+ LOGGER.debug(e)
+
+
if profile_json is None or profile_format is None:
return
@@ -92,6 +125,7 @@ def update(self, profile_json, profile_format):
self.risk = new_profile.risk
def get_file_path(self):
+ """Returns the file path for the current risk profile json"""
return os.path.join(PROFILES_PATH,
self.name + '.json')
@@ -108,6 +142,7 @@ def _validate(self, profile_json, profile_format):
self.status = 'Draft'
def update_risk(self, profile_format):
+ """Update the calculated risk for the risk profile"""
if self.status == 'Valid':
@@ -176,6 +211,15 @@ def update_risk(self, profile_format):
self.risk = risk
+ def _update_risk_by_device(self):
+ risk = self.risk
+ if self._device and self.status == 'Valid':
+ for question in self._device.additional_info:
+ if 'risk' in question and question['risk'] == 'High':
+ risk = 'High'
+ break
+ return risk
+
def _get_format_question(self, question: str, profile_format: dict):
for q in profile_format:
@@ -281,6 +325,7 @@ def _expired(self):
return today > expiry_date
def to_json(self, pretty=False):
+ """Returns the current risk profile in JSON format"""
json_dict = {
'name': self.name,
'version': self.version,
@@ -293,386 +338,88 @@ def to_json(self, pretty=False):
return json.dumps(json_dict, indent=indent)
def to_html(self, device):
-
- self._device = device
-
- return f'''
-
-
- {self._generate_head()}
-
-
- {self._generate_header()}
- {self._generate_risk_banner()}
- {self._generate_risk_questions()}
- {self._generate_footer()}
-
-
-
- '''
-
- def _generate_head(self):
-
- return f'''
-
-
-
- Risk Assessment
-
-
- '''
-
- def _generate_header(self):
+ """Returns the current risk profile in HTML format"""
+
+ high_risk_message = '''The device has been assessed to be high
+ risk due to the nature of the answers provided
+ about the device functionality.'''
+ limited_risk_message = '''The device has been assessed to be limited risk
+ due to the nature of the answers provided about
+ the device functionality.'''
with open(test_run_img_file, 'rb') as f:
- tr_img_b64 = base64.b64encode(f.read()).decode('utf-8')
- header = f'''
-
- '''
- return header
-
- def _generate_risk_banner(self):
- return f'''
-
-
-
{'high' if self.risk == 'High' else 'limited'} Risk
-
-
- {
- 'The device has been assessed to be high risk due to the nature of the answers provided about the device functionality.'
- if self.risk == 'High' else
- 'The device has been assessed to be limited risk due to the nature of the answers provided about the device functionality.'
- }
-
-
- '''
-
- def _generate_risk_questions(self):
-
+ logo_img_b64 = base64.b64encode(f.read()).decode('utf-8')
+
+ self._device = self._format_device_profile(device)
+ pages = self._generate_report_pages()
+ return self._template.render(
+ styles=self._template_styles,
+ manufacturer=self._device.manufacturer,
+ model=self._device.model,
+ logo=logo_img_b64,
+ risk=self._update_risk_by_device(),
+ high_risk_message=high_risk_message,
+ limited_risk_message=limited_risk_message,
+ pages=pages,
+ total_pages=len(pages),
+ version=self.version,
+ created_at=self.created.strftime('%d.%m.%Y')
+ )
+
+ def _generate_report_pages(self):
max_page_height = 350
- content = ''
-
- content += self._generate_table_head()
-
- index = 1
height = 0
+ pages = []
+ current_page = []
+ index = 1
+
+ questions = deepcopy(self._device.additional_info)
+ questions.extend(self.questions)
- for question in self.questions:
+ for question in questions:
if height > max_page_height:
- content += self._generate_new_page()
+ pages.append(current_page)
height = 0
+ current_page = []
- content += f'''
-
-
{index}.
-
{question['question']}
-
'''
+ page_item = deepcopy(question)
- # String answers (one line)
- if isinstance(question['answer'], str):
- content += question['answer']
+ if isinstance(page_item['answer'], str):
- if len(question['answer']) > 400:
+ if len(page_item['answer']) > 400:
height += 160
- elif len(question['answer']) > 300:
+ elif len(page_item['answer']) > 300:
height += 140
- elif len(question['answer']) > 200:
+ elif len(page_item['answer']) > 200:
height += 120
- elif len(question['answer']) > 100:
+ elif len(page_item['answer']) > 100:
height += 70
else:
height += 53
# Select multiple answers
- elif isinstance(question['answer'], list):
- content += '
'
+ elif isinstance(page_item['answer'], list):
+ text_answers = []
options = self._get_format_question(
- question=question['question'],
+ question=page_item['question'],
profile_format=self._profile_format)['options']
- for answer_index in question['answer']:
- height += 40
- content += f'''
- -
- {self._get_option_from_index(options, answer_index)['text']}
'''
-
- content += '
'
-
- # Question risk label
- if 'risk' in question:
- if question['risk'] == 'High':
- content += '
HIGH RISK
'
- elif question['risk'] == 'Limited':
- content += '''
- LIMITED RISK
'''
-
- content += '''
'''
+ options_dict = dict(enumerate(options))
+ for answer_index in page_item['answer']:
+ height += 40
+ text_answers.append(options_dict[answer_index]['text'])
+ page_item['answer'] = text_answers
+ page_item['index'] = index
index += 1
+ current_page.append(page_item)
+ pages.append(current_page)
- return content
-
- def _generate_table_head(self):
- return '''
-
-
'''
-
- def _generate_new_page(self):
-
- # End the current table
- content = '''
-
'''
-
- # End the page
- content += self._generate_footer()
- content += ''
-
- # Start a new page
- content += '''
-
- '''
-
- content += self._generate_header()
-
- content += self._generate_table_head()
-
- return content
-
- def _generate_footer(self):
- footer = f'''
-
- '''
- return footer
-
- def _generate_css(self):
- return '''
- /* Set some global variables */
- :root {
- --header-height: .75in;
- --header-width: 8.5in;
- --header-pos-x: 0in;
- --header-pos-y: 0in;
- --page-width: 8.5in;
- }
-
- @font-face {
- font-family: 'Google Sans';
- font-style: normal;
- src: url(https://fonts.gstatic.com/s/googlesans/v58/4Ua_rENHsxJlGDuGo1OIlJfC6l_24rlCK1Yo_Iqcsih3SAyH6cAwhX9RFD48TE63OOYKtrwEIJllpyk.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
- }
-
- @font-face {
- font-family: 'Roboto Mono';
- font-style: normal;
- src: url(https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
- }
-
- /* Define some common body formatting*/
- body {
- font-family: 'Google Sans', sans-serif;
- margin: 0px;
- padding: 0px;
- }
-
- /* Sets proper page size during print to pdf for weasyprint */
- @page {
- size: Letter;
- width: 8.5in;
- height: 11in;
- }
-
- .page {
- position: relative;
- margin: 0 20px;
- width: 8.5in;
- height: 11in;
- }
-
- /* Define the header related css elements*/
- .header {
- position: relative;
- }
-
- h1 {
- margin: 0 0 8px 0;
- font-size: 20px;
- font-weight: 400;
- }
-
- h2 {
- margin: 0px;
- font-size: 48px;
- font-weight: 700;
- }
-
- h3 {
- font-size: 24px;
- margin-bottom: 10px;
- margin-top: 15px;
- }
-
- h4 {
- font-size: 12px;
- font-weight: 500;
- color: #5F6368;
- margin-bottom: 0;
- margin-top: 0;
- }
-
- /* CSS for the footer */
- .footer {
- position: absolute;
- height: 30px;
- width: 8.5in;
- bottom: 0in;
- border-top: 1px solid #D3D3D3;
- }
-
- .footer-label {
- color: #3C4043;
- position: absolute;
- top: 5px;
- font-size: 12px;
- }
-
- @media print {
- @page {
- size: Letter;
- width: 8.5in;
- height: 11in;
- }
- }
-
- .risk-banner {
- min-height: 120px;
- padding: 5px 40px 0 40px;
- margin-top: 30px;
- }
-
- .risk-banner-limited {
- background-color: #E4F7FB;
- color: #007B83;
- }
-
- .risk-banner-high {
- background-color: #FCE8E6;
- color: #C5221F;
- }
-
- .risk-banner-title {
- text-transform: uppercase;
- font-weight: bold;
- }
-
- .risk-table {
- width: 100%;
- margin-top: 40px;
- text-align: left;
- color: #3C4043;
- font-size: 14px;
- }
-
- .risk-table-head {
- margin-bottom: 15px;
- }
-
- .risk-table-head-question {
- display: inline-block;
- margin-left: 70px;
- font-weight: bold;
- }
-
- .risk-table-head-answer {
- display: inline-block;
- margin-left: 325px;
- font-weight: bold;
- }
-
- .risk-table-row {
- margin-bottom: 8px;
- background-color: #F8F9FA;
- display: flex;
- align-items: stretch;
- overflow: hidden;
- }
-
- .risk-question-no {
- padding: 15px 20px;
- width: 10px;
- display: inline-block;
- vertical-align: top;
- position: relative;
- }
-
- .risk-question {
- padding: 15px 20px;
- display: inline-block;
- width: 350px;
- vertical-align: top;
- position: relative;
- height: 100%;
- }
-
- .risk-answer {
- background-color: #E8F0FE;
- padding: 15px 20px;
- display: inline-block;
- width: 340px;
- position: relative;
- height: 100%;
- }
-
- ul {
- margin-top: 0;
- }
-
- .risk-label{
- position: absolute;
- top: 0px;
- right: 0px;
- width: 52px;
- height: 16px;
- font-family: 'Google Sans', sans-serif;
- font-size: 8px;
- font-weight: 500;
- line-height: 16px;
- letter-spacing: 0.64px;
- text-align: center;
- font-weight: bold;
- border-radius: 3px;
- }
-
- .risk-label-high{
- background-color: #FCE8E6;
- color: #C5221F;
- }
-
- .risk-label-limited{
- width: 65px;
- background-color:#E4F7FB;
- color: #007B83;
- }
- '''
+ return pages
def to_pdf(self, device):
+ """Returns the current risk profile in PDF format"""
# Resolve the data as html first
html = self.to_html(device)
@@ -681,3 +428,21 @@ def to_pdf(self, device):
pdf_bytes = BytesIO()
HTML(string=html).write_pdf(pdf_bytes)
return pdf_bytes
+
+ # Adding risks to device profile questions
+ def _format_device_profile(self, device):
+ device_copy = deepcopy(device)
+ risk_map = {
+ question['question']: {
+ option['text']: option.get('risk', None)
+ for option in question['options'] if 'risk' in option
+ }
+ for question in self._device_format
+ }
+ for question in device_copy.additional_info:
+ risk = risk_map.get(
+ question['question'], {}
+ ).get(question['answer'], None)
+ if risk:
+ question['risk'] = risk
+ return device_copy
diff --git a/framework/python/src/common/statuses.py b/framework/python/src/common/statuses.py
new file mode 100644
index 000000000..4817d7cf8
--- /dev/null
+++ b/framework/python/src/common/statuses.py
@@ -0,0 +1,36 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+"""Enums for Testrun"""
+
+
+class TestrunStatus:
+ IDLE = "Idle"
+ WAITING_FOR_DEVICE = "Waiting for Device"
+ MONITORING = "Monitoring"
+ IN_PROGRESS = "In Progress"
+ CANCELLED = "Cancelled"
+ COMPLIANT = "Compliant"
+ NON_COMPLIANT = "Non-Compliant"
+ STOPPING = "Stopping"
+
+
+class TestResult:
+ IN_PROGRESS = "In Progress"
+ COMPLIANT = "Compliant"
+ NON_COMPLIANT = "Non-Compliant"
+ ERROR = "Error"
+ FEATURE_NOT_DETECTED = "Feature Not Detected"
+ INFORMATIONAL = "Informational"
+ NOT_STARTED = "Not Started"
+ DISABLED = "Disabled"
diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py
index 88a25a2b1..f9401fe80 100644
--- a/framework/python/src/common/testreport.py
+++ b/framework/python/src/common/testreport.py
@@ -17,14 +17,19 @@
from weasyprint import HTML
from io import BytesIO
from common import util
+from common.statuses import TestrunStatus
import base64
import os
from test_orc.test_case import TestCase
+from jinja2 import Environment, FileSystemLoader
+from collections import OrderedDict
DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
RESOURCES_DIR = 'resources/report'
TESTS_FIRST_PAGE = 11
TESTS_PER_PAGE = 20
+TEST_REPORT_STYLES = 'test_report_styles.css'
+TEST_REPORT_TEMPLATE = 'test_report_template.html'
# Locate parent directory
current_dir = os.path.dirname(os.path.realpath(__file__))
@@ -37,13 +42,15 @@
report_resource_dir = os.path.join(root_dir, RESOURCES_DIR)
test_run_img_file = os.path.join(report_resource_dir, 'testrun.png')
+qualification_icon = os.path.join(report_resource_dir, 'qualification-icon.png')
+pilot_icon = os.path.join(report_resource_dir, 'pilot-icon.png')
class TestReport():
"""Represents a previous Testrun report."""
def __init__(self,
- status='Non-Compliant',
+ status=TestrunStatus.NON_COMPLIANT,
started=None,
finished=None,
total_tests=0):
@@ -115,6 +122,10 @@ def to_json(self):
if test.recommendations is not None and len(test.recommendations) > 0:
test_dict['recommendations'] = test.recommendations
+ if (test.optional_recommendations is not None
+ and len(test.optional_recommendations) > 0):
+ test_dict['optional_recommendations'] = test.optional_recommendations
+
test_results.append(test_dict)
report_json['tests'] = {'total': self._total_tests,
@@ -141,6 +152,12 @@ def from_json(self, json_file):
if 'test_modules' in json_file['device']:
self._device['test_modules'] = json_file['device']['test_modules']
+ if 'test_pack' in json_file['device']:
+ self._device['test_pack'] = json_file['device']['test_pack']
+
+ if 'additional_info' in json_file['device']:
+ self._device['device_profile'] = json_file['device']['additional_info']
+
self._status = json_file['status']
self._started = datetime.strptime(json_file['started'], DATE_TIME_FORMAT)
self._finished = datetime.strptime(json_file['finished'], DATE_TIME_FORMAT)
@@ -157,8 +174,16 @@ def from_json(self, json_file):
expected_behavior=test_result['expected_behavior'],
required_result=test_result['required_result'],
result=test_result['result'])
+
+ # Add test recommendations
if 'recommendations' in test_result:
test_case.recommendations = test_result['recommendations']
+
+ # Add optional test recommendations
+ if 'optional_recommendations' in test_result:
+ test_case.optional_recommendations = test_result[
+ 'optional_recommendations']
+
self.add_test(test_case)
# Create a pdf file in memory and return the bytes
@@ -172,35 +197,81 @@ def to_pdf(self):
return pdf_bytes
def to_html(self):
- json_data = self.to_json()
- return f'''
-
-
- {self.generate_head()}
-
- {self.generate_body(json_data)}
-
-
- '''
-
- def generate_test_sections(self, json_data):
- results = json_data['tests']['results']
- sections = ''
- for result in results:
- sections += self.generate_test_section(result)
- return sections
-
- def generate_test_section(self, result):
- section_content = '
\n'
- for key, value in result.items():
- if value is not None: # Check if the value is not None
- # Replace underscores and capitalize
- formatted_key = key.replace('_', ' ').title()
- section_content += f'{formatted_key}: {value}
\n'
- section_content += '\n
\n'
- return section_content
-
- def generate_pages(self, json_data):
+
+ # Jinja template
+ template_env = Environment(loader=FileSystemLoader(report_resource_dir))
+ template = template_env.get_template(TEST_REPORT_TEMPLATE)
+ with open(os.path.join(report_resource_dir,
+ TEST_REPORT_STYLES),
+ 'r',
+ encoding='UTF-8'
+ ) as style_file:
+ styles = style_file.read()
+
+ # Load Testrun logo to base64
+ with open(test_run_img_file, 'rb') as f:
+ logo = base64.b64encode(f.read()).decode('utf-8')
+
+ json_data=self.to_json()
+
+ # Icons
+ with open(qualification_icon, 'rb') as f:
+ icon_qualification = base64.b64encode(f.read()).decode('utf-8')
+ with open(pilot_icon, 'rb') as f:
+ icon_pilot = base64.b64encode(f.read()).decode('utf-8')
+
+ # Convert the timestamp strings to datetime objects
+ start_time = datetime.strptime(json_data['started'], '%Y-%m-%d %H:%M:%S')
+ end_time = datetime.strptime(json_data['finished'], '%Y-%m-%d %H:%M:%S')
+
+ # Calculate the duration
+ duration = end_time - start_time
+
+ # Calculate number of successful tests
+ successful_tests = 0
+ for test in json_data['tests']['results']:
+ if test['result'] != 'Error':
+ successful_tests += 1
+
+ # Obtain the steps to resolve
+ steps_to_resolve = self._get_steps_to_resolve(json_data)
+
+ # Obtain optional recommendations
+ optional_steps_to_resolve = self._get_optional_steps_to_resolve(json_data)
+
+ module_reports = self._get_module_pages()
+ pages_num = self._pages_num(json_data)
+ total_pages = pages_num + len(module_reports) + 1
+ if len(steps_to_resolve) > 0:
+ total_pages += 1
+ if (len(optional_steps_to_resolve) > 0
+ and json_data['device']['test_pack'] == 'Pilot Assessment'
+ ):
+ total_pages += 1
+
+ return template.render(styles=styles,
+ logo=logo,
+ icon_qualification=icon_qualification,
+ icon_pilot=icon_pilot,
+ version=self._version,
+ json_data=json_data,
+ device=json_data['device'],
+ modules=self._device_modules(json_data['device']),
+ test_status=json_data['status'],
+ duration=duration,
+ successful_tests=successful_tests,
+ total_tests=self._total_tests,
+ test_results=json_data['tests']['results'],
+ steps_to_resolve=steps_to_resolve,
+ optional_steps_to_resolve=optional_steps_to_resolve,
+ module_reports=module_reports,
+ pages_num=pages_num,
+ total_pages=total_pages,
+ tests_first_page=TESTS_FIRST_PAGE,
+ tests_per_page=TESTS_PER_PAGE,
+ )
+
+ def _pages_num(self, json_data):
# Calculate pages
test_count = len(json_data['tests']['results'])
@@ -208,136 +279,62 @@ def generate_pages(self, json_data):
# Multiple pages required
if test_count > TESTS_FIRST_PAGE:
# First page
- full_page = 1
+ pages = 1
- # Remaining tests
+ # Remaining testsgenerate
test_count -= TESTS_FIRST_PAGE
- full_page += (int)(test_count / TESTS_PER_PAGE)
- partial_page = 1 if test_count % TESTS_PER_PAGE > 0 else 0
+ pages += (int)(test_count / TESTS_PER_PAGE)
+ pages = pages + 1 if test_count % TESTS_PER_PAGE > 0 else pages
# 1 page required
- elif test_count == TESTS_FIRST_PAGE:
- full_page = 1
- partial_page = 0
- # Less than 1 page required
else:
- full_page = 0
- partial_page = 1
+ pages = 1
- num_pages = full_page + partial_page
-
- pages = ''
- for _ in range(num_pages):
- self._cur_page += 1
- pages += self.generate_results_page(json_data=json_data,
- page_num=self._cur_page)
return pages
- def generate_results_page(self, json_data, page_num):
- page = '
'
- page += self.generate_header(json_data, (page_num == 1))
- if page_num == 1:
- page += self.generate_summary(json_data)
- page += self.generate_results(json_data, page_num)
- page += self.generate_footer(page_num)
- page += '
'
- page += '
'
- return page
-
- def generate_module_page(self, json_data, module_report):
- self._cur_page += 1
- page = '
'
- page += self.generate_header(json_data, False)
- page += f'''
-
- {module_report}
-
'''
- page += self.generate_footer(self._cur_page)
- page += '
' # Page end
- page += '
'
- return page
-
- def generate_steps_to_resolve(self, json_data):
-
- steps_so_far = 0
+ def _device_modules(self, device):
+ sorted_modules = {}
+
+ if 'test_modules' in device:
+
+ for test_module in device['test_modules']:
+ if 'enabled' in device['test_modules'][test_module]:
+ sorted_modules[
+ util.get_module_display_name(test_module)] = device['test_modules'][
+ test_module]['enabled']
+
+ # Sort the modules by enabled first
+ sorted_modules = OrderedDict(sorted(sorted_modules.items(),
+ key=lambda x:x[1],
+ reverse=True)
+ )
+ return sorted_modules
+
+ def _get_steps_to_resolve(self, json_data):
tests_with_recommendations = []
- index = 1
# Collect all tests with recommendations
for test in json_data['tests']['results']:
if 'recommendations' in test:
tests_with_recommendations.append(test)
- # Check if test has recommendations
- if len(tests_with_recommendations) == 0:
- return ''
-
- # Start new page
- self._cur_page += 1
- page = '
'
- page += self.generate_header(json_data, False)
-
- # Add title
- page += '
Steps to Resolve
'
-
- for test in tests_with_recommendations:
-
- # Generate new page
- if steps_so_far == 4 and (
- len(tests_with_recommendations) - (index-1) > 0):
-
- # Reset steps counter
- steps_so_far = 0
-
- # Render footer
- page += self.generate_footer(self._cur_page)
- page += '' # Page end
- page += '
'
-
- # Render new header
- self._cur_page += 1
- page += '
'
- page += self.generate_header(json_data, False)
-
- # Render test recommendations
- page += f'''
-
-
-
{index}.
-
- Name
{test["name"]}
-
-
- Description
{test["description"]}
-
-
-
- Steps to resolve
- '''
-
- step_number = 1
- for recommendation in test['recommendations']:
- page += f'''
-
{
- step_number}. {recommendation}'''
- step_number += 1
-
- page += '
'
-
- index += 1
- steps_so_far += 1
-
- # Render final footer
- page += self.generate_footer(self._cur_page)
- page += '
' # Page end
- page += '
'
-
- return page
-
- def generate_module_pages(self, json_data):
- pages = ''
+ return tests_with_recommendations
+
+ def _get_optional_steps_to_resolve(self, json_data):
+ tests_with_recommendations = []
+
+ # Collect all tests with recommendations
+ for test in json_data['tests']['results']:
+ if 'optional_recommendations' in test:
+ tests_with_recommendations.append(test)
+
+ return tests_with_recommendations
+
+ def _get_module_pages(self):
content_max_size = 913
+ reports = []
+
for module_reports in self._module_reports:
# ToDo: Figure out how to make this dynamic
# Padding values from CSS
@@ -391,8 +388,7 @@ def generate_module_pages(self, json_data):
# If in the middle of a table, close the table
if data_rows_active:
page_content += ''
- page = self.generate_module_page(json_data, page_content)
- pages += page + '\n'
+ reports.append(page_content)
content_size = 0
# If in the middle of a data table, restart
# it for the rest of the rows
@@ -400,727 +396,5 @@ def generate_module_pages(self, json_data):
if data_rows_active else '')
page_content += line + '\n'
if len(page_content) > 0:
- page = self.generate_module_page(json_data, page_content)
- pages += page + '\n'
- return pages
-
- def generate_body(self, json_data):
- self._num_pages = 0
- self._cur_page = 0
- body = f'''
-
- {self.generate_pages(json_data)}
- {self.generate_steps_to_resolve(json_data)}
- {self.generate_module_pages(json_data)}
-
- '''
- # Set the max pages after all pages have been generated
- return body.replace('MAX_PAGE', str(self._cur_page))
-
- def generate_footer(self, page_num):
- footer = f'''
-
- '''
- return footer
-
- def generate_results(self, json_data, page_num):
-
- successful_tests = 0
- for test in json_data['tests']['results']:
- if test['result'] != 'Error':
- successful_tests += 1
-
- result_list = f'''
-
-
Results List ({successful_tests}/{self._total_tests})
-
-
-
-
-
'''
- if page_num == 1:
- start = 0
- elif page_num == 2:
- start = TESTS_FIRST_PAGE
- else:
- start = (page_num - 2) * TESTS_PER_PAGE + TESTS_FIRST_PAGE
- results_on_page = TESTS_FIRST_PAGE if page_num == 1 else TESTS_PER_PAGE
- result_end = min(start + results_on_page,
- len(json_data['tests']['results']))
- for ix in range(result_end - start):
- result = json_data['tests']['results'][ix + start]
- result_list += self.generate_result(result)
- result_list += '
'
- return result_list
-
- def generate_result(self, result):
- if result['result'] == 'Non-Compliant':
- result_class = 'result-test-result-non-compliant'
- elif result['result'] == 'Compliant':
- result_class = 'result-test-result-compliant'
- elif result['result'] == 'Error':
- result_class = 'result-test-result-error'
- elif result['result'] == 'Feature Not Detected':
- result_class = 'result-test-result-feature-not-detected'
- elif result['result'] == 'Informational':
- result_class = 'result-test-result-informational'
- else:
- result_class = 'result-test-result-skipped'
-
- result_html = f'''
-
-
{result['name']}
-
{result['description']}
-
{result['result']}
-
- '''
- return result_html
-
- def generate_header(self, json_data, first_page):
- with open(test_run_img_file, 'rb') as f:
- tr_img_b64 = base64.b64encode(f.read()).decode('utf-8')
- header = ''
-
- if first_page:
- header += f'''
-