在 Docker 环境中使用 Otii 进行自动化测试
这些功能需要 Automation Toolbox(自动化工具箱) 软件许可证。Docker 容器在持续集成(CI)系统中具有极高的价值,因为它们能够在流水线的每个阶段都能提供一致且可复现的环境。通过将应用程序及其依赖项、运行时和配置打包到一个镜像中,容器彻底解决了经典的“在我的电脑上能运行”的问题——无论是在开发者的笔记本电脑、CI 运行节点(Runner),还是在类似于生产环境的预发布(Staging)服务器上,构建和测试的行为都完全一致。
此外,容器启动速度极快且相互隔离,这使得 CI 系统能够并行运行多个任务而互不干扰,为每次构建启动干净独立的环境,并能轻松针对多种版本的语言、数据库或服务进行兼容性测试。最重要的是,通过 CI 验证的同一个镜像可以直接晋升并部署到生产环境中,缩短了从提交到上线的路径,降低了因环境差异导致 Bug 漏检的风险。
在 Docker 中使用Otii
在 Ubuntu 系统上,只要确保将宿主机(Host)中系统必需的路径透传过去,用户就可以在 Docker 实例中运行Otii Server服务器。
- 首先,我们将向您展示如何在 Docker 容器中使用一台 Otii 设备运行测试脚本的示例。
- 随后,我们将向您展示如何在同一台 Ubuntu 服务器上并行运行多个容器,并为每个 Docker 容器分配特定的 Otii 设备。
Dockerfile 文件
以下是一个安装了 Otii Server 并运行由 Python 编写的简单测试的 Dockerfile 示例。
FROM ubuntu:24.04
RUN apt update && apt install -y curl ca-certificates
ADD https://astral.sh/uv/install.sh /uv-installer.sh
RUN sh /uv-installer.sh && rm /uv-installer.sh
ENV PATH="/root/.local/bin/:$PATH"
COPY docker_test.py /
COPY credentials.json /
COPY otii-server_3.6.4_amd64.deb /
RUN dpkg -i otii-server_3.7.0_amd64.deb
CMD [ "uv", "run", "--script", "/docker_test.py" ]在此示例中,我们使用uv来管理 Python 及其依赖项。
除了 Dockerfile 之外,我们还需要credentials.json文件,它要包含 Otii 许可证服务器凭据;Otii Server 服务器的安装包以及如下所示的测试脚本。
$ tree .
.
├── credentials.json
├── devices.json
├── Dockerfile
├── docker_test.py
├── otii-server_3.6.4_amd64.deb
├── test.py
└── test.sh
1 directory, 7 files测试脚本
这是 docker_test.py 测试脚本。
该脚本在运行测试之前会先启动 Otii Server,并在测试完成后关闭该服务器。
import subprocess
import time
from otii_tcp_client import otii_client
MEASURE_TIME = 20
class AppException(Exception):
'''Application Exception'''
def docker_test(otii: otii_client.Connect) -> None:
# Get a reference to a Arc or Ace device
devices = otii.get_devices()
if len(devices) == 0:
raise AppException('No Arc or Ace connected!')
device = devices[0]
print(device.name)
project = otii.get_active_project()
device.set_main_voltage(3.7)
device.enable_channel('mc', True)
project.start_recording()
device.set_main(True)
time.sleep(MEASURE_TIME)
device.set_main(False)
project.stop_recording()
recording = project.get_last_recording()
assert recording is not None
info = recording.get_channel_info(device.id, 'mc')
statistics = recording.get_channel_statistics(device.id, 'mc', info['from'], info['to'])
# Print the statistics
print('Statistics')
print('==========')
print(f'From: {info["from"]} s')
print(f'To: {info["to"]} s')
print(f'Offset: {info["offset"]} s')
print(f'Sample rate: {info["sample_rate"]}')
print('')
print(f'Min: {statistics["min"]:.5} A')
print(f'Max: {statistics["max"]:.5} A')
print(f'Average: {statistics["average"]:.5} A')
print(f'Energy: {statistics["energy"] / 3600:.5} Wh')
print(f'Charge: {statistics["charge"] / 3600:.5} Ah')
def main() -> None:
'''Connect to the Otii 3 application and run the measurement'''
otii_server = subprocess.Popen(['otii_server'])
time.sleep(10)
try:
client = otii_client.OtiiClient()
with client.connect() as otii:
docker_test(otii)
finally:
otii_server.terminate()
otii_server.wait()
if __name__ == '__main__':
main()在 Docker 中构建并运行测试
准备好上述 Dockerfile 和脚本后,我们就可以构建 Docker 镜像并运行它:
我们需要将宿主机上的 Otii 设备、/dev/serial 以及 /run/udev 目录透传(或挂载)给 Docker 容器。
在此示例中,Otii 设备在系统中体现为 /dev/ttyACM0。
sudo docker build -t test .
sudo docker run --device=/dev/ttyACM0 -v /dev/serial:/dev/serial:ro -v /run/udev:/run/udev:ro test运行多个容器
假设用户希望并行运行多个容器,并为每个容器分配特定的 Otii 设备。
Ubuntu 系统中的一个核心问题是,在重启等操作后,设备名称(ttyACMX)无法保证保持不变。
因此,我们需要一种方法将物理设备固定映射到当前的设备节点。如果用户始终将设备连接到相同的 USB 端口上,则可以使用 /dev/serial/by-path 中的条目来替代。
例如,以下是连接到运行 Ubuntu 24 系统的服务器上的两台 Otii Arc 设备:
$ ls -l /dev/serial/by-path
total 0
lrwxrwxrwx 1 root root 13 Apr 28 09:59 pci-0000:00:14.0-usb-0:2:1.0 -> ../../ttyACM0
lrwxrwxrwx 1 root root 13 Apr 29 09:51 pci-0000:00:14.0-usb-0:5:1.0 -> ../../ttyACM1
lrwxrwxrwx 1 root root 13 Apr 28 09:59 pci-0000:00:14.0-usbv2-0:2:1.0 -> ../../ttyACM0
lrwxrwxrwx 1 root root 13 Apr 29 09:51 pci-0000:00:14.0-usbv2-0:5:1.0 -> ../../ttyACM1如果用户每次只断开除一台之外的所有设备,您就能获得该连接通道的唯一 ID。记录下这个 ID,并对每台设备重复此操作。然后,我们将这些信息添加到名为devices.json 的配置文件中:
[{
"name":"Arc 1",
"device_path": "pci-0000:00:14.0-usb-0:2:1.0"
}, {
"name":"Arc 2",
"device_path": "pci-0000:00:14.0-usb-0:5:1.0"
}]这里的名称(name)只是一种在 Python 脚本中引用它们的方法,我们用该脚本来启动挂载了正确设备的 Docker 实例。
现在,让我们编写一个使用上述配置的Python 脚本。您将设备名称作为参数传递给该脚本,它就会在当前工作目录下启动一个 Docker 容器,并将正确的 Otii 设备连接(挂载)到该容器中:
#!/usr/bin/env python3
import argparse
import json
import os
from pathlib import Path
import subprocess
BYPATH = '/dev/serial/by-path'
DEVICES = 'devices.json'
def run_test(device_name: str) -> None:
paths = [{
'device_path': file,
'device': Path(os.path.join(BYPATH, file)).resolve()
}
for file in os.listdir(BYPATH)
]
with open(DEVICES) as file:
devices = json.load(file)
device_paths = [
device['device_path']
for device in devices
if device['name'] == device_name
]
assert(len(device_paths) == 1)
device_path = device_paths[0]
devices = [
path['device']
for path in paths
if path['device_path'] == device_path
]
assert(len(devices) == 1)
device = devices[0]
subprocess.run([
'docker',
'run',
f'--device={device}',
'-v', '/dev/serial:/dev/serial:ro',
'-v', '/run/udev:/run/udev:ro',
'test'
])
def main() -> None:
parser = argparse.ArgumentParser(description='test')
parser.add_argument(
'-n',
'--device-name',
required=True,
dest='name',
help='Device name',
)
args = parser.parse_args()
device_name = args.name
run_test(device_name)
if __name__ == '__main__':
main()
现在,我们就可以在连接了正确设备的情况下启动 Docker 容器了。
$ sudo ./test.py --device-name 'Arc 1'