Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0d8fb39
Add communication (LoLa/mw::com) feature integration tests to ITF
Subramanian-K812 Jul 10, 2026
0b10b78
fix(itf): add missing test_properties.py
Subramanian-K812 Jul 15, 2026
a4be122
fix(itf): producer leak comm tests and skip ACL isolation on QNX
Subramanian-K812 Jul 16, 2026
d7aea6b
Add fit_sender/fit_receiver two-process communication ITF scenario
Subramanian-K812 Jul 17, 2026
f20ad24
Add negative-config and ASIL-B ACL isolation tests for communication ITF
Subramanian-K812 Jul 17, 2026
6f7b501
Fix negative-config test bleed by using a never-offered instance spec…
Subramanian-K812 Jul 17, 2026
034cb3b
Declare process ASIL-B capability in ACL isolation config to fix inst…
Subramanian-K812 Jul 17, 2026
9ae4991
Fix rust copyright header style and add missing known_good.json patch…
Subramanian-K812 Jul 17, 2026
028f1db
Update docstrings to remove outdated design commentary
Subramanian-K812 Jul 20, 2026
a6ae3a2
Tighten communication ITF assertions and fix BUILD/plugin wiring
Subramanian-K812 Jul 21, 2026
bcee72b
fix: update comm_fit to use score_com crate (renamed from com_api)
Subramanian-K812 Oct 28, 2026
8492918
fix: format rust imports
Subramanian-K812 Oct 28, 2026
2026dc5
fix: add comm_fit to showcases_bundle for QNX image deployment
Subramanian-K812 Oct 28, 2026
420921d
fix: address review comments on communication ITF tests
Subramanian-K812 Oct 28, 2026
8fea593
fix: remove stale comment about attribute_plugin from BUILD
Subramanian-K812 Oct 28, 2026
a02a58e
fix: add C++ FIT binaries and cross-language exchange tests
Subramanian-K812 Oct 29, 2026
5c7c84b
fix: address review findings on C++ FIT binaries and patch
Subramanian-K812 Oct 29, 2026
6437cbf
fix: harden C++ FIT binaries and extend negative-config test coverage
Subramanian-K812 Oct 29, 2026
7855c8d
fix: trim comments in C++ FIT binaries and comm helpers
Subramanian-K812 Oct 29, 2026
701c8e2
fix: rewrite C++ FIT binaries using native AsSkeleton/AsProxy templat…
Subramanian-K812 Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bazel_common/score_modules_target_sw.MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ git_override(
patch_strip = 1,
patches = [
"//patches/communication:001-expose-comm-examples.patch",
"//patches/communication:002-expose-com-api-gen.patch",
"//patches/communication:003-module-deps-visibility.patch",
"//patches/communication:004-rules-build-error-not-dev-dependency.patch",
"//patches/communication:005-migrate-baselibs-rust-targets.patch",
Expand Down
15 changes: 14 additions & 1 deletion feature_integration_tests/itf/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,23 @@ config_setting(
],
)

# Grouped per suite so future additions don't mix into one undifferentiated
# list; all_tests below is the union of these groups plus shared utilities.
filegroup(
name = "communication_tests",
srcs = [
"comm_helpers.py",
"test_communication.py",
],
)

filegroup(
name = "all_tests",
srcs = [
"test_remote_logging.py",
"test_showcases.py",
"test_ssh.py",
":communication_tests",
],
)

Expand Down Expand Up @@ -82,7 +93,9 @@ py_itf_test(
],
},
no_match_error = "Unsupported platform",
),
) + [
"@score_itf//score/itf/plugins:attribute_plugin",
],
tags = [
"manual",
],
Expand Down
301 changes: 301 additions & 0 deletions feature_integration_tests/itf/comm_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
# *******************************************************************************
# 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
# *******************************************************************************
"""Helpers for the communication (LoLa / mw::com) ITF tests."""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass

logger = logging.getLogger(__name__)

FIT_SENDER_BIN = "/showcases/bin/fit_sender"
FIT_RECEIVER_BIN = "/showcases/bin/fit_receiver"
FIT_SENDER_CC_BIN = "/showcases/bin/fit_sender_cc"
FIT_RECEIVER_CC_BIN = "/showcases/bin/fit_receiver_cc"
COMM_CWD = "/showcases/data/comm"
DEFAULT_MANIFEST = "/showcases/data/comm/etc/mw_com_config.json"

RECV_SAMPLE_RE = re.compile(r"FIT_RECV seq=(\S+)")
FOUND_SERVICE_MARKER = "FIT_FOUND_SERVICE"
RECV_DONE_MARKER = "FIT_RECV_DONE"


@dataclass
class CommResult:
"""Captured output of one producer/consumer exchange."""

recv_exit_code: int
recv_stdout: str
send_stdout: str

# --- oracles ---------------------------------------------------------
@property
def received_samples(self) -> list[int]:
"""Ordered sequence numbers received; non-integer tokens fail (corruption guard)."""
tokens = RECV_SAMPLE_RE.findall(self.recv_stdout)
for token in tokens:
assert token.isdigit(), f"non-integer sequence token (corruption): {token!r} in {tokens}"
return [int(token) for token in tokens]

@property
def found_service(self) -> bool:
"""True if service discovery located the offered instance."""
return FOUND_SERVICE_MARKER in self.recv_stdout

@property
def completed(self) -> bool:
"""True if the receiver consumed all requested samples and exited cleanly."""
return RECV_DONE_MARKER in self.recv_stdout


def _shell_exit_marker(out: str, marker: str = "RECV_EXIT") -> int:
"""Extract ``<marker>=<int>`` printed by the exchange command."""
m = re.search(rf"{marker}=(-?\d+)", out)
if m is None:
raise AssertionError(f"Could not find {marker}= in command output:\n{out}")
return int(m.group(1))


def run_event_exchange(
target,
send_cycles: int = 50,
recv_cycles: int = 10,
interval_ms: int = 100,
startup_delay_s: int = 1,
manifest: str | None = None,
instance_specifier: str | None = None,
sender_bin: str | None = None,
receiver_bin: str | None = None,
) -> CommResult:
"""Run one producer/consumer exchange on ``target`` and return parsed output."""
manifest_path = manifest or DEFAULT_MANIFEST
instance_arg = f" -i {instance_specifier}" if instance_specifier else ""
sender = sender_bin or FIT_SENDER_BIN
receiver = receiver_bin or FIT_RECEIVER_BIN
send_log = "/tmp/fit_send.log"
recv_log = "/tmp/fit_recv.log"

command = (
f"cd {COMM_CWD} && rm -f {send_log} {recv_log} && "
f"{{ {sender} -n {send_cycles} -t {interval_ms} -s {manifest_path}{instance_arg} "
f"> {send_log} 2>&1 & SEND_PID=$!; }} && "
f"sleep {startup_delay_s} && "
f"{receiver} -n {recv_cycles} -t {interval_ms} -s {manifest_path}{instance_arg} "
f"> {recv_log} 2>&1 ; RECV_RC=$? ; "
f"kill $SEND_PID 2>/dev/null ; wait $SEND_PID 2>/dev/null ; "
f"echo RECV_EXIT=$RECV_RC"
)
logger.info("running command:\n%s", command)
_, out = target.execute(command)
Comment thread
Subramanian-K812 marked this conversation as resolved.
recv_exit_code = _shell_exit_marker(out.decode(errors="replace"))

_, recv_bytes = target.execute(f"cat {recv_log}")
_, send_bytes = target.execute(f"cat {send_log}")

return CommResult(
recv_exit_code=recv_exit_code,
recv_stdout=recv_bytes.decode(errors="replace"),
send_stdout=send_bytes.decode(errors="replace"),
)


TRUNCATED_CONFIG_JSON = '{ "serviceTypes": [ { "serviceTypeName"'
SCHEMA_INVALID_CONFIG_JSON = "{}"
# Distinct instance specifier so a misconfigured receiver cannot latch onto leftover SHM state.
NEGATIVE_CONFIG_INSTANCE_SPECIFIER = "/Vehicle/ServiceNegativeConfigTest/Instance"


def write_remote_file(target, path: str, content: str) -> None:
"""Write ``content`` to ``path`` on the target via a quoted heredoc."""
heredoc = f"cat > {path} <<'COMM_FIT_EOF'\n{content}\nCOMM_FIT_EOF\n"
logger.info("writing remote file: %s", path)
exit_code, out = target.execute(heredoc)
if exit_code != 0:
raise AssertionError(f"Failed to write {path} on target: {out!r}")


def run_receiver_with_manifest(
target,
manifest_path: str,
cycles: int = 3,
interval_ms: int = 100,
max_polls: int = 10,
timeout_s: int = 10,
instance_specifier: str = NEGATIVE_CONFIG_INSTANCE_SPECIFIER,
receiver_bin: str | None = None,
) -> CommResult:
"""Run only the receiver against ``manifest_path``; no producer started (negative-config scenarios)."""
receiver = receiver_bin or FIT_RECEIVER_BIN
recv_log = "/tmp/fit_recv_negative.log"
command = (
f"cd {COMM_CWD} && rm -f {recv_log} && "
f"timeout {timeout_s} {receiver} -n {cycles} -t {interval_ms} "
f"--max-polls {max_polls} -s {manifest_path} -i {instance_specifier} "
f"> {recv_log} 2>&1 ; echo RECV_EXIT=$?"
)
logger.info("running command:\n%s", command)
_, out = target.execute(command)
recv_exit_code = _shell_exit_marker(out.decode(errors="replace"))
_, recv_bytes = target.execute(f"cat {recv_log}")
return CommResult(
recv_exit_code=recv_exit_code,
recv_stdout=recv_bytes.decode(errors="replace"),
send_stdout="",
)


ASILB_INSTANCE_SPECIFIER = "/Vehicle/ServiceAcl/Instance"

# ACL denial surfaces as a panic in the generated bindings (ProxyCreationFailed),
# not as an Err from build() — so we match LoLa's own log strings, not just exit code.
ACL_DENIAL_MARKERS = (
"FIT_RECV_PROXY_BUILD_ERROR",
"Could not create Proxy",
"ProxyCreationFailed",
)
DENIED_CONSUMER_UID = 64000

ASILB_CONFIG_TEMPLATE = """{{
"serviceTypes": [
{{
"serviceTypeName": "/bmw/adp/VehicleInterface",
"version": {{"major": 1, "minor": 0}},
"bindings": [
{{
"binding": "SHM",
"serviceId": 6433,
"events": [
{{"eventName": "left_tire", "eventId": 1}},
{{"eventName": "exhaust", "eventId": 2}}
]
}}
]
}}
],
"global": {{"applicationID": 4099, "asil-level": "B"}},
"serviceInstances": [
{{
"instanceSpecifier": "{instance_specifier}",
"serviceTypeName": "/bmw/adp/VehicleInterface",
"version": {{"major": 1, "minor": 0}},
"instances": [
{{
"instanceId": 9,
"allowedConsumer": {{"B": [{allowed_uid}], "QM": [{allowed_uid}]}},
"allowedProvider": {{"B": [{allowed_uid}], "QM": [{allowed_uid}]}},
"asil-level": "B",
"binding": "SHM",
"events": [
{{"eventName": "left_tire", "numberOfSampleSlots": 30, "maxSubscribers": 5}},
{{"eventName": "exhaust", "numberOfSampleSlots": 30, "maxSubscribers": 5}}
]
}}
]
}}
]
}}"""


def render_asilb_config(allowed_uid: int) -> str:
"""ASIL-B manifest granting only ``allowed_uid`` (not DENIED_CONSUMER_UID)."""
return ASILB_CONFIG_TEMPLATE.format(
instance_specifier=ASILB_INSTANCE_SPECIFIER,
allowed_uid=allowed_uid,
)


def discover_default_uid(target) -> int:
"""The real UID `target.execute` commands run as by default."""
_, out = target.execute("id -u")
return int(out.decode(errors="replace").strip())


def is_qnx(target) -> bool:
"""Same detection approach as test_remote_logging.py's `_is_qnx`."""
_, out = target.execute("uname -s")
return b"QNX" in out


def as_uid_prefix(target, uid: int) -> str:
"""
Shell prefix that re-executes the following command under ``uid``.

QNX: `on -u <uid>:<gid>` needs no /etc/passwd entry.
Linux: `setpriv` needs CAP_SETUID (i.e. running as root), the same
privilege level the Linux ITF container already runs at.
"""
if is_qnx(target):
return f"on -u {uid}:{uid} "
return f"setpriv --reuid={uid} --regid={uid} --clear-groups "


def run_acl_isolation_scenario(
target,
send_cycles: int = 40,
recv_cycles: int = 8,
interval_ms: int = 150,
startup_delay_s: int = 1,
):
"""
Run an ASIL-B producer, one consumer with an allowed UID, and one
consumer with a UID deliberately excluded from the ACL, all running
concurrently against the same instance.

Returns (allowed_result, denied_result, allowed_uid).
"""
allowed_uid = discover_default_uid(target)
manifest_path = "/tmp/comm_fit_asilb_config.json"
write_remote_file(target, manifest_path, render_asilb_config(allowed_uid))

send_log = "/tmp/fit_acl_send.log"
allowed_log = "/tmp/fit_acl_recv_allowed.log"
denied_log = "/tmp/fit_acl_recv_denied.log"
denied_prefix = as_uid_prefix(target, DENIED_CONSUMER_UID)

command = (
f"cd {COMM_CWD} && rm -f {send_log} {allowed_log} {denied_log} && "
f"chmod 666 {manifest_path} && "
f"{{ {FIT_SENDER_BIN} -n {send_cycles} -t {interval_ms} -s {manifest_path} "
f"-i {ASILB_INSTANCE_SPECIFIER} > {send_log} 2>&1 & SEND_PID=$!; }} && "
f"sleep {startup_delay_s} && "
f"( {FIT_RECEIVER_BIN} -n {recv_cycles} -t {interval_ms} -s {manifest_path} "
f"-i {ASILB_INSTANCE_SPECIFIER} > {allowed_log} 2>&1 & ALLOWED_PID=$! ; "
f"{denied_prefix}timeout 8 {FIT_RECEIVER_BIN} -n {recv_cycles} -t {interval_ms} "
f"--max-polls 20 -s {manifest_path} -i {ASILB_INSTANCE_SPECIFIER} "
f"> {denied_log} 2>&1 ; DENIED_EXIT=$? ; "
f"wait $ALLOWED_PID ; ALLOWED_EXIT=$? ; "
f"echo ALLOWED_EXIT=$ALLOWED_EXIT ; echo DENIED_EXIT=$DENIED_EXIT ) ; "
f"kill $SEND_PID 2>/dev/null ; wait $SEND_PID 2>/dev/null"
)
logger.info("running command:\n%s", command)
_, out = target.execute(command)
decoded = out.decode(errors="replace")
allowed_exit = _shell_exit_marker(decoded, "ALLOWED_EXIT")
denied_exit = _shell_exit_marker(decoded, "DENIED_EXIT")

_, allowed_bytes = target.execute(f"cat {allowed_log}")
_, denied_bytes = target.execute(f"cat {denied_log}")

allowed_result = CommResult(
recv_exit_code=allowed_exit,
recv_stdout=allowed_bytes.decode(errors="replace"),
send_stdout="",
)
denied_result = CommResult(
recv_exit_code=denied_exit,
recv_stdout=denied_bytes.decode(errors="replace"),
send_stdout="",
)
return allowed_result, denied_result, allowed_uid
Loading
Loading