blob: e57a85c5400683d8cb705f6162dbac540ab16748 [file]
#!/usr/bin/env python3
# Copyright (c) 2024 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""End-to-end driver for the Media File Management BDX flows.
Launches the Linux tv-app (Media Device / server) and the simplified
tv-casting-app (client), drives the User Directed Commissioning handshake, and
then waits for the three MediaFileManagement demo flows that the simplified app
auto-fires from ConnectionHandler after connecting:
- AddFile : client -> tv-app (tv-app is the BDX receiver / Requestor)
- OfferFile : client -> tv-app (tv-app is the BDX receiver / Requestor)
- GetSharedFile: tv-app -> client (client is the BDX receiver / Client)
It reuses ProcessOutputCapture from the existing CI harness so the interactive
apps keep a live stdin (otherwise they read EOF and exit immediately).
"""
import glob
import hashlib
import logging
import os
import sys
import time
# Reuse the CI harness' process wrapper (holds stdin open, tees output).
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from log_line_processing import ProcessOutputCapture # noqa: E402
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
log = logging.getLogger("mfm_bdx_e2e")
SDK_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
TV_APP = os.path.join(SDK_ROOT, "examples/tv-app/linux/out/debug/chip-tv-app")
CASTING_APP = os.path.join(
SDK_ROOT, "examples/tv-casting-app/linux/out/simplified/chip-tv-casting-app")
# tv-app operational ports (per examples/tv-app/linux/README.md) so it does not
# collide with the casting-app which also opens an operational listener. Give the
# tv-app its own KVS too, since both apps default to /tmp/chip_kvs.
TV_APP_KVS = "/tmp/chip_kvs_tvapp"
TV_APP_ARGS = ["--secured-device-port", "5640", "--secured-commissioner-port", "5552",
"--KVS", TV_APP_KVS]
# The simplified casting-app has no --KVS flag; its build overrides the path to
# /tmp/chip_casting_kvs (see the "KVS will be written to" log line at startup).
CLIENT_KVS_GLOB = "/tmp/chip_casting_kvs"
LOG_DIR = "/tmp/mfm-bdx-logs"
TV_APP_LOG = os.path.join(LOG_DIR, "tv-app.log")
CASTING_LOG = os.path.join(LOG_DIR, "casting-app.log")
# Byte-landing checks. The tv-app writes received files under this dir; the
# casting-app writes its GetSharedFile download to this path.
TV_APP_MEDIA_DIR = "/tmp/chip-media-files"
CLIENT_DOWNLOAD = "/tmp/shared-download.bin"
DEMO_CLIP = "/tmp/demo-clip.mp4"
def _sha256(path: str) -> str:
"""Return the hex SHA-256 digest of the file at `path`."""
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def wait_for(proc: ProcessOutputCapture, needle: str, timeout_sec: float, label: str) -> str:
"""Block until `needle` appears in proc's output, or raise on timeout."""
deadline = time.time() + timeout_sec
while time.time() < deadline:
line = proc.next_output_line(timeout_sec=deadline - time.time())
if line is None:
continue
if needle in line:
log.info("[%s] matched: %s", label, needle)
return line
raise TimeoutError(f"[{label}] timed out waiting for: {needle!r}")
def wait_for_any(proc: ProcessOutputCapture, needles: list[str], timeout_sec: float,
label: str) -> tuple[str, str]:
"""Block until any needle appears; returns the (needle, line) that matched."""
deadline = time.time() + timeout_sec
remaining = set(needles)
while time.time() < deadline:
line = proc.next_output_line(timeout_sec=deadline - time.time())
if line is None:
continue
for n in list(remaining):
if n in line:
log.info("[%s] matched: %s", label, n)
return n, line
raise TimeoutError(f"[{label}] timed out waiting for any of: {needles}")
def main() -> int:
"""Run all three MediaFileManagement BDX flows end-to-end.
Launches the tv-app and casting-app, drives the UDC handshake, waits for the
auto-fired AddFile/OfferFile/GetSharedFile flows to report completion, then
verifies the transferred bytes match the source payload by SHA-256 digest and
that the expected file count landed. Returns 0 on success, 1 on any failure
(missing binary, timeout, wrong file count, or digest mismatch).
"""
os.makedirs(LOG_DIR, exist_ok=True)
# Fresh demo clip + clean landing spots so size checks are meaningful.
with open(DEMO_CLIP, "wb") as f:
f.write(bytes((i % 251) for i in range(1024)))
if os.path.isdir(TV_APP_MEDIA_DIR):
for f in os.listdir(TV_APP_MEDIA_DIR):
os.remove(os.path.join(TV_APP_MEDIA_DIR, f))
if os.path.exists(CLIENT_DOWNLOAD):
os.remove(CLIENT_DOWNLOAD)
# Clear persisted fabrics so both apps commission fresh: otherwise the client
# silently reconnects to a cached fabric and the tv-app never prints the UDC
# "controller ux ok" prompt the handshake below waits for.
for kvs in glob.glob(CLIENT_KVS_GLOB + "*") + glob.glob(TV_APP_KVS + "*"):
os.remove(kvs)
for p in (TV_APP, CASTING_APP):
if not os.path.exists(p):
log.error("Missing binary: %s", p)
return 1
tv_cmd = ["stdbuf", "-o0", "-i0", TV_APP] + TV_APP_ARGS
cast_cmd = ["stdbuf", "-o0", "-i0", CASTING_APP]
with ProcessOutputCapture(tv_cmd, TV_APP_LOG) as tv_app:
wait_for(tv_app, "Started commissioner", 30, "tv-app")
with ProcessOutputCapture(cast_cmd, CASTING_LOG) as casting:
wait_for(casting, "Server initialization complete", 30, "casting")
# 1. Discovery -> request a cast to player #0.
wait_for(casting, "Discovered CastingPlayer #0", 30, "casting")
time.sleep(1)
casting.send_to_program("cast request 0\n")
# 2. UDC handshake on the tv-app side.
wait_for(tv_app, "Via Shell Enter: controller ux ok|cancel", 30, "tv-app")
tv_app.send_to_program("controller ux ok\n")
wait_for(tv_app, "Via Shell Enter: controller ux ok [passcode]", 30, "tv-app")
tv_app.send_to_program("controller ux ok 20202021\n")
# 3. Connection established -> ConnectionHandler auto-fires the flows.
wait_for(casting, "Successfully connected to CastingPlayer", 60, "casting")
log.info("Connected. Waiting for MediaFileManagement BDX flows...")
# The demo staggers the flows on ~5s timers (AddFile t=0, OfferFile
# t=5, RequestSharedFiles t=10, GetSharedFile t=15) so the single BDX
# sender/receiver on each side is never asked to run two at once.
# 4a. AddFile + OfferFile: tv-app is the BDX receiver. Two Requestor
# downloads should complete (one per flow).
wait_for(tv_app, "MediaFileManagementBdxRequestor: download complete", 60,
"tv-app AddFile")
wait_for(tv_app, "MediaFileManagementBdxRequestor: download complete", 60,
"tv-app OfferFile")
# 4b. GetSharedFile: client is the BDX receiver (preceded by
# RequestSharedFiles which allocates the ResponseID).
wait_for(casting, "MediaFileManagementBdxClient: download complete", 60,
"casting GetSharedFile")
log.info("All BDX flows reported complete. Letting IO settle...")
time.sleep(2)
# 5. Verify the transferred bytes match the source payload exactly. Every
# flow moves the same demo clip: AddFile and OfferFile push it client -> tv-app
# (landing as .bin blobs under TV_APP_MEDIA_DIR), and GetSharedFile pulls one
# of those stored blobs tv-app -> client (landing at CLIENT_DOWNLOAD). A
# non-empty check alone would pass a truncated or corrupted transfer, so
# compare SHA-256 digests against the known source.
expected_digest = _sha256(DEMO_CLIP)
log.info("expected payload %s : %d bytes, sha256 %s",
DEMO_CLIP, os.path.getsize(DEMO_CLIP), expected_digest)
ok = True
received = []
if os.path.isdir(TV_APP_MEDIA_DIR):
received = sorted(os.path.join(TV_APP_MEDIA_DIR, f)
for f in os.listdir(TV_APP_MEDIA_DIR)
if f.endswith(".bin"))
log.info("tv-app received files: %s", received)
# AddFile + OfferFile => exactly two received blobs, each matching the source.
if len(received) != 2:
log.error("Expected exactly 2 received files (AddFile+OfferFile), got %d",
len(received))
ok = False
for f in received:
digest = _sha256(f)
if digest == expected_digest:
log.info(" %s : %d bytes, digest OK", f, os.path.getsize(f))
else:
log.error(" %s : %d bytes, digest MISMATCH (got %s, expected %s)",
f, os.path.getsize(f), digest, expected_digest)
ok = False
# GetSharedFile => the client download must match the same source payload.
if not os.path.exists(CLIENT_DOWNLOAD):
log.error("client download %s missing", CLIENT_DOWNLOAD)
ok = False
else:
digest = _sha256(CLIENT_DOWNLOAD)
if digest == expected_digest:
log.info("client GetSharedFile download: %s : %d bytes, digest OK",
CLIENT_DOWNLOAD, os.path.getsize(CLIENT_DOWNLOAD))
else:
log.error("client download %s digest MISMATCH (got %s, expected %s)",
CLIENT_DOWNLOAD, digest, expected_digest)
ok = False
log.info("RESULT: %s", "PASS" if ok else "FAIL")
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())