257 lines
10 KiB
Python
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)
|