Skip to content

Commit 98118b1

Browse files
authored
Merge pull request #104 from HiLivin/jakuza_docker_dlt
Enable parallel dlt tests
2 parents b785d74 + 875182f commit 98118b1

7 files changed

Lines changed: 285 additions & 51 deletions

File tree

score/itf/plugins/dlt/__init__.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@
1111
# SPDX-License-Identifier: Apache-2.0
1212
# *******************************************************************************
1313
import json
14+
import logging
15+
from contextlib import contextmanager
16+
1417
import pytest
1518

1619
from score.itf.core.utils.bunch import Bunch
17-
from score.itf.plugins.core import determine_target_scope
18-
from score.itf.plugins.dlt.dlt_receive import DltReceive, Protocol
20+
from score.itf.plugins.dlt.dlt_receive import DltReceive, Protocol, protocol_arguments
21+
22+
23+
logger = logging.getLogger(__name__)
1924

2025

2126
def pytest_addoption(parser):
@@ -31,6 +36,12 @@ def pytest_addoption(parser):
3136
required=True,
3237
help="Path to dlt-receive binary.",
3338
)
39+
parser.addoption(
40+
"--dlt-receive-on-target-path",
41+
action="store",
42+
required=False,
43+
help="Path to dlt-receive binary cross-compiled for the target platform.",
44+
)
3445

3546

3647
@pytest.fixture(scope="session")
@@ -66,3 +77,84 @@ def dlt(dlt_config):
6677
binary_path=dlt_config.dlt_receive_path,
6778
):
6879
yield
80+
81+
82+
_DLT_RECEIVE_REMOTE_PATH = "/tmp/dlt-receive"
83+
_DLT_OUTPUT_DIR = "/tmp"
84+
85+
86+
class DltReceiver:
87+
"""Thin wrapper around an :class:`AsyncProcess` that also tracks the DLT output file."""
88+
89+
def __init__(self, proc, dlt_file=None):
90+
self._proc = proc
91+
self.dlt_file = dlt_file
92+
93+
def __getattr__(self, name):
94+
return getattr(self._proc, name)
95+
96+
97+
@pytest.fixture()
98+
def dlt_on_target(request, target, dlt_config):
99+
"""Upload ``dlt-receive`` to the target and yield a factory for starting it.
100+
101+
The factory returns a :class:`DltReceiver` handle that delegates to the
102+
underlying :class:`~score.itf.core.process.async_process.AsyncProcess`.
103+
All receivers started via the factory are stopped automatically when the
104+
fixture tears down.
105+
106+
Example usage::
107+
108+
def test_example(target, dlt_on_target):
109+
with target.wrap_exec("/usr/bin/dlt-daemon"):
110+
with dlt_on_target(Protocol.UDP, multicast_ips=["224.0.0.1"]) as receiver:
111+
# ... send messages ...
112+
pass
113+
assert "expected" in receiver.get_output()
114+
target.download(receiver.dlt_file, "local_trace.dlt")
115+
"""
116+
# Note: Currently dlt_on_target is only used on docker Linux,
117+
# so we default to the host-built binary
118+
on_target_path = request.config.getoption("dlt_receive_on_target_path", default=None)
119+
local_binary = on_target_path or dlt_config.dlt_receive_path
120+
121+
target.upload(local_binary, _DLT_RECEIVE_REMOTE_PATH)
122+
target.execute(f"chmod +x {_DLT_RECEIVE_REMOTE_PATH}")
123+
124+
receivers = []
125+
_counter = 0
126+
127+
@contextmanager
128+
def start(
129+
protocol,
130+
host_ip="127.0.0.1",
131+
target_ip="127.0.0.1",
132+
multicast_ips=None,
133+
print_to_stdout=True,
134+
output_file=None,
135+
):
136+
nonlocal _counter
137+
_counter += 1
138+
dlt_file = output_file or f"{_DLT_OUTPUT_DIR}/dlt-receive-{_counter}.dlt"
139+
140+
args = protocol_arguments(protocol, host_ip, target_ip, multicast_ips or [])
141+
args += ["-o", dlt_file]
142+
if print_to_stdout:
143+
args += ["-a", "--stdout-flush"]
144+
proc = target.execute_async(_DLT_RECEIVE_REMOTE_PATH, args=args)
145+
receiver = DltReceiver(proc, dlt_file=dlt_file)
146+
receivers.append(proc)
147+
try:
148+
yield receiver
149+
finally:
150+
if proc.is_running():
151+
proc.stop()
152+
153+
yield start
154+
155+
for proc in receivers:
156+
try:
157+
if proc.is_running():
158+
proc.stop()
159+
except Exception:
160+
logger.warning("Failed to stop on-target dlt-receive", exc_info=True)

score/itf/plugins/docker.py

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,10 @@ def get_output(self) -> str:
134134

135135

136136
class DockerTarget(Target):
137-
def __init__(self, container):
137+
def __init__(self, container, network=None):
138138
super().__init__()
139139
self.container = container
140+
self.network = network
140141
self._client = pypi_docker.from_env(timeout=DOCKER_CLIENT_TIMEOUT)
141142

142143
def __getattr__(self, name):
@@ -248,13 +249,36 @@ def download(self, remote_path: str, local_path: str) -> None:
248249
def restart(self) -> None:
249250
self.container.restart()
250251

251-
def get_ip(self):
252-
self.container.reload()
253-
return self.container.attrs["NetworkSettings"]["Networks"]["bridge"]["IPAddress"]
252+
def _network_attr(self, key, network=None):
253+
"""Return a NetworkSettings attribute for the given Docker network.
254254
255-
def get_gateway(self):
255+
If *network* is ``None`` and the target was created with a dedicated
256+
network, that network is used. Otherwise the value from the first
257+
attached network that has a non-empty value for *key* is returned.
258+
"""
259+
if network is None and self.network is not None:
260+
network = self.network.name
256261
self.container.reload()
257-
return self.container.attrs["NetworkSettings"]["Networks"]["bridge"]["Gateway"]
262+
networks = self.container.attrs["NetworkSettings"]["Networks"]
263+
if network is not None:
264+
if network not in networks:
265+
raise RuntimeError(f"Container {self.container.short_id} is not attached to network '{network}'")
266+
return networks[network][key]
267+
value = next(
268+
(v.get(key) for v in networks.values() if v.get(key, "") != ""),
269+
None,
270+
)
271+
if value is None:
272+
raise RuntimeError(f"Container {self.container.short_id} has no {key} on any network")
273+
return value
274+
275+
def get_ip(self, network=None):
276+
"""Return the container IP on the given Docker network."""
277+
return self._network_attr("IPAddress", network)
278+
279+
def get_gateway(self, network=None):
280+
"""Return the gateway IP on the given Docker network."""
281+
return self._network_attr("Gateway", network)
258282

259283
def ssh(self, username="score", password="score", port=2222):
260284
return Ssh(target_ip=self.get_ip(), port=port, username=username, password=password)
@@ -322,25 +346,40 @@ def target_init(request, _docker_configuration):
322346

323347
docker_image = request.config.getoption("docker_image")
324348
client = pypi_docker.from_env(timeout=DOCKER_CLIENT_TIMEOUT)
349+
325350
known_keys = {"command", "init", "environment", "volumes", "shm_size", "detach", "auto_remove"}
326351
reserved_overrides = {k for k in ("detach", "auto_remove") if k in _docker_configuration}
327352
if reserved_overrides:
328353
logger.warning(f"docker_configuration contains reserved keys {reserved_overrides} which will be ignored")
329354
extra_kwargs = {k: v for k, v in _docker_configuration.items() if k not in known_keys}
330-
container = client.containers.run(
331-
docker_image,
332-
_docker_configuration["command"],
333-
detach=True,
334-
auto_remove=False,
335-
init=_docker_configuration["init"],
336-
environment=_docker_configuration["environment"],
337-
volumes=_docker_configuration["volumes"],
338-
shm_size=_docker_configuration["shm_size"],
339-
**extra_kwargs,
355+
356+
# Create a per-container bridge network so that get_ip() / get_gateway()
357+
# return addresses unique to this container.
358+
network = client.networks.create(
359+
f"score_itf_{os.urandom(8).hex()}",
360+
driver="bridge",
340361
)
362+
363+
try:
364+
container = client.containers.run(
365+
docker_image,
366+
_docker_configuration["command"],
367+
detach=True,
368+
auto_remove=False,
369+
init=_docker_configuration["init"],
370+
environment=_docker_configuration["environment"],
371+
volumes=_docker_configuration["volumes"],
372+
shm_size=_docker_configuration["shm_size"],
373+
network=network.name,
374+
**extra_kwargs,
375+
)
376+
except Exception:
377+
network.remove()
378+
raise
379+
341380
target = None
342381
try:
343-
target = DockerTarget(container)
382+
target = DockerTarget(container, network=network)
344383
yield target
345384
finally:
346385
try:
@@ -352,7 +391,13 @@ def target_init(request, _docker_configuration):
352391
except Exception:
353392
logger.warning("Coverage extraction failed", exc_info=True)
354393
try:
355-
container.stop(timeout=1)
394+
try:
395+
container.stop(timeout=1)
396+
finally:
397+
# Ensure restart() doesn't accidentally delete the container mid-test.
398+
container.remove(force=True)
356399
finally:
357-
# Ensure restart() doesn't accidentally delete the container mid-test.
358-
container.remove(force=True)
400+
try:
401+
network.remove()
402+
except Exception:
403+
logger.warning(f"Failed to remove network {network.name}", exc_info=True)

score/itf/plugins/qemu/qemu_target.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ def execute_async(self, binary_path, args=None, cwd="/", **kwargs) -> QemuAsyncP
172172
try:
173173
transport = ssh_ctx.get_paramiko_client().get_transport()
174174
channel = transport.open_session()
175+
channel.set_combine_stderr(True)
175176
inner = (
176177
f"[ -r /etc/profile ] && . /etc/profile >/dev/null 2>&1; echo $$; cd {shlex.quote(cwd)} && {command}"
177178
)

test/BUILD

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,22 @@ py_itf_test(
5050
],
5151
)
5252

53+
py_itf_test(
54+
name = "test_dlt_on_target",
55+
srcs = [
56+
"test_dlt_on_target.py",
57+
],
58+
args = [
59+
"--docker-image-bootstrap=$(location //test/resources:image_load)",
60+
"--docker-image=score_itf_examples:latest",
61+
],
62+
data = ["//test/resources:image_load"],
63+
plugins = [
64+
"//score/itf/plugins:dlt_plugin",
65+
"//score/itf/plugins:docker_plugin",
66+
],
67+
)
68+
5369
py_itf_test(
5470
name = "test_ssh",
5571
srcs = [

test/test_dlt.py

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,6 @@ def test_dlt_custom_config(target, dlt_config):
4444
time.sleep(1)
4545

4646

47-
def get_container_ip(target):
48-
target.reload()
49-
return target.attrs["NetworkSettings"]["Networks"]["bridge"]["IPAddress"]
50-
51-
52-
def get_docker_network_gateway(target):
53-
target.reload()
54-
return target.attrs["NetworkSettings"]["Networks"]["bridge"]["Gateway"]
55-
56-
5747
def send_secret_dlt_message(target):
5848
for i in range(10):
5949
target.execute(f'/bin/sh -c "echo -n message{i} | /usr/bin/dlt-adaptor-stdin"')
@@ -65,19 +55,17 @@ def send_secret_dlt_message(target):
6555

6656

6757
def test_dlt_direct_tcp(target, dlt_config, caplog):
68-
ipaddress = get_container_ip(target)
6958
target.execute(f"/usr/bin/dlt-daemon -d")
7059

7160
with DltReceive(
7261
protocol=Protocol.TCP,
73-
target_ip=ipaddress,
62+
target_ip=target.get_ip(),
7463
print_to_stdout=True,
7564
logger_name="fixed_dlt_receive",
7665
binary_path=dlt_config.dlt_receive_path,
7766
):
7867
send_secret_dlt_message(target)
7968

80-
captured_logs = []
8169
for record in caplog.records:
8270
if record.name == "fixed_dlt_receive":
8371
if "This is a secret message" in record.getMessage():
@@ -87,21 +75,18 @@ def test_dlt_direct_tcp(target, dlt_config, caplog):
8775

8876

8977
def test_dlt_multicast_udp(target, dlt_config, caplog):
90-
ipaddress = get_container_ip(target)
91-
gateway = get_docker_network_gateway(target)
9278
target.execute(f"/usr/bin/dlt-daemon -d")
9379

9480
with DltReceive(
9581
protocol=Protocol.UDP,
96-
host_ip=gateway,
82+
host_ip=target.get_gateway(),
9783
multicast_ips=["224.0.0.1"],
9884
print_to_stdout=True,
9985
logger_name="fixed_dlt_receive",
10086
binary_path=dlt_config.dlt_receive_path,
10187
):
10288
send_secret_dlt_message(target)
10389

104-
captured_logs = []
10590
for record in caplog.records:
10691
if record.name == "fixed_dlt_receive":
10792
if "This is a secret message" in record.getMessage():
@@ -111,13 +96,11 @@ def test_dlt_multicast_udp(target, dlt_config, caplog):
11196

11297

11398
def test_dlt_window_no_stdout(target, dlt_config):
114-
ipaddress = get_container_ip(target)
115-
gateway = get_docker_network_gateway(target)
11699
target.execute(f"/usr/bin/dlt-daemon -d")
117100

118101
with DltWindow(
119102
protocol=Protocol.UDP,
120-
host_ip=gateway,
103+
host_ip=target.get_gateway(),
121104
multicast_ips=["224.0.0.1"],
122105
print_to_stdout=False,
123106
binary_path=dlt_config.dlt_receive_path,
@@ -128,13 +111,11 @@ def test_dlt_window_no_stdout(target, dlt_config):
128111

129112

130113
def test_dlt_window_stdout(target, dlt_config):
131-
ipaddress = get_container_ip(target)
132-
gateway = get_docker_network_gateway(target)
133114
target.execute(f"/usr/bin/dlt-daemon -d")
134115

135116
with DltWindow(
136117
protocol=Protocol.UDP,
137-
host_ip=gateway,
118+
host_ip=target.get_gateway(),
138119
multicast_ips=["224.0.0.1"],
139120
print_to_stdout=True,
140121
binary_path=dlt_config.dlt_receive_path,
@@ -146,13 +127,11 @@ def test_dlt_window_stdout(target, dlt_config):
146127

147128

148129
def test_dlt_window_with_filter(target, dlt_config):
149-
ipaddress = get_container_ip(target)
150-
gateway = get_docker_network_gateway(target)
151130
target.execute(f"/usr/bin/dlt-daemon -d")
152131

153132
with DltWindow(
154133
protocol=Protocol.UDP,
155-
host_ip=gateway,
134+
host_ip=target.get_gateway(),
156135
multicast_ips=["224.0.0.1"],
157136
print_to_stdout=True,
158137
dlt_filter="SINA SINC",
@@ -165,13 +144,11 @@ def test_dlt_window_with_filter(target, dlt_config):
165144

166145

167146
def test_dlt_window_with_record(target, dlt_config):
168-
ipaddress = get_container_ip(target)
169-
gateway = get_docker_network_gateway(target)
170147
target.execute(f"/usr/bin/dlt-daemon -d")
171148

172149
with DltWindow(
173150
protocol=Protocol.UDP,
174-
host_ip=gateway,
151+
host_ip=target.get_gateway(),
175152
multicast_ips=["224.0.0.1"],
176153
print_to_stdout=False,
177154
binary_path=dlt_config.dlt_receive_path,

0 commit comments

Comments
 (0)