diff --git a/score/test/component/datarouter/BUILD b/score/test/component/datarouter/BUILD index 50e3ffb9..e4d0badf 100644 --- a/score/test/component/datarouter/BUILD +++ b/score/test/component/datarouter/BUILD @@ -102,3 +102,100 @@ py_logging_itf_test( filesystem_pkg = "//score/test/component/filters_app:filtertest_filesystem_pkg", deps = ["@score_itf//score/itf/plugins/dlt"], ) + +# ============================================================================= +# dr_restart test +# ============================================================================= + +# No app-under-test needed: datarouter itself is already baked into every +# test image via //score/test/component:datarouter_pkg. Reused below by any +# other test that only needs datarouter itself, with no additional app. +pkg_filegroup( + name = "empty_filesystem_pkg", + srcs = [], +) + +pkg_tar( + name = "empty_filesystem", + srcs = [":empty_filesystem_pkg"], +) + +py_logging_itf_test( + name = "test_dr_restart", + srcs = ["test_dr_restart.py"], + filesystem = ":empty_filesystem", + filesystem_pkg = ":empty_filesystem_pkg", +) + +# ============================================================================= +# bandwidth quotas test +# ============================================================================= + +py_logging_itf_test( + name = "test_bandwidth_quotes_exists", + srcs = ["test_bandwidth_quotes_exists.py"], + filesystem = ":empty_filesystem", + filesystem_pkg = ":empty_filesystem_pkg", +) + +# ============================================================================= +# file_logging_test (kFile logging) +# ============================================================================= + +py_logging_itf_test( + name = "test_file_logging", + srcs = ["test_file_logging.py"], + filesystem = "//score/test/component/dlt_generator_app:dlt_generator_filesystem", + filesystem_pkg = "//score/test/component/dlt_generator_app:dlt_generator_filesystem_pkg", + deps = [ + "@score_itf//third_party/python_dlt", + ], +) + +# ============================================================================= +# quota_exceed test +# ============================================================================= + +py_logging_itf_test( + name = "test_quota_exceed", + timeout = "long", + srcs = ["test_quota_exceed.py"], + filesystem = "//score/test/component/dlt_generator_app:dlt_generator_filesystem", + filesystem_pkg = "//score/test/component/dlt_generator_app:dlt_generator_filesystem_pkg", + deps = ["@score_itf//score/itf/plugins/dlt"], +) + +# ============================================================================= +# safe_logging_ipc test +# ============================================================================= + +py_logging_itf_test( + name = "test_safe_logging_ipc", + srcs = ["test_safe_logging_ipc.py"], + filesystem = ":empty_filesystem", + filesystem_pkg = ":empty_filesystem_pkg", +) + +# ============================================================================= +# logging_after_delayed_dr_start test +# ============================================================================= + +py_logging_itf_test( + name = "test_logging_after_delayed_dr_start", + srcs = ["test_logging_after_delayed_dr_start.py"], + filesystem = "//score/test/component/dlt_generator_app:dlt_generator_filesystem", + filesystem_pkg = "//score/test/component/dlt_generator_app:dlt_generator_filesystem_pkg", + deps = ["@score_itf//score/itf/plugins/dlt"], +) + +# ============================================================================= +# logging_detached_logs test +# ============================================================================= + +py_logging_itf_test( + name = "test_logging_detached_logs", + srcs = ["test_logging_detached_logs.py"], + filesystem = "//score/test/component/dlt_generator_app:dlt_generator_filesystem", + filesystem_pkg = "//score/test/component/dlt_generator_app:dlt_generator_filesystem_pkg", + deps = ["@score_itf//score/itf/plugins/dlt"], +) diff --git a/score/test/component/datarouter/etc/log-channels.json b/score/test/component/datarouter/etc/log-channels.json index 5571f434..da392f26 100644 --- a/score/test/component/datarouter/etc/log-channels.json +++ b/score/test/component/datarouter/etc/log-channels.json @@ -41,6 +41,13 @@ ] } }, + "quotas": { + "quotaEnforcementEnabled": false, + "throughput": { + "overallMbps": 100, + "applicationsKbps": {} + } + }, "defaultChannel": "3493", "defaultThresold": "kVerbose", "messageThresholds": { diff --git a/score/test/component/datarouter/test_bandwidth_quotes_exists.py b/score/test/component/datarouter/test_bandwidth_quotes_exists.py new file mode 100644 index 00000000..1a4fd8ef --- /dev/null +++ b/score/test/component/datarouter/test_bandwidth_quotes_exists.py @@ -0,0 +1,49 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Integration test verifying datarouter's log-channels.json declares DLT +bandwidth quotas. Ported from SPP's test_bandwidth_quotes_exists.py. +""" + +import json +import logging +import os +import tempfile + +LOGGER = logging.getLogger(__name__) + +_LINUX_LOG_CHANNELS_PATH = "/opt/datarouter/etc/log-channels.json" +_QNX_LOG_CHANNELS_PATH = "/usr/bin/datarouter/etc/log-channels.json" + + +def _is_qnx(target) -> bool: + exit_code, out = target.execute("uname -s") + output = out.decode() if isinstance(out, bytes) else out + return "QNX" in output + + +def test_bandwidth_quotes_exists(target): + """Verify datarouter's log-channels.json declares a 'quotas' section.""" + remote_path = ( + _QNX_LOG_CHANNELS_PATH if _is_qnx(target) else _LINUX_LOG_CHANNELS_PATH + ) + + with tempfile.TemporaryDirectory() as tmpdir: + local_path = os.path.join(tmpdir, "log-channels.json") + target.download(remote_path, local_path) + + with open(local_path, "r") as f: + log_channel_content = json.load(f) + + LOGGER.info(f"log-channels.json keys: {list(log_channel_content.keys())}") + assert "quotas" in log_channel_content diff --git a/score/test/component/datarouter/test_dr_restart.py b/score/test/component/datarouter/test_dr_restart.py new file mode 100644 index 00000000..70b3a663 --- /dev/null +++ b/score/test/component/datarouter/test_dr_restart.py @@ -0,0 +1,62 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Migration status for SPP's datarouter auto-restart-on-crash test. + +SPP's version (test_logging_dr_restart.py::test_datarouter_restart) does: + execute_command(ssh, "killall -9 datarouter") + time.sleep(1) + assert execute_command(ssh, "pidof datarouter", timeout=60) == 0 + +That assertion only holds because SPP's target runs datarouter as a systemd +service with a Restart=always policy -- systemd, not the test, is what +relaunches the process after the kill. The test itself contains no restart +logic at all. + +SCORE's test infrastructure has no equivalent service manager or process +supervisor. Evidence checked directly in this repo before concluding that: + - quality/integration_testing/environments/qnx8_qemu/system.build has no + service-manager entry for datarouter, only a direct IFS binary mapping + ("[perms=777] /usr/bin/datarouter/datarouter = ${DATAROUTER}"). + - score/test/component/logging_plugin.py's datarouter_on_target fixture + launches datarouter with a single direct execute_async/exec call + (Docker) or a single "on ... ./datarouter" invocation (QNX) -- in both + cases a one-shot launch with no restart wrapper. + - No systemd unit, supervisor process, or Restart=/respawn configuration + exists anywhere under score/test/component/ or + quality/integration_testing/ (grepped for "systemd", "supervisor", + "Restart=", "respawn" -- zero matches). + +This is infrastructure the SUT's test deployment doesn't have, not a test +authoring gap -- per the ticket, migrating means porting the test against +what SCORE already provides, not building new supervisory infrastructure to +make an unsupported scenario pass. Skipped, not deleted, so the gap stays +visible in test output/reports. +""" + +import pytest + + +@pytest.mark.skip( + reason=( + "SPP's test relies on systemd's Restart=always policy to relaunch " + "datarouter after a kill -- SCORE's Docker/QNX test images run no " + "service manager or process supervisor at all (verified: no " + "systemd/supervisor/Restart=/respawn configuration anywhere in " + "quality/integration_testing/ or score/test/component/). Not " + "portable without adding new supervisory infrastructure, which is " + "out of scope for this migration." + ) +) +def test_dr_restart(): + pass diff --git a/score/test/component/datarouter/test_file_logging.py b/score/test/component/datarouter/test_file_logging.py new file mode 100644 index 00000000..61531fba --- /dev/null +++ b/score/test/component/datarouter/test_file_logging.py @@ -0,0 +1,60 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Integration test for DLT kFile logging via dlt_generator. + +Verifies that dlt_generator writes the expected number of DLT messages to a +local .dlt file when configured with kFile logging mode. Ported from SPP's +test_file_logging.py::test_dlt_kfile_logging. +""" + +import logging +import os +import tempfile + +import dlt.dlt as python_dlt + +LOGGER = logging.getLogger(__name__) + +APP_ID = "LGGG" +ITERATIONS = 12 +DEFAULT_MESSAGE = "default message text for example log generating application" +# use_full_output defaults to true in dlt_generator: Fatal + Error + Warn + Info + Verbose + Debug +MESSAGES_PER_ITERATION = 6 + + +def test_dlt_kfile_logging(target, datarouter_on_target): + """Verify DLT kFile logging produces the expected number of messages.""" + expected = ITERATIONS * MESSAGES_PER_ITERATION + + target.execute( + f"cd /opt/test_apps/dlt_generator && ./bin/dlt_generator -i {ITERATIONS} -s 0" + ) + + exit_code, _ = target.execute("test -f /tmp/LGGG.dlt && echo EXISTS") + assert exit_code == 0, "DLT file /tmp/LGGG.dlt was not created on the target" + + with tempfile.TemporaryDirectory() as tmpdir: + local_dlt = os.path.join(tmpdir, "LGGG.dlt") + target.download("/tmp/LGGG.dlt", local_dlt) + LOGGER.info(f"Downloaded DLT file: {os.path.getsize(local_dlt)} bytes") + + dlt_messages = python_dlt.load(local_dlt, None) + occurrences = sum( + 1 + for m in dlt_messages + if getattr(m, "apid", None) == APP_ID + and DEFAULT_MESSAGE in str(getattr(m, "payload_decoded", "")) + ) + LOGGER.info(f"Expected {expected} occurrences, got {occurrences}") + assert occurrences == expected diff --git a/score/test/component/datarouter/test_logging_after_delayed_dr_start.py b/score/test/component/datarouter/test_logging_after_delayed_dr_start.py new file mode 100644 index 00000000..5b1ce8a5 --- /dev/null +++ b/score/test/component/datarouter/test_logging_after_delayed_dr_start.py @@ -0,0 +1,80 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Integration test verifying DataRouter can pick up an already-running +logging app's queued messages after a delayed start. Ported from SPP's +test_logging_after_delayed_dr_start.py. + +SPP's assertion looks for a specific log line ("Accumulated frontend +exectution time:") that SPP's own dlt_generator prints at shutdown; SCORE's +dlt_generator (score/test/component/dlt_generator_app/dlt_generator.cpp) has +no such message. The actual intent (verify DataRouter can connect to and +drain a client's already-buffered messages once it finally starts) doesn't +depend on that specific string, so this checks for the app's regular +messages having been received instead. + +Uses the datarouter_manager fixture (manual start/stop) instead of +datarouter_on_target so the app can be started BEFORE DataRouter, matching +SPP's delayed-start scenario. This needs kRemote + dlt_capture (not kFile): +the client's own local file writer doesn't care whether DataRouter is +running, so kFile-mode counting would trivially pass regardless of the +delayed-start behavior being tested -- see test_quota_exceed.py's docstring +for the same client-vs-DataRouter-side distinction. As with any +dlt_capture()-based test in this suite, this is subject to the local +Docker/multicast limitation and is build-verified locally, relying on CI for +the actual pass/fail. +""" + +import logging +import time + +from logging_plugin import download_dlt + +LOGGER = logging.getLogger(__name__) + +APP_ID = "LGGG" +DEFAULT_MESSAGE = "default message text for example log generating application" + +_DR_START_DELAY_SEC = 2 +_GENERATION_ITERATIONS = 20 +_GENERATION_SLEEP_MS = 500 +_SHUTDOWN_WAIT_MS = 2000 + + +def test_logging_after_delayed_dr_start(target, datarouter_manager, dlt_capture): + """Verify DataRouter connects to and drains a client started before it.""" + with dlt_capture() as receiver: + proc = target.execute_async( + "/opt/test_apps/dlt_generator/bin/dlt_generator", + args=[ + "-i", + str(_GENERATION_ITERATIONS), + "-s", + str(_GENERATION_SLEEP_MS), + "-w", + str(_SHUTDOWN_WAIT_MS), + ], + cwd="/opt/test_apps/dlt_generator", + ) + LOGGER.info( + f"dlt_generator started; delaying DataRouter start by {_DR_START_DELAY_SEC}s" + ) + time.sleep(_DR_START_DELAY_SEC) + datarouter_manager.start() + proc.wait(timeout_s=30) + + record = download_dlt(target, receiver.dlt_file) + messages = record.find(query=dict(apid=APP_ID)) + count = sum(1 for m in messages if DEFAULT_MESSAGE in str(m.payload)) + LOGGER.info(f"Received {count} messages after delayed DataRouter start") + assert count > 0, "No messages received after delayed DataRouter start" diff --git a/score/test/component/datarouter/test_logging_detached_logs.py b/score/test/component/datarouter/test_logging_detached_logs.py new file mode 100644 index 00000000..b87c5d1e --- /dev/null +++ b/score/test/component/datarouter/test_logging_detached_logs.py @@ -0,0 +1,68 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Integration test verifying DataRouter can retrieve a client's log data +after the client exits immediately (detached mode). Ported from SPP's +test_logging_detached_logs.py. + +Product support for this is confirmed present: score/mw/log/detail/ +data_router/shared_memory/shared_memory_reader.{h,cpp} implements +ReadDetached()/DetachWriter()/IsWriterDetached(), and +score/datarouter/datarouter/data_router.cpp's +SourceSession::ProcessDetachedLogs() drives it from the daemon side. + +SPP's version uses logMode "kSystem" and asserts on a specific dlt_generator +shutdown log line ("Accumulated frontend exectution time:") that doesn't +exist in SCORE's dlt_generator (score/test/component/dlt_generator_app/ +dlt_generator.cpp). "kSystem" isn't a DataRouter-relay mode in SCORE (it +maps to the QNX slog2 backend, see score/mw/log/backend/slog_registrant.cpp) +-- this test needs the kRemote path specifically to exercise DataRouter's +detached-read code at all, so it uses the existing dlt_generator_filesystem +(kFile|kRemote) instead, and asserts on the app's regular messages having +been received rather than a message SCORE's app doesn't print. + +The app is launched with -w 0 (sleep_before_shutdown_ms=0) so it exits +immediately after logging, without waiting for DataRouter -- this is what +forces the shared-memory writer into detached state for DataRouter to +discover. Needs kRemote + dlt_capture (see test_quota_exceed.py's docstring +for why kFile-mode counting wouldn't exercise the DataRouter-side behavior +being tested here); subject to the same local Docker/multicast limitation +as other dlt_capture()-based tests in this suite -- build-verified locally, +relies on CI for the actual pass/fail. +""" + +import logging +import time + +from logging_plugin import download_dlt + +LOGGER = logging.getLogger(__name__) + +APP_ID = "LGGG" +DEFAULT_MESSAGE = "default message text for example log generating application" + +_POST_EXIT_WAIT_SEC = 5 + + +def test_logging_detached_logs(target, datarouter_on_target, dlt_capture): + """Verify DataRouter retrieves logs from a client that exits immediately.""" + with dlt_capture() as receiver: + target.execute("cd /opt/test_apps/dlt_generator && ./bin/dlt_generator -w 0") + # Give DataRouter time to detect the detached writer and drain it. + time.sleep(_POST_EXIT_WAIT_SEC) + + record = download_dlt(target, receiver.dlt_file) + messages = record.find(query=dict(apid=APP_ID)) + count = sum(1 for m in messages if DEFAULT_MESSAGE in str(m.payload)) + LOGGER.info(f"Received {count} messages from the detached client") + assert count > 0, "Couldn't find logs from dlt_generator after detached exit" diff --git a/score/test/component/datarouter/test_quota_exceed.py b/score/test/component/datarouter/test_quota_exceed.py new file mode 100644 index 00000000..7c05facf --- /dev/null +++ b/score/test/component/datarouter/test_quota_exceed.py @@ -0,0 +1,191 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Integration test for datarouter's per-application bandwidth quota +enforcement. Ported from SPP's test_quota_exceed.py. + +Quota enforcement is re-evaluated every 10s (score/datarouter/src/daemon/ +socketserver.cpp: kStatisticsLogPeriodUs = 10_000_000), matching SPP's own +10s ShowStats() cycle. Messages exceeding the app's applicationsKbps limit +are dropped inside the DataRouter daemon's SourceSession read loop +(data_router.cpp: ProcessAndRouteLogMessages -> on_new_record returns early +when quota_overlimit_detected). + +Important: this drop happens in the DataRouter DAEMON's own read of the +client's shared-memory ring buffer -- it is specific to the kRemote path. +kFile-mode messages are written directly by the client process via mw::log's +own FileRecorder backend, entirely bypassing DataRouter's SourceSession, so +they are NOT subject to quota enforcement at all (confirmed empirically: +kFile-based counting showed identical message counts across every quota +scenario, enabled or not). This test therefore uses kRemote + dlt_capture to +observe the actually-throttled message stream, unlike the kFile-based +counting used elsewhere in this test suite -- which also means, unlike most +of this suite, it depends on multicast DLT reception and is subject to the +same local Docker/multicast limitation as test_filetransfer/ +test_datarouter_filters: build-verified locally, relies on CI for the actual +pass/fail. + +Each scenario writes its own log-channels.json to the target and restarts +datarouter via the datarouter_manager fixture to pick it up. On Linux this +replaces datarouter's default config file directly (writable). On QNX the +default config path is baked into the read-only IFS boot image, so the +scenario config is written to /tmp instead (RAM-backed, writable) and +datarouter is started with its existing --config flag (see +score/datarouter/src/applications/options.cpp) pointing at that path -- +this uses only datarouter's already-documented config-path option, not any +new capability. +""" + +import copy +import json +import logging +import os +import tempfile + +from logging_plugin import download_dlt + +LOGGER = logging.getLogger(__name__) + +APP_ID = "LGGG" +DEFAULT_MESSAGE = "default message text for example log generating application" + +_LINUX_LOG_CHANNELS_PATH = "/opt/datarouter/etc/log-channels.json" +_QNX_LOG_CHANNELS_PATH = "/tmp/log-channels.json" + +# Sustained generation across ~30s (3 quota stats cycles @ 10s each): the +# first cycle establishes the observed rate, enforcement (if enabled) then +# applies from the second cycle onward. +_GENERATION_ITERATIONS = 3000 +_GENERATION_SLEEP_MS = 10 + +# CheckAndSetQuotaEnforcement (data_router.cpp) computes +# rate_k_bps = totalsize_bytes * 1000 / 1024 / elapsed_ms, i.e. KB/s (not +# Kbit/s despite the "Kbps" field name in log-channels.json). Empirically +# measured baseline for the generation params above (via kFile byte count, +# same message volume applies to kRemote): ~65 KB/s. SPP's own 500/100 +# values are calibrated to SPP's hardware throughput and don't translate +# directly; these limits are chosen to sit below the measured baseline here +# so enforcement is actually exercised. + +# Mirrors score/test/component/datarouter/etc/log-channels.json, varying +# only the "quotas" section per scenario. +_BASE_LOG_CHANNELS = { + "channels": { + "3491": { + "address": "0.0.0.0", + "channelThreshold": "kError", + "dstAddress": "239.255.42.99", + "dstPort": 3490, + "ecu": "TST1", + "port": 3491, + }, + "3492": { + "address": "0.0.0.0", + "channelThreshold": "kInfo", + "dstAddress": "239.255.42.99", + "dstPort": 3490, + "ecu": "TST2", + "port": 3492, + }, + "3493": { + "address": "0.0.0.0", + "channelThreshold": "kVerbose", + "dstAddress": "239.255.42.99", + "dstPort": 3490, + "ecu": "TST3", + "port": 3493, + }, + }, + "channelAssignments": { + "DR": {"": ["3492"], "CTX1": ["3492", "3493"]}, + "-NI-": {"": ["3491"]}, + }, + "defaultChannel": "3493", + "defaultThresold": "kVerbose", + "messageThresholds": { + "": {"vcip": "kInfo"}, + "DR": {"": "kVerbose", "CTX1": "kVerbose", "STAT": "kDebug"}, + "-NI-": {"": "kVerbose"}, + }, +} + +SCENARIOS = [ + {"name": "quota_disabled_50", "enabled": False, "limit_kbps": 50}, + {"name": "quota_enabled_50", "enabled": True, "limit_kbps": 50}, + {"name": "quota_enabled_15", "enabled": True, "limit_kbps": 15}, +] + + +def _is_qnx(target) -> bool: + exit_code, out = target.execute("uname -s") + output = out.decode() if isinstance(out, bytes) else out + return "QNX" in output + + +def _build_log_channels(enabled, limit_kbps): + config = copy.deepcopy(_BASE_LOG_CHANNELS) + config["quotas"] = { + "quotaEnforcementEnabled": enabled, + "throughput": { + "overallMbps": 100, + "applicationsKbps": {APP_ID: limit_kbps}, + }, + } + return config + + +def _apply_scenario_config(target, enabled, limit_kbps): + remote_path = ( + _QNX_LOG_CHANNELS_PATH if _is_qnx(target) else _LINUX_LOG_CHANNELS_PATH + ) + config = _build_log_channels(enabled, limit_kbps) + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(config, f) + local_path = f.name + try: + target.upload(local_path, remote_path) + finally: + os.unlink(local_path) + + +def test_quota_exceed(target, datarouter_manager, dlt_capture): + """Verify quota enforcement reduces observed throughput as configured.""" + results = {} + qnx_config_path = _QNX_LOG_CHANNELS_PATH if _is_qnx(target) else None + for scenario in SCENARIOS: + LOGGER.info(f"Scenario: {scenario['name']}") + datarouter_manager.stop() + _apply_scenario_config(target, scenario["enabled"], scenario["limit_kbps"]) + datarouter_manager.start(config_path=qnx_config_path) + + with dlt_capture() as receiver: + target.execute( + f"cd /opt/test_apps/dlt_generator && " + f"./bin/dlt_generator -i {_GENERATION_ITERATIONS} -s {_GENERATION_SLEEP_MS}" + ) + + record = download_dlt(target, receiver.dlt_file) + messages = record.find(query=dict(apid=APP_ID)) + count = sum(1 for m in messages if DEFAULT_MESSAGE in str(m.payload)) + LOGGER.info(f"{scenario['name']}: {count} messages received") + results[scenario["name"]] = count + + LOGGER.info(f"Results: {results}") + assert results["quota_enabled_50"] <= results["quota_disabled_50"], ( + f"50 enabled ({results['quota_enabled_50']}) should be <= disabled " + f"({results['quota_disabled_50']})" + ) + assert results["quota_enabled_15"] < results["quota_enabled_50"], ( + f"15 ({results['quota_enabled_15']}) should be < 50 " + f"({results['quota_enabled_50']})" + ) diff --git a/score/test/component/datarouter/test_safe_logging_ipc.py b/score/test/component/datarouter/test_safe_logging_ipc.py new file mode 100644 index 00000000..45d5b320 --- /dev/null +++ b/score/test/component/datarouter/test_safe_logging_ipc.py @@ -0,0 +1,45 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Migration status for SPP's safe-logging-IPC-disconnect test. + +SPP's own test_safe_logging_ipc.py has exactly one test function, +test_datarouter_when_app_disconnect_immediate, and it is itself decorated +with: + + @pytest.mark.skip(reason="Temporary disabled, should be enabled when + SWP-132779 would be implemented") + +i.e. SPP's own team does not run this test either -- it's pending an +internal SPP ticket (SWP-132779) that has not been implemented as of the +pinned commit this migration is based on +(fc1229f5c0c6ddc4eafa29dbaab7b0e82a150055). There is nothing to migrate a +*working* version of; migrating it faithfully means preserving the same +skip, not inventing a fix SPP itself hasn't shipped. +""" + +import pytest + + +@pytest.mark.skip( + reason=( + "SPP's own version of this test is itself skipped " + '("Temporary disabled, should be enabled when SWP-132779 would be ' + 'implemented") as of the pinned commit ' + "fc1229f5c0c6ddc4eafa29dbaab7b0e82a150055 -- there is no working " + "version to migrate. SWP-132779 is an internal SPP ticket outside " + "this repo's visibility/control." + ) +) +def test_safe_logging_ipc(): + pass diff --git a/score/test/component/logging_plugin.py b/score/test/component/logging_plugin.py index 8d94dc44..6215c718 100644 --- a/score/test/component/logging_plugin.py +++ b/score/test/component/logging_plugin.py @@ -131,6 +131,55 @@ def datarouter_on_target(target): proc.stop() +class _DatarouterManager: + """Manual start/stop control over the DataRouter process on the target.""" + + def __init__(self, target): + self._target = target + self._proc = None + self._started = False + + def start(self, config_path=None): + if self._started: + return + if _is_qnx(self._target): + cmd = _QNX_DR_CMD + if config_path: + cmd = cmd.replace( + "--no_adaptive_runtime ", + f"--no_adaptive_runtime --config {config_path} ", + ) + self._target.execute(cmd) + else: + self._proc = self._target.execute_async( + "/opt/datarouter/bin/datarouter", + args=["--no_adaptive_runtime"], + cwd="/opt/datarouter", + ) + _wait_for_datarouter(self._target) + self._started = True + + def stop(self): + if not self._started: + return + if _is_qnx(self._target): + self._target.execute(_QNX_DR_STOP_CMD) + else: + if self._proc is not None and self._proc.is_running(): + self._proc.stop() + self._started = False + + +@pytest.fixture(scope="function") +def datarouter_manager(target): + """Yield a _DatarouterManager for tests that need manual DR start/stop control.""" + manager = _DatarouterManager(target) + try: + yield manager + finally: + manager.stop() + + @pytest.fixture(scope="function") def dlt_capture(target, dlt_on_target, request): """Start a DLT receiver. On QNX, runs dlt-receive on the host; on Docker, on the target."""