can-utils/tests/test_cangen.py

365 lines
14 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 ---
def parse_candump_line(line):
"""
Parses a line from 'candump -L' (logging format).
Format: (timestamp) interface ID#DATA or ID##FLAGS_DATA (FD)
Returns a dict with 'id', 'data', 'flags' (for FD) or None if parsing fails.
"""
# Regex for standard CAN: (123.456) vcan0 123#112233
match_std = re.search(r'\(\d+\.\d+\)\s+\S+\s+([0-9A-Fa-f]+)#([0-9A-Fa-f]*)', line)
if match_std:
return {'id': match_std.group(1), 'data': match_std.group(2), 'type': 'classic'}
# Regex for CAN FD: (123.456) vcan0 123##<flags><data>
match_fd = re.search(r'\(\d+\.\d+\)\s+\S+\s+([0-9A-Fa-f]+)##([0-9A-Fa-f]+)', line)
if match_fd:
# In log format, data usually follows flags. Not splitting strictly here,
# just capturing the payload block for verification.
return {'id': match_fd.group(1), 'payload_raw': match_fd.group(2), 'type': 'fd'}
return None
class TrafficMonitor:
"""Context manager to run candump in the background."""
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
)
time.sleep(0.2) # Wait for socket bind
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.process:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
out, _ = self.process.communicate()
self.output = out
# --- Tests for cangen ---
def test_help_option(bin_path):
"""Test -h option: Should print usage and exit with 1."""
result = subprocess.run([os.path.join(bin_path, "cangen"), "-h"], capture_output=True, text=True)
assert result.returncode == 1
assert "Usage: cangen" in result.stderr
def test_count_n(bin_path, can_interface):
"""Test -n <count>: Generate exact number of frames."""
count = 5
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", str(count)],
check=True
)
lines = [l for l in monitor.output.splitlines() if can_interface in l]
assert len(lines) == count, f"Expected {count} frames, got {len(lines)}"
def test_gap_g(bin_path, can_interface):
"""Test -g <ms>: Gap generation (timing check)."""
# Send 5 frames with 100ms gap -> should take approx 400-500ms
start = time.time()
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "5", "-g", "100"],
check=True
)
duration = time.time() - start
# Allow some tolerance (e.g., at least 0.4s for 4 gaps)
assert duration >= 0.4, f"Gap logic too fast: {duration}s"
def test_absolute_time_a(bin_path, can_interface):
"""Test -a: Use absolute time for gap (Functional check)."""
# Difficult to verify timing strictly without real-time analysis, checking for successful execution
result = subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "2", "-g", "10", "-a"],
check=True
)
assert result.returncode == 0
def test_txtime_t(bin_path, can_interface):
"""Test -t: Use SO_TXTIME (Functional check)."""
# Requires kernel support, checking for no crash
result = subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-t"],
capture_output=True
)
# Don't assert 0 explicitly if kernel might not support it, but vcan usually works.
if result.returncode != 0:
pytest.skip("Kernel might not support SO_TXTIME or config issue")
def test_start_time_option(bin_path, can_interface):
"""Test --start <ns>: Start time (Functional check)."""
# Using a timestamp in the past to ensure immediate execution or future for delay
# Just checking argument parsing valid
result = subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "--start", "0"],
check=True
)
assert result.returncode == 0
def test_extended_frames_e(bin_path, can_interface):
"""Test -e: Extended frame mode (EFF)."""
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-e"],
check=True
)
parsed = parse_candump_line(monitor.output.strip())
assert parsed, "No frame captured"
# Extended IDs are usually shown with 8 chars in candump or check length > 3
# Standard: 3 chars (e.g., 123), Extended: 8 chars (e.g., 12345678)
# Note: cangen generates random IDs.
assert len(parsed['id']) == 8, f"Expected 8-char hex ID for EFF, got {parsed['id']}"
def test_can_fd_f(bin_path, can_interface):
"""Test -f: CAN FD frames."""
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-f"],
check=True
)
# candump -L for FD uses '##' separator
assert "##" in monitor.output, "CAN FD separator '##' not found in candump output"
def test_can_fd_brs_b(bin_path, can_interface):
"""Test -b: CAN FD with Bitrate Switch (BRS)."""
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-b"],
check=True
)
assert "##" in monitor.output
def test_can_fd_esi_E(bin_path, can_interface):
"""Test -E: CAN FD with Error State Indicator (ESI)."""
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-E"],
check=True
)
assert "##" in monitor.output
def test_can_xl_X(bin_path, can_interface):
"""Test -X: CAN XL frames."""
# This might fail on older kernels/interfaces. We skip if return code is error.
result = subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-X"],
capture_output=True
)
if result.returncode != 0:
pytest.skip("CAN XL not supported by this environment/kernel")
def test_rtr_frames_R(bin_path, can_interface):
"""Test -R: Remote Transmission Request (RTR)."""
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-R"],
check=True
)
# candump -L often represents RTR with 'R' in the data part or 'remote' flag
# For candump -L: "vcan0 123#R"
assert "#R" in monitor.output, f"RTR flag not found in: {monitor.output}"
def test_dlc_greater_8_option_8(bin_path, can_interface):
"""Test -8: Allow DLC > 8 for Classic CAN."""
# This generates frames with DLC > 8 but len 8.
# It's a specific protocol violation test.
result = subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-8"],
check=True
)
assert result.returncode == 0
def test_mix_modes_m(bin_path, can_interface):
"""Test -m: Mix CC, FD, XL."""
result = subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "5", "-m"],
check=True
)
assert result.returncode == 0
def test_id_generation_mode_I(bin_path, can_interface):
"""Test -I <mode>: ID generation."""
# Fixed ID
target_id = "123"
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-I", target_id],
check=True
)
parsed = parse_candump_line(monitor.output)
assert parsed['id'] == target_id
# Increment ID ('i')
# Generate 3 frames, IDs should be consecutive (or incrementing)
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "3", "-I", "i"],
check=True
)
lines = monitor.output.strip().splitlines()
ids = [int(parse_candump_line(l)['id'], 16) for l in lines]
assert len(ids) == 3
assert ids[1] == ids[0] + 1
assert ids[2] == ids[1] + 1
def test_dlc_generation_mode_L(bin_path, can_interface):
"""Test -L <mode>: DLC/Length generation."""
# Fixed Length 4
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-L", "4"],
check=True
)
parsed = parse_candump_line(monitor.output)
# Data hex string length should be 2 * DLC (e.g. 4 bytes = 8 chars)
assert len(parsed['data']) == 8
def test_data_generation_mode_D(bin_path, can_interface):
"""Test -D <mode>: Data content generation."""
# Fixed Payload
payload = "11223344"
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-D", payload, "-L", "4"],
check=True
)
parsed = parse_candump_line(monitor.output)
assert parsed['data'] == payload
# Increment Payload ('i')
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "2", "-D", "i", "-L", "1"],
check=True
)
lines = monitor.output.strip().splitlines()
data_bytes = [int(parse_candump_line(l)['data'], 16) for l in lines]
# The first byte should increment (modulo 256)
assert (data_bytes[1] - data_bytes[0]) % 256 == 1
def test_disable_loopback_x(bin_path, can_interface):
"""Test -x: Disable loopback."""
# If loopback is disabled, candump on the SAME interface (same socket namespace)
# should NOT receive the frames if it's running on the same host usually.
# Note: On vcan, local loopback is handled by the driver.
# cangen -x sets CAN_RAW_LOOPBACK = 0.
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "3", "-x"],
check=True
)
# We expect output to be empty because candump is a local socket on the same node
# and we disabled loopback for the sender.
assert monitor.output.strip() == "", "candump received frames despite -x (disable loopback)"
def test_poll_p(bin_path, can_interface):
"""Test -p <timeout>: Poll on ENOBUFS."""
# Functional test, hard to provoke ENOBUFS on empty vcan
result = subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-p", "10"],
check=True
)
assert result.returncode == 0
def test_priority_P(bin_path, can_interface):
"""Test -P <priority>: Set socket priority."""
result = subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-P", "5"],
check=True
)
assert result.returncode == 0
def test_ignore_enobufs_i(bin_path, can_interface):
"""Test -i: Ignore ENOBUFS."""
result = subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-i"],
check=True
)
assert result.returncode == 0
def test_burst_c(bin_path, can_interface):
"""Test -c <count>: Burst count."""
count = 6
burst = 3
# Sending 6 frames in bursts of 3. Total should still be 6.
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", str(count), "-c", str(burst)],
check=True
)
lines = [l for l in monitor.output.splitlines() if can_interface in l]
assert len(lines) == count
def test_verbose_v(bin_path, can_interface):
"""Test -v: Verbose output to stdout."""
# -v prints ascii art/dots, -v -v prints details
result = subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "1", "-v", "-v"],
capture_output=True,
text=True
)
# Output should contain the Interface and ID
assert can_interface in result.stdout
# The output format is like: " vcan0 737 [3] EF 32 23"
# It does not contain labels like "ID:", "data:", "DLC:".
# Check for the presence of brackets which indicate DLC in this format.
assert "[" in result.stdout and "]" in result.stdout
def test_random_id_flags(bin_path, can_interface):
"""Test ID generation flags: r (random), e (even), o (odd)."""
# Test Even
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "5", "-I", "e"],
check=True
)
lines = monitor.output.strip().splitlines()
for l in lines:
pid = int(parse_candump_line(l)['id'], 16)
assert pid % 2 == 0, f"ID {pid} is not even"
# Test Odd
with TrafficMonitor(bin_path, can_interface) as monitor:
subprocess.run(
[os.path.join(bin_path, "cangen"), can_interface, "-n", "5", "-I", "o"],
check=True
)
lines = monitor.output.strip().splitlines()
for l in lines:
pid = int(parse_candump_line(l)['id'], 16)
assert pid % 2 != 0, f"ID {pid} is not odd"