can-utils/tests/test_j1939acd.py

257 lines
10 KiB
Python

# SPDX-License-Identifier: GPL-2.0-only
import subprocess
import time
import os
import pytest
import signal
import tempfile
# Note: bin_path and interface fixtures are provided implicitly by conftest.py
def wait_for_can_msg(file_obj, pattern, timeout=5.0):
"""
Helper to poll the log file for a specific pattern.
Returns True if found within timeout, False otherwise.
"""
start = time.time()
while time.time() - start < timeout:
file_obj.seek(0)
content = file_obj.read()
if pattern in content:
return True
time.sleep(0.1)
return False
def test_j1939acd_usage(bin_path):
"""
Test usage/help output for j1939acd.
"""
j1939acd = os.path.join(bin_path, "j1939acd")
if not os.path.exists(j1939acd):
pytest.skip("j1939acd binary not found")
result = subprocess.run(
[j1939acd, "-h"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
assert "Usage: j1939acd" in result.stderr or "Usage: j1939acd" in result.stdout
def test_j1939acd_claiming_lifecycle(bin_path, can_interface):
"""
Test J1939 Address Claiming procedure (Startup & Shutdown).
Corresponds to Test Scenario 1 (Happy Flow) and Cleanup.
"""
j1939acd = os.path.join(bin_path, "j1939acd")
candump = os.path.join(bin_path, "candump")
if not os.path.exists(j1939acd):
pytest.skip("j1939acd binary not found")
nodename_hex = "1122334455667788"
# candump -L format uses compact hex strings (no spaces)
nodename_payload = "8877665544332211"
with tempfile.NamedTemporaryFile(suffix=".jacd", delete=False) as tmp_cache:
cache_file = tmp_cache.name
# Start candump
dump_out = tempfile.TemporaryFile(mode='w+')
dump_proc = subprocess.Popen([candump, "-L", can_interface], stdout=dump_out, stderr=subprocess.PIPE, text=True)
time.sleep(0.5) # Allow candump to initialize
daemon_proc = None
try:
# Start j1939acd with range starting at 80 (0x50).
# We rely on -r because -a is reportedly not working properly.
# Range: 80 to 128 (Decimal) -> 0x50 to 0x80.
cmd_daemon = [j1939acd, "-r", "80-128", "-c", cache_file, nodename_hex, can_interface]
print(f"DEBUG: Starting j1939acd: {' '.join(cmd_daemon)}")
daemon_proc = subprocess.Popen(cmd_daemon, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# Wait for initial claim of 0x50 (which is 80 decimal)
if not wait_for_can_msg(dump_out, f"18EEFF50#{nodename_payload}", timeout=3.0):
dump_out.seek(0)
print(f"DEBUG: Timeout waiting for start. Log:\n{dump_out.read()}")
pytest.fail("j1939acd did not send Initial Address Claim (0x50) within timeout")
# Stop
daemon_proc.send_signal(signal.SIGINT)
daemon_proc.wait(timeout=2.0)
# Analyze shutdown
if not wait_for_can_msg(dump_out, f"18EEFFFE#{nodename_payload}", timeout=3.0):
dump_out.seek(0)
print(f"DEBUG: Timeout waiting for stop. Log:\n{dump_out.read()}")
pytest.fail("j1939acd did not send Address Release (0xFE) after SIGINT")
finally:
if daemon_proc and daemon_proc.poll() is None: daemon_proc.kill()
if dump_proc.poll() is None: dump_proc.terminate()
dump_out.close()
if os.path.exists(cache_file): os.remove(cache_file)
def test_j1939acd_conflict_defense(bin_path, can_interface):
"""
Test Scenario 2: Conflict Resolution - Defense (Winning).
The DUT (Name: 11...) should defend address 0x50 against a lower priority challenger (Name: FF...).
"""
j1939acd = os.path.join(bin_path, "j1939acd")
candump = os.path.join(bin_path, "candump")
cansend = os.path.join(bin_path, "cansend")
if not os.path.exists(j1939acd): pytest.skip("j1939acd missing")
nodename_hex = "1122334455667788"
dut_payload = "8877665544332211" # Little Endian of DUT Name, compact for candump -L
# Challenger: Name FFFFFFFFFFFFFFFF (Lower Priority)
challenger_payload_send = "FFFFFFFFFFFFFFFF"
challenger_payload_dump = "FFFFFFFFFFFFFFFF"
with tempfile.NamedTemporaryFile(suffix=".jacd", delete=False) as tmp_cache:
cache_file = tmp_cache.name
dump_out = tempfile.TemporaryFile(mode='w+')
dump_proc = subprocess.Popen([candump, "-L", can_interface], stdout=dump_out, stderr=subprocess.PIPE, text=True)
time.sleep(0.5)
daemon_proc = None
try:
# 1. Start DUT (Claims 0x50)
# Using range starting at 80 (decimal) for 0x50
cmd_daemon = [j1939acd, "-r", "80-128", "-c", cache_file, nodename_hex, can_interface]
daemon_proc = subprocess.Popen(cmd_daemon, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# Wait for DUT to actually claim the address BEFORE injecting conflict
print("DEBUG: Waiting for DUT to claim 0x50...")
if not wait_for_can_msg(dump_out, f"18EEFF50#{dut_payload}", timeout=3.0):
dump_out.seek(0)
print(f"DEBUG: DUT failed to claim 0x50 initially. Log:\n{dump_out.read()}")
pytest.fail("DUT did not claim preferred address 0x50 on startup")
# 2. Inject Conflict (Lower Priority)
# ID 18EEFF50 (Claim for 0x50), Payload F...
conflict_cmd = [cansend, can_interface, f"18EEFF50#{challenger_payload_send}"]
print(f"DEBUG: Injecting Conflict (Defense): {' '.join(conflict_cmd)}")
subprocess.run(conflict_cmd, check=True)
# 3. Wait for Defense (DUT should re-send claim)
time.sleep(1.0)
# Analyze
dump_proc.terminate()
dump_proc.wait()
dump_out.seek(0)
log = dump_out.read()
lines = log.splitlines()
injection_index = -1
defense_index = -1
for i, line in enumerate(lines):
# Check for Challenger Frame
if "18EEFF50" in line and challenger_payload_dump in line:
injection_index = i
# Check for DUT Frame
elif "18EEFF50" in line and dut_payload in line:
# We want a DUT claim that appears AFTER the injection
if injection_index != -1 and i > injection_index:
defense_index = i
if injection_index == -1:
print(f"DEBUG: Log content:\n{log}")
pytest.fail("Injected conflict frame not found in candump")
if defense_index == -1:
print(f"DEBUG: Log content:\n{log}")
pytest.fail("DUT did not defend address (no Claim 0x50 sent after injection)")
finally:
if daemon_proc: daemon_proc.send_signal(signal.SIGINT); daemon_proc.wait()
if dump_proc.poll() is None: dump_proc.terminate()
dump_out.close()
if os.path.exists(cache_file): os.remove(cache_file)
def test_j1939acd_conflict_yielding(bin_path, can_interface):
"""
Test Scenario 3: Conflict Resolution - Yielding (Losing).
The DUT (Name: 11...) should yield address 0x50 to a higher priority challenger (Name: 01...)
and claim a new address (e.g., 0x51).
"""
j1939acd = os.path.join(bin_path, "j1939acd")
candump = os.path.join(bin_path, "candump")
cansend = os.path.join(bin_path, "cansend")
if not os.path.exists(j1939acd): pytest.skip("j1939acd missing")
nodename_hex = "1122334455667788"
dut_payload = "8877665544332211"
# Challenger: Name 0000000000000001 (Higher Priority)
challenger_payload_send = "0100000000000000"
challenger_payload_dump = "0100000000000000"
with tempfile.NamedTemporaryFile(suffix=".jacd", delete=False) as tmp_cache:
cache_file = tmp_cache.name
dump_out = tempfile.TemporaryFile(mode='w+')
dump_proc = subprocess.Popen([candump, "-L", can_interface], stdout=dump_out, stderr=subprocess.PIPE, text=True)
time.sleep(0.5)
daemon_proc = None
try:
# 1. Start DUT (Claims 0x50)
# Using decimal 80 for 0x50. Range 80-96.
cmd_daemon = [j1939acd, "-r", "80-96", "-c", cache_file, nodename_hex, can_interface]
daemon_proc = subprocess.Popen(cmd_daemon, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# Wait for DUT to actually claim 0x50 BEFORE injecting
print("DEBUG: Waiting for DUT to claim 0x50...")
if not wait_for_can_msg(dump_out, f"18EEFF50#{dut_payload}", timeout=3.0):
dump_out.seek(0)
print(f"DEBUG: DUT failed to claim 0x50 initially. Log:\n{dump_out.read()}")
pytest.fail("DUT did not claim preferred address 0x50 on startup")
# 2. Inject Conflict (Higher Priority)
conflict_cmd = [cansend, can_interface, f"18EEFF50#{challenger_payload_send}"]
print(f"DEBUG: Injecting Conflict (Yielding): {' '.join(conflict_cmd)}")
subprocess.run(conflict_cmd, check=True)
time.sleep(1.0) # Wait for DUT response (New Claim)
# Analyze
dump_proc.terminate()
dump_proc.wait()
dump_out.seek(0)
log = dump_out.read()
lines = log.splitlines()
injection_index = -1
new_claim_index = -1
for i, line in enumerate(lines):
if "18EEFF50" in line and challenger_payload_dump in line:
injection_index = i
# Look for DUT claiming a NEW address (e.g. 51 hex / 81 dec)
# Frame 18EEFF51
elif "18EEFF51" in line and dut_payload in line:
if injection_index != -1 and i > injection_index:
new_claim_index = i
if injection_index == -1:
print(f"DEBUG: Log content:\n{log}")
pytest.fail("Injected conflict frame not found in candump")
if new_claim_index == -1:
print(f"DEBUG: Log content:\n{log}")
pytest.fail("DUT did not claim new address (0x51) after yielding 0x50")
finally:
if daemon_proc: daemon_proc.send_signal(signal.SIGINT); daemon_proc.wait()
if dump_proc.poll() is None: dump_proc.terminate()
dump_out.close()
if os.path.exists(cache_file): os.remove(cache_file)