diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9dabb7f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,50 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version-file: .python-version + + - name: Install dependencies + run: pip install -e ".[all]" + + - name: Build test image + run: docker build -t debian-systemd tests/images/debian-systemd + + - name: Start ssh test device + run: | + docker run -d --rm --name ssh-device --privileged --tmpfs /run --tmpfs /tmp -p 2222:22 debian-systemd + # Wait for the ssh service to be ready + for i in $(seq 1 30); do + if docker exec ssh-device systemctl is-active ssh >/dev/null 2>&1; then + echo "ssh service is ready" + break + fi + sleep 1 + done + + - name: Run tests (local, ssh, docker and docker compose adapters) + env: + SSH_CONFIG_HOSTNAME: 127.0.0.1 + SSH_CONFIG_PORT: "2222" + SSH_CONFIG_USERNAME: root + SSH_CONFIG_PASSWORD: inttest + run: python -m robot --outputdir output --name acceptance tests/acceptance + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: robot-reports + path: output/ diff --git a/.gitignore b/.gitignore index 235d093..4616d44 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ log.html report.html output.xml DeviceLibrary/_version.py +output/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..3767b4b --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 \ No newline at end of file diff --git a/DeviceLibrary/DeviceLibrary.py b/DeviceLibrary/DeviceLibrary.py index ce6dcb0..ee0cac1 100644 --- a/DeviceLibrary/DeviceLibrary.py +++ b/DeviceLibrary/DeviceLibrary.py @@ -6,7 +6,7 @@ """ import logging -from typing import Any, Dict, List, Union, Optional +from typing import Any, Dict, List, Tuple, Union, Optional from datetime import datetime, timezone import re import os @@ -90,6 +90,8 @@ def __init__( bootstrap_script: str = DEFAULT_BOOTSTRAP_SCRIPT, ): self.devices: Dict[str, DeviceAdapter] = {} + # Compose stacks indexed by the serial number of their main device + self._compose_stacks: Dict[str, Any] = {} self._bootstrap_scripts: Dict[str, str] = {} self.devices_setup_times = {} self.__image = image @@ -173,6 +175,7 @@ def end_suite(self, _data: Any, result: Any): logger.info("Suite %s (%s) ending", result.name, result.message) self.teardown() self.devices.clear() + self._compose_stacks.clear() def end_test(self, _data: Any, result: Any): """End test hook which is called by Robot Framework @@ -255,6 +258,83 @@ def get_unix_timestamp_from_host( return time.time() return int(time.time()) + def _get_extra_hosts(self, env_file: str) -> Dict[str, str]: + """Read any environment variables which contain a host to ip mapping + as they will be added to the /etc/hosts list of docker/compose devices + to reduce any problems with external ip addresses + + Example env variable: + DEVICELIBRARY_HOST_MYDOMAIN="example.mydomain.com=1.2.3.4" + """ + extra_hosts = {} + if os.path.exists(env_file): + env_values = dotenv.dotenv_values(env_file) + hosts = [ + key + for key in env_values.keys() + if key.startswith("DEVICELIBRARY_HOST_") + ] + + for key in hosts: + entry = env_values.get(key) + if entry: + hostname, _, ip_address = entry.partition("=") + hostname = re.sub(r"^\w+://", "", hostname) + if hostname and ip_address: + extra_hosts[hostname] = ip_address + return extra_hosts + + def _setup_compose_stack( + self, + device_sn: str, + compose_file: str, + env_file: str, + config: Dict[str, Any], + ) -> DeviceAdapter: + """Create a docker compose stack and register every service of the + stack as a device. The main device (under test) is returned, all other + services are addressable using ':' + """ + try: + from device_test_core.compose.factory import ComposeDeviceFactory + + compose_factory = ComposeDeviceFactory() + except (ImportError, AttributeError): + raise_adapter_error("docker") + + env = config.pop("env", None) or {} + stack = compose_factory.create_stack( + compose_file, + device_service=config.pop("device_service", None), + env_file=env_file, + env={**env, "DEVICE_ID": device_sn}, + extra_hosts=self._get_extra_hosts(env_file), + **config, + ) + self._compose_stacks[device_sn] = stack + + device = stack.get_device( + stack.device_service, name=device_sn, device_id=device_sn + ) + + # Register the other services of the stack so they are addressable + # via device_name=: + for service_name in stack.services: + if service_name == stack.device_service: + continue + service_device = stack.get_device( + service_name, + name=f"{device_sn}:{service_name}", + device_id=device_sn, + ) + # The stack is torn down via the main device to avoid services + # triggering the (idempotent) stack cleanup multiple times + service_device.should_cleanup = False + configure_retry_on_members(service_device, "^assert_command") + self.devices[service_device.name] = service_device + + return device + @keyword("Setup") def setup( self, @@ -271,6 +351,20 @@ def setup( from the library settings which controls what device interface is used, e.g. docker or ssh. + Docker adapter: + If a 'compose_file' is provided (either as keyword argument or via + the &{DOCKER_CONFIG} variable), then the whole stack defined in the + given docker compose file is created instead of a single container. + Each setup gets its own isolated compose project (unique project + name, network, volumes), so test suites can run in parallel. + + One service of the stack acts as the main device under test (see + the 'device_service' argument). All other services are registered + as additional devices using the name ':', e.g. + + | ${SERIAL}= Setup compose_file=${CURDIR}/docker-compose.yaml | + | Execute Command ls -l device_name=${SERIAL}:broker | + Args: skip_bootstrap (bool, optional): Don't run the bootstrap script. Defaults to None bootstrap_args (str, optional): Additional arguments to be passed to the bootstrap @@ -278,7 +372,14 @@ def setup( cleanup (bool, optional): Should the cleanup be run or not. Defaults to None adapter (str, optional): Type of adapter to use, e.g. ssh, docker etc. Defaults to None **adaptor_config: Additional configuration that is passed to the adapter. It will override - any existing settings. + any existing settings. Notable docker adapter settings: + compose_file (str): Path to a docker compose file. The whole + stack will be created (compose mode). + device_service (str): Name of the compose service acting as the + main device under test (compose mode). If not set, it is + resolved from the compose file (label + 'device-test-core.role: main', single service, or a service + named 'device') Returns: str: Device serial number @@ -316,49 +417,35 @@ def setup( bootstrap_script = config.pop("bootstrap_script", self.__bootstrap_script) if adapter_type == "docker": - docker_device_factory = None - try: - from device_test_core.docker.factory import DockerDeviceFactory - - docker_device_factory = DockerDeviceFactory() - except (ImportError, AttributeError): - raise_adapter_error(adapter_type) - device_sn = normalize_container_name(generate_custom_name()) - - # Use any env variables which contain a host to ip mapping - # as it will be added to the docker /etc/hosts list to reduce - # any problems with external ip addresses - # Example env variable: - # DEVICELIBRARY_HOST_MYDOMAIN="example.mydomain.com=1.2.3.4" - # - extra_hosts = {} - if os.path.exists(env_file): - env_values = dotenv.dotenv_values(env_file) - hosts = [ - key - for key in env_values.keys() - if key.startswith("DEVICELIBRARY_HOST_") - ] - - for key in hosts: - entry = env_values.get(key) - if entry: - hostname, _, ip_address = entry.partition("=") - hostname = re.sub(r"^\w+://", "", hostname) - if hostname and ip_address: - extra_hosts[hostname] = ip_address - - if docker_device_factory is None: - raise Exception(f"Could not import adapter. type={adapter_type}") - - device = docker_device_factory.create_device( - device_sn, - image=config.pop("image", self.__image), - env_file=env_file, - extra_hosts=extra_hosts, - **config, - ) + compose_file = config.pop("compose_file", None) + + if compose_file: + device = self._setup_compose_stack( + device_sn, + compose_file, + env_file=env_file, + config=config, + ) + else: + docker_device_factory = None + try: + from device_test_core.docker.factory import DockerDeviceFactory + + docker_device_factory = DockerDeviceFactory() + except (ImportError, AttributeError): + raise_adapter_error(adapter_type) + + if docker_device_factory is None: + raise Exception(f"Could not import adapter. type={adapter_type}") + + device = docker_device_factory.create_device( + device_sn, + image=config.pop("image", self.__image), + env_file=env_file, + extra_hosts=self._get_extra_hosts(env_file), + **config, + ) elif adapter_type == "ssh": ssh_device_factory = None try: @@ -581,6 +668,95 @@ def connect_network(self, device_name: Optional[str] = None): """ self.get_device(device_name).connect_network() + def _get_compose_stack(self, device_name: Optional[str] = None): + """Get the compose stack which a device belongs to + + Args: + device_name (optional, str): Device. Defaults to the current device. + + Raises: + AssertionError: The device was not created from a compose file + """ + device = self.get_device(device_name) + base_name, _, _ = device.name.partition(":") + stack = self._compose_stacks.get(base_name) + assert stack, ( + f"Device '{device.name}' was not created from a docker compose file. " + "This keyword requires the device to be created using " + "'Setup compose_file=...'" + ) + return stack + + @keyword("Get Service Port") + def get_service_port( + self, + service: str, + port: Union[int, str], + protocol: str = "tcp", + device_name: Optional[str] = None, + ) -> Tuple[str, int]: + """Get the host address/port under which an (ephemeral) published + container port of a compose service is reachable from the test host. + + Only available for devices created from a docker compose file. The + service must publish the port without a fixed host port, e.g. + 'ports: ["1883"]', so that parallel test runs do not conflict. + + Examples: + + | ${HOST} ${PORT}= Get Service Port service=broker port=1883 | + + Args: + service (str): Compose service name + port (Union[int, str]): Container port, e.g. 1883 + protocol (str, optional): Port protocol. Defaults to 'tcp'. + device_name (optional, str): Device + + Returns: + Tuple[str, int]: Host address and host port + """ + stack = self._get_compose_stack(device_name) + host, host_port = stack.get_service_port(service, int(port), protocol) + return host, host_port + + @keyword("Get Service Logs") + def get_service_logs( + self, + service: Optional[str] = None, + device_name: Optional[str] = None, + show: bool = True, + ) -> List[str]: + """Get the container logs (docker compose logs) of one or all services + of the compose stack a device belongs to. + + Unlike 'Get Logs' (which reads journalctl inside the device), this + keyword reads the container output of the services, which is useful + for supporting services that don't run systemd (e.g. brokers, + registries, simulators). + + Only available for devices created from a docker compose file. + + Examples: + + | ${lines}= Get Service Logs service=broker | + | ${lines}= Get Service Logs | + + Args: + service (str, optional): Only include logs of the given service. + Defaults to all services. + device_name (optional, str): Device + show (bool, optional): Show/Display the log entries + + Returns: + List[str]: Log lines + """ + stack = self._get_compose_stack(device_name) + log_output = stack.get_logs(service=service) + if show: + for line in log_output: + print(line) + return log_output + def teardown(self): """Stop and cleanup the device""" for name, device in self.devices.items(): diff --git a/README.md b/README.md index 118d82a..5af2e8c 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,76 @@ Robot Framework Library for creating and interacting with devices using various robot tests/Example.robot ``` +## Docker Compose setups + +If a test setup needs more than a single container (e.g. a device plus a broker or other supporting services), pass a docker compose file to the `Setup` keyword and the whole stack will be created: + +```robot +*** Settings *** +Library DeviceLibrary adapter=docker + +*** Test Cases *** +Device with supporting services + ${SERIAL}= Setup compose_file=${CURDIR}/docker-compose.yaml + + # commands run on the main device under test by default + Execute Command tedge connect c8y + + # but every service of the stack is addressable via : + Execute Command mosquitto_sub -t 'te/#' -C 1 device_name=${SERIAL}:broker + + # resolve the dynamically assigned host port of a published container port + ${HOST} ${PORT}= Get Service Port service=broker port=1883 + + # container logs of supporting (non systemd) services + ${lines}= Get Service Logs service=broker + + # simulate a network outage of the device only (the rest of the stack stays up) + Disconnect From Network + Connect To Network +``` + +With an example compose file. The label marks which service acts as the main device under test (alternatively pass `device_service=` to `Setup`, or name the service `device`): + +```yaml +services: + device: + image: debian-systemd + labels: + device-test-core.role: main + broker: + image: eclipse-mosquitto:2 + ports: + - "1883" +``` + +Each `Setup` creates the stack under a unique compose project name, so all containers, networks and volumes are isolated per test setup and suites can run in parallel. To keep it that way, the compose file must not use `container_name`, fixed host ports (e.g. `8080:80`, use ephemeral ports like `"80"` plus the `Get Service Port` keyword instead), or external/fixed-name networks and volumes. The compose file is validated and the setup is rejected with an explanatory error if it contains such settings. + +The `compose_file` and `device_service` settings can also be provided via the `&{DOCKER_CONFIG}` variable instead of keyword arguments. + +## Development + +The project uses [just](https://github.com/casey/just) to run the common development tasks. + +```sh +# Create the python virtual environment (editable install with all adapters) +just venv + +# Run the tests (local, ssh, docker and docker compose adapters) +just build-test-images +just start-ssh-device +SSH_CONFIG_HOSTNAME=127.0.0.1 SSH_CONFIG_PORT=2222 SSH_CONFIG_USERNAME=root SSH_CONFIG_PASSWORD=inttest just test +just stop-ssh-device +``` + +**Notes** + +* You can also run a subset of the tests by passing additional robot arguments, e.g. `just test --suite compose` only runs the docker compose tests (which don't need the test image or the ssh device). +* The tests use the `debian-systemd` image built by `just build-test-images`. The same image is started by `just start-ssh-device` and used as the target device for the ssh adapter tests. +* The `local` adapter tests execute commands on your own machine using sudo, so they require passwordless sudo (as is the case on the GitHub Actions runners). Without it those tests will fail locally. + +The same tests are run on every push/pull request by the [test workflow](.github/workflows/test.yml). + ## Library docs Checkout the [DeviceLibrary docs](./docs/DeviceLibrary.rst) diff --git a/justfile b/justfile new file mode 100644 index 0000000..fa31375 --- /dev/null +++ b/justfile @@ -0,0 +1,41 @@ + +# Note: Windows stores virtual environment scripts under Scripts/ directory instead of bin/ +venv_bin := if os_family() == "windows" { ".venv/Scripts" } else { ".venv/bin" } + +ssh_device_name := "devicelibrary-ssh-device" + +# Install python virtual environment +venv: + [ -d .venv ] || python3 -m venv .venv + {{venv_bin}}/python3 -m pip install -e ".[all]" + +# Build the docker image used by the acceptance tests +# (it is also used as the target device for the ssh adapter tests) +build-test-images: + docker build -t debian-systemd tests/images/debian-systemd + +# Start an ssh-able test device to run the ssh adapter acceptance tests against +start-ssh-device port="2222": build-test-images + # Remove any stale known_hosts entry as the image generates a new host key on each build + -ssh-keygen -R "[127.0.0.1]:{{port}}" 2>/dev/null + docker run -d --rm --name {{ssh_device_name}} --privileged --tmpfs /run --tmpfs /tmp -p {{port}}:22 debian-systemd + @echo "" + @echo "Use the following settings (e.g. add them to your .env file):" + @echo " SSH_CONFIG_HOSTNAME=127.0.0.1" + @echo " SSH_CONFIG_PORT={{port}}" + @echo " SSH_CONFIG_USERNAME=root" + @echo " SSH_CONFIG_PASSWORD=inttest" + +# Stop the ssh test device +stop-ssh-device: + docker rm -f {{ssh_device_name}} + +# Run the tests (local, ssh, docker and docker compose adapters). +# Requires the test image (build-test-images) and a running ssh test +# device (start-ssh-device) +test *ARGS: + {{venv_bin}}/python3 -m robot --outputdir output {{ARGS}} tests/acceptance + +# Generate the keyword documentation +docs: + {{venv_bin}}/python3 -m robot.libdoc DeviceLibrary/DeviceLibrary.py docs/DeviceLibrary.rst diff --git a/pyproject.toml b/pyproject.toml index 2458d2f..6e9b411 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dynamic = ["version"] dependencies = [ "robotframework >= 6.0.0, < 8.0.0", "unidecode >= 1.3.6, < 2.0.0", - "device-test-core @ git+https://github.com/reubenmiller/device-test-core.git@1.15.1", + "device-test-core @ git+https://github.com/reubenmiller/device-test-core.git@1.16.0", ] [project.optional-dependencies] @@ -33,13 +33,13 @@ all = [ "robotframework-devicelibrary[local]", ] ssh = [ - "device-test-core[ssh] @ git+https://github.com/reubenmiller/device-test-core.git@1.15.1", + "device-test-core[ssh] @ git+https://github.com/reubenmiller/device-test-core.git@1.16.0", ] local = [ - "device-test-core[local] @ git+https://github.com/reubenmiller/device-test-core.git@1.15.1", + "device-test-core[local] @ git+https://github.com/reubenmiller/device-test-core.git@1.16.0", ] docker = [ - "device-test-core[docker] @ git+https://github.com/reubenmiller/device-test-core.git@1.15.1", + "device-test-core[docker] @ git+https://github.com/reubenmiller/device-test-core.git@1.16.0", ] test = [ diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/acceptance/common.resource b/tests/acceptance/common.resource index f4fc5cb..18947f9 100644 --- a/tests/acceptance/common.resource +++ b/tests/acceptance/common.resource @@ -2,5 +2,5 @@ # Adapter settings ${DEVICE_ADAPTER} docker -&{SSH_CONFIG} hostname=%{SSH_CONFIG_HOSTNAME= } username=%{SSH_CONFIG_USERNAME= } password=%{SSH_CONFIG_PASSWORD= } configpath=%{SSH_CONFIG_CONFIGPATH= } +&{SSH_CONFIG} hostname=%{SSH_CONFIG_HOSTNAME= } port=%{SSH_CONFIG_PORT= } username=%{SSH_CONFIG_USERNAME= } password=%{SSH_CONFIG_PASSWORD= } configpath=%{SSH_CONFIG_CONFIGPATH= } &{DOCKER_CONFIG} image=%{DOCKER_CONFIG_IMAGE=debian-systemd} diff --git a/tests/acceptance/compose/compose.robot b/tests/acceptance/compose/compose.robot new file mode 100644 index 0000000..b5dd681 --- /dev/null +++ b/tests/acceptance/compose/compose.robot @@ -0,0 +1,39 @@ +*** Settings *** +Documentation Integration tests for the docker compose support. +... Requires a running docker daemon with the compose v2 plugin. +Library DeviceLibrary adapter=docker + +*** Test Cases *** +Create A Stack From A Compose File + ${SERIAL}= Setup skip_bootstrap=${True} compose_file=${CURDIR}/docker-compose.yaml + # main device (labelled with device-test-core.role: main) answers by default + ${output}= Execute Command echo device says $DEVICE_ID strip=${True} + Should Be Equal ${output} device says ${SERIAL} + # sidecar services are addressable via : + ${output}= Execute Command hostname strip=${True} device_name=${SERIAL}:helper + Should Not Be Empty ${output} + # services reach each other via service names on the isolated project network + Execute Command nc -z -w 5 web 80 + # ephemeral published ports can be resolved to the assigned host port + ${HOST} ${PORT}= Get Service Port service=web port=80 + Should Not Be Empty ${HOST} + Should Be True ${PORT} > 0 + # container logs of supporting (non systemd) services + ${lines}= Get Service Logs service=web show=${False} + Should Not Be Empty ${lines} + +Simulate Network Outage Of The Device Only + ${SERIAL}= Setup skip_bootstrap=${True} compose_file=${CURDIR}/docker-compose.yaml + Disconnect From Network + Execute Command nc -z -w 2 web 80 exp_exit_code=!0 + # the rest of the stack is unaffected + Execute Command nc -z -w 5 web 80 device_name=${SERIAL}:helper + Connect To Network + Execute Command nc -z -w 5 web 80 + +Use A Different Service As The Main Device + ${SERIAL}= Setup skip_bootstrap=${True} compose_file=${CURDIR}/docker-compose.yaml device_service=helper + ${output}= Execute Command echo i am the helper strip=${True} + Should Be Equal ${output} i am the helper + ${output}= Execute Command echo i am the device strip=${True} device_name=${SERIAL}:device + Should Be Equal ${output} i am the device diff --git a/tests/acceptance/compose/docker-compose.yaml b/tests/acceptance/compose/docker-compose.yaml new file mode 100644 index 0000000..80987a1 --- /dev/null +++ b/tests/acceptance/compose/docker-compose.yaml @@ -0,0 +1,15 @@ +services: + device: + image: alpine:3.23 + command: sleep infinity + environment: + - DEVICE_ID=${DEVICE_ID:-unknown} + labels: + device-test-core.role: main + helper: + image: alpine:3.23 + command: sleep infinity + web: + image: nginx:alpine + ports: + - "80" diff --git a/tests/acceptance/docker/docker.robot b/tests/acceptance/docker/docker.robot new file mode 100644 index 0000000..49c1370 --- /dev/null +++ b/tests/acceptance/docker/docker.robot @@ -0,0 +1,20 @@ +*** Settings *** +Documentation Integration tests for the single container docker adapter. +... Requires a running docker daemon. The tests are self-contained +... and only use public images. +Library DeviceLibrary adapter=docker + +*** Test Cases *** +Create A Single Container Device + ${SERIAL}= Setup skip_bootstrap=${True} image=alpine:3.19 + ${output}= Execute Command echo device $DEVICE_ID strip=${True} + Should Be Equal ${output} device ${SERIAL} + +Create Multiple Devices In One Suite + ${DEVICE1}= Setup skip_bootstrap=${True} image=alpine:3.19 + ${DEVICE2}= Setup skip_bootstrap=${True} image=alpine:3.19 + Should Not Be Equal ${DEVICE1} ${DEVICE2} + ${output}= Execute Command echo $DEVICE_ID strip=${True} device_name=${DEVICE1} + Should Be Equal ${output} ${DEVICE1} + ${output}= Execute Command echo $DEVICE_ID strip=${True} device_name=${DEVICE2} + Should Be Equal ${output} ${DEVICE2} diff --git a/tests/images/debian-systemd/Dockerfile b/tests/images/debian-systemd/Dockerfile new file mode 100644 index 0000000..95b3845 --- /dev/null +++ b/tests/images/debian-systemd/Dockerfile @@ -0,0 +1,41 @@ +# Test image used by the acceptance tests. +# +# It mimics a typical debian device running systemd, and is used both as +# the docker adapter's device image and as the target device for the ssh +# adapter tests (it runs an openssh-server which allows root login using +# a password). +# +# NOTE: This image is strictly for testing, don't use it for anything else! +FROM debian:12-slim + +ARG ROOT_PASSWORD=inttest + +ENV DEBIAN_FRONTEND=noninteractive +ENV container=docker + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + systemd \ + systemd-sysv \ + dbus \ + openssh-server \ + sudo \ + curl \ + ca-certificates \ + procps \ + && rm -rf /var/lib/apt/lists/* + +# Allow logging in as root via ssh with a password (test image only!) +RUN echo "root:${ROOT_PASSWORD}" | chpasswd \ + && sed -i 's/#\?PermitRootLogin .*/PermitRootLogin yes/' /etc/ssh/sshd_config \ + && sed -i 's/#\?PasswordAuthentication .*/PasswordAuthentication yes/' /etc/ssh/sshd_config \ + && systemctl enable ssh + +# Debian ships 'D /tmp' which makes systemd-tmpfiles wipe the contents of +# /tmp during boot, racing with tests which start as soon as the container +# is running. Override it so /tmp is left alone ('d' creates but never purges) +RUN echo 'd /tmp 1777 root root -' > /etc/tmpfiles.d/tmp.conf + +STOPSIGNAL SIGRTMIN+3 + +CMD ["/lib/systemd/systemd"] diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_device_library.py b/tests/integration/test_device_library.py deleted file mode 100644 index 754933b..0000000 --- a/tests/integration/test_device_library.py +++ /dev/null @@ -1,6 +0,0 @@ -from DeviceLibrary import DeviceLibrary - - -def test_docker_device(): - adapter = DeviceLibrary(adapter="docker") - adapter.start()