can-utils/tests/test_canplayer.py

246 lines
8.5 KiB
Python

# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (c) 2023 Linux CAN project
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License for more details.
import pytest
import subprocess
import os
import time
import signal
import re
# --- Helper Functions ---
class TrafficMonitor:
"""
Context manager to run candump in the background.
Captures the bus traffic to verify what canplayer sends.
"""
def __init__(self, bin_path, interface, args=None):
self.cmd = [os.path.join(bin_path, "candump"), "-L", interface]
if args:
self.cmd.extend(args)
self.process = None
self.output = ""
def __enter__(self):
self.process = subprocess.Popen(
self.cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
preexec_fn=os.setsid
)
# Give candump a moment to bind to the socket
time.sleep(0.2)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.process:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
self.output, _ = self.process.communicate()
def create_logfile(path, content):
"""Creates a temporary CAN log file."""
with open(path, 'w') as f:
f.write(content)
return path
# --- Tests for canplayer ---
def test_help_option(bin_path):
"""Test -h option: Should print usage."""
# canplayer might exit with 0 or 1, check output primarily
result = subprocess.run([os.path.join(bin_path, "canplayer"), "-h"], capture_output=True, text=True)
assert "Usage: canplayer" in result.stdout or "Usage: canplayer" in result.stderr
def test_replay_file(bin_path, can_interface, tmp_path):
"""Test -I: Replay a simple log file."""
log_content = f"(1600000000.000000) {can_interface} 123#112233\n"
logfile = create_logfile(os.path.join(tmp_path, "test.log"), log_content)
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "canplayer"), "-I", logfile],
check=True
)
assert "123#112233" in monitor.output
def test_stdin_input(bin_path, can_interface):
"""Test input via stdin (default behavior without -I)."""
log_content = f"(1600000000.000000) {can_interface} 123#AABBCC\n"
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "canplayer")],
input=log_content,
text=True,
check=True
)
assert "123#AABBCC" in monitor.output
def test_interface_mapping(bin_path, can_interface, tmp_path):
"""Test interface assignment (e.g., vcan0=can99)."""
# Log file contains 'can99', but we map it to our real 'can_interface'
fake_iface = "can99"
log_content = f"(1600000000.000000) {fake_iface} 123#DEADBEEF\n"
logfile = create_logfile(os.path.join(tmp_path, "test.log"), log_content)
# Assignment syntax: dest=src (send frames received from src on dest)
# We want to send ON can_interface frames that came FROM fake_iface
mapping = f"{can_interface}={fake_iface}"
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "canplayer"), "-I", logfile, mapping],
check=True
)
# Monitor should see it on can_interface
assert "123#DEADBEEF" in monitor.output
def test_loop_l(bin_path, can_interface, tmp_path):
"""Test -l <num>: Loop playback."""
log_content = f"(1600000000.000000) {can_interface} 123#01\n"
logfile = create_logfile(os.path.join(tmp_path, "test.log"), log_content)
count = 3
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "canplayer"), "-I", logfile, "-l", str(count)],
check=True
)
# Should appear 3 times
occurrences = monitor.output.count("123#01")
assert occurrences == count
def test_ignore_timestamps_t(bin_path, can_interface, tmp_path):
"""Test -t: Ignore timestamps (send immediately)."""
# Create log with 2 seconds delay between frames
# If -t works, execution should be instant, not taking >2 seconds
log_content = (
f"(100.000000) {can_interface} 123#01\n"
f"(102.000000) {can_interface} 123#02\n"
)
logfile = create_logfile(os.path.join(tmp_path, "test.log"), log_content)
start = time.time()
subprocess.run(
[os.path.join(bin_path, "canplayer"), "-I", logfile, "-t"],
check=True
)
duration = time.time() - start
# Should be very fast, definitely under 1 second
assert duration < 1.0
def test_skip_gaps_s(bin_path, can_interface, tmp_path):
"""Test -s <s>: Skip gaps in timestamps > 's' seconds."""
# Gap of 2 seconds in log file
log_content = (
f"(100.000000) {can_interface} 123#01\n"
f"(102.000000) {can_interface} 123#02\n"
)
logfile = create_logfile(os.path.join(tmp_path, "test.log"), log_content)
# Tell canplayer to skip gaps > 1s (-s 1)
start = time.time()
subprocess.run(
[os.path.join(bin_path, "canplayer"), "-I", logfile, "-s", "1"],
check=True
)
duration = time.time() - start
# Should skip the 2s wait
assert duration < 1.5
def test_terminate_n(bin_path, can_interface, tmp_path):
"""Test -n <count>: Terminate after sending count frames."""
# Timestamps fixed to 6 decimal places (microseconds)
log_content = (
f"(100.000000) {can_interface} 123#01\n"
f"(100.001000) {can_interface} 123#02\n"
f"(100.002000) {can_interface} 123#03\n"
)
logfile = create_logfile(os.path.join(tmp_path, "test.log"), log_content)
# Process only 2 frames
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "canplayer"), "-I", logfile, "-n", "2"],
check=True
)
assert "123#01" in monitor.output
assert "123#02" in monitor.output
assert "123#03" not in monitor.output
def test_verbose_v(bin_path, can_interface, tmp_path):
"""Test -v: Verbose output."""
log_content = f"(1600000000.000000) {can_interface} 123#11\n"
logfile = create_logfile(os.path.join(tmp_path, "test.log"), log_content)
result = subprocess.run(
[os.path.join(bin_path, "canplayer"), "-I", logfile, "-v"],
capture_output=True,
text=True,
check=True
)
# Verbose mode usually prints the frame being sent to stdout
assert "123" in result.stdout and "11" in result.stdout
def test_disable_loopback_x(bin_path, can_interface, tmp_path):
"""Test -x: Disable local loopback."""
log_content = f"(1600000000.000000) {can_interface} 123#FF\n"
logfile = create_logfile(os.path.join(tmp_path, "test.log"), log_content)
# If loopback is disabled, a local candump should NOT see the frame
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "canplayer"), "-I", logfile, "-x"],
check=True
)
# Expect empty output (or at least the frame shouldn't be there)
assert "123#FF" not in monitor.output
def test_gap_g(bin_path, can_interface, tmp_path):
"""Test -g <ms>: Gap generation (functional check)."""
# -g adds a fixed gap. Difficult to measure precisely without affecting test stability,
# but we can check it runs successfully.
# Timestamps fixed to 6 decimal places
log_content = f"(100.000000) {can_interface} 123#11\n(100.000000) {can_interface} 123#22\n"
logfile = create_logfile(os.path.join(tmp_path, "test.log"), log_content)
subprocess.run(
[os.path.join(bin_path, "canplayer"), "-I", logfile, "-g", "10"],
check=True
)
def test_parsing_bad_lines(bin_path, can_interface, tmp_path):
"""Test that lines not starting with '(' are ignored."""
# Timestamps fixed to 6 decimal places
log_content = (
"This is a comment line\n"
f"(100.000000) {can_interface} 123#AA\n"
"Another invalid line\n"
)
logfile = create_logfile(os.path.join(tmp_path, "test.log"), log_content)
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "canplayer"), "-I", logfile],
check=True
)
# Should only process the valid frame
assert "123#AA" in monitor.output
# Ensure it didn't crash or error on invalid lines (return code check is implicit in check=True)