252 lines
8.2 KiB
Python
252 lines
8.2 KiB
Python
# SPDX-License-Identifier: GPL-2.0-only
|
|
|
|
import subprocess
|
|
import time
|
|
import os
|
|
import pytest
|
|
import socket
|
|
import signal
|
|
import pty
|
|
import select
|
|
import sys
|
|
import tempfile
|
|
|
|
# Note: bin_path and interface fixtures are provided implicitly by conftest.py
|
|
|
|
def check_isotp_support(bin_path, interface):
|
|
"""
|
|
Helper to check if ISO-TP kernel module is loaded/available.
|
|
Reused logic to prevent failing tests on systems without can-isotp.
|
|
"""
|
|
isotpsend = os.path.join(bin_path, "isotpsend")
|
|
if not os.path.exists(isotpsend):
|
|
return False
|
|
|
|
try:
|
|
proc = subprocess.Popen(
|
|
[isotpsend, "-s", "123", "-d", "321", interface],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
try:
|
|
outs, errs = proc.communicate(input="00", timeout=0.5)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
outs, errs = proc.communicate()
|
|
|
|
if "socket: Protocol not supported" in errs or "socket: Protocol not supported" in outs:
|
|
return False
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
def get_free_port():
|
|
"""Find a free port on localhost."""
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(('127.0.0.1', 0))
|
|
return s.getsockname()[1]
|
|
|
|
def test_isotpserver_usage(bin_path):
|
|
"""
|
|
Test usage/help output for isotpserver.
|
|
|
|
Manual Reproduction:
|
|
1. Run: ./isotpserver -h
|
|
2. Expect: Output containing "Usage: isotpserver".
|
|
"""
|
|
isotpserver = os.path.join(bin_path, "isotpserver")
|
|
|
|
if not os.path.exists(isotpserver):
|
|
pytest.skip("isotpserver binary not found")
|
|
|
|
result = subprocess.run(
|
|
[isotpserver, "-h"],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
|
|
assert "Usage: isotpserver" in result.stderr or "Usage: isotpserver" in result.stdout
|
|
|
|
def test_isotpserver_bridging(bin_path, can_interface):
|
|
"""
|
|
Test TCP <-> CAN bridging using isotpserver with specific format <HEX>.
|
|
|
|
Description:
|
|
1. Start candump to monitor bus traffic (diagnostics).
|
|
2. Start isotpserver on a local port.
|
|
3. Start isotprecv to capture CAN frames sent by server.
|
|
4. Connect a TCP client to the server.
|
|
5. Send ASCII HEX via TCP in format <112233> -> Verify reception on CAN (via isotprecv as '11 22 33').
|
|
6. Send ASCII HEX via CAN (isotpsend '44 55 66') -> Verify reception on TCP as <445566>.
|
|
|
|
Manual Reproduction:
|
|
1. ./isotpserver -l 12345 -s 123 -d 321 vcan0
|
|
2. ./isotprecv -s 321 -d 123 vcan0
|
|
3. telnet localhost 12345
|
|
4. Type "<112233>" in telnet -> Check isotprecv output for "11 22 33".
|
|
5. echo "44 55 66" | ./isotpsend -s 321 -d 123 vcan0
|
|
6. Check telnet output for "<445566>".
|
|
"""
|
|
isotpserver = os.path.join(bin_path, "isotpserver")
|
|
isotprecv = os.path.join(bin_path, "isotprecv")
|
|
isotpsend = os.path.join(bin_path, "isotpsend")
|
|
candump = os.path.join(bin_path, "candump")
|
|
|
|
for tool in [isotpserver, isotprecv, isotpsend, candump]:
|
|
if not os.path.exists(tool):
|
|
pytest.skip(f"{tool} not found")
|
|
|
|
if not check_isotp_support(bin_path, can_interface):
|
|
pytest.skip("ISO-TP kernel support missing")
|
|
|
|
port = get_free_port()
|
|
SRC_ID = "123"
|
|
DST_ID = "321"
|
|
|
|
# 0. Start candump for diagnostics
|
|
dump_out = tempfile.TemporaryFile(mode='w+')
|
|
print(f"DEBUG: Starting candump on {can_interface}")
|
|
dump_proc = subprocess.Popen(
|
|
[candump, "-L", can_interface],
|
|
stdout=dump_out,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
|
|
# 1. Start isotpserver
|
|
# We use PIPE for stderr to catch errors
|
|
server_cmd = [isotpserver, "-l", str(port), "-s", SRC_ID, "-d", DST_ID, can_interface]
|
|
print(f"DEBUG: Starting isotpserver: {' '.join(server_cmd)}")
|
|
server_proc = subprocess.Popen(
|
|
server_cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
|
|
recv_master_fd = None
|
|
recv_slave_fd = None
|
|
recv_proc = None
|
|
sock = None
|
|
|
|
try:
|
|
time.sleep(0.5)
|
|
if server_proc.poll() is not None:
|
|
_, err = server_proc.communicate()
|
|
pytest.fail(f"isotpserver failed to start. Stderr: {err}")
|
|
|
|
# 2. Start isotprecv (to receive what server sends to CAN)
|
|
# Use PTY to avoid buffering issues
|
|
recv_master_fd, recv_slave_fd = pty.openpty()
|
|
|
|
recv_cmd = [isotprecv, "-s", DST_ID, "-d", SRC_ID, can_interface]
|
|
print(f"DEBUG: Starting isotprecv: {' '.join(recv_cmd)}")
|
|
recv_proc = subprocess.Popen(
|
|
recv_cmd,
|
|
stdout=recv_slave_fd,
|
|
stderr=subprocess.PIPE, # Capture errors normally
|
|
close_fds=True
|
|
)
|
|
os.close(recv_slave_fd) # Close slave in parent
|
|
recv_slave_fd = None # Mark as closed
|
|
|
|
time.sleep(0.5)
|
|
|
|
# 3. Connect TCP Client
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(2.0)
|
|
sock.connect(('127.0.0.1', port))
|
|
except ConnectionRefusedError:
|
|
pytest.fail(f"Could not connect to isotpserver on port {port}")
|
|
|
|
# --- TCP -> CAN ---
|
|
# The test requirement is to use <HEX> format.
|
|
# Sending <112233> to the server.
|
|
payload_tcp = "<112233>"
|
|
print(f"DEBUG: Sending via TCP: {payload_tcp}")
|
|
sock.sendall(payload_tcp.encode('ascii'))
|
|
|
|
# Give it a moment to process and forward to CAN
|
|
time.sleep(1.0)
|
|
|
|
# Read isotprecv output via PTY
|
|
recv_out = ""
|
|
try:
|
|
# Simple non-blocking read loop
|
|
while True:
|
|
r, _, _ = select.select([recv_master_fd], [], [], 0.1)
|
|
if recv_master_fd in r:
|
|
chunk = os.read(recv_master_fd, 1024)
|
|
if not chunk:
|
|
break
|
|
recv_out += chunk.decode('utf-8', errors='replace')
|
|
else:
|
|
break
|
|
except OSError:
|
|
pass
|
|
|
|
print(f"DEBUG: isotprecv output: {recv_out}")
|
|
|
|
# Check success
|
|
# isotprecv prints standard space-separated hex (e.g., "11 22 33")
|
|
# even if the input was compact <112233>.
|
|
if "11 22 33" not in recv_out:
|
|
print("DEBUG: Failure detected in TCP->CAN. Checking candump...")
|
|
time.sleep(0.5)
|
|
|
|
assert "11 22 33" in recv_out, "TCP -> CAN failed: Data not received on CAN bus (isotprecv empty or mismatch)"
|
|
|
|
# --- CAN -> TCP ---
|
|
# Sending standard space-separated hex on CAN
|
|
payload_can = "44 55 66"
|
|
print(f"DEBUG: Sending via CAN (isotpsend): {payload_can}")
|
|
|
|
send_cmd = [isotpsend, "-s", DST_ID, "-d", SRC_ID, can_interface]
|
|
subprocess.run(
|
|
send_cmd,
|
|
input=payload_can,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
check=True
|
|
)
|
|
|
|
# Receive on TCP
|
|
# Expecting the server to format the output as <445566> based on the requirement.
|
|
try:
|
|
tcp_data = sock.recv(1024).decode('ascii')
|
|
print(f"DEBUG: Received via TCP: {tcp_data}")
|
|
# The expectation is <445566> (compact hex inside brackets)
|
|
assert "<445566>" in tcp_data, "CAN -> TCP failed: Data format mismatch or not received on TCP socket"
|
|
except socket.timeout:
|
|
pytest.fail("CAN -> TCP failed: Timeout waiting for data on TCP socket")
|
|
|
|
finally:
|
|
if sock:
|
|
sock.close()
|
|
|
|
server_proc.terminate()
|
|
server_out, server_err = server_proc.communicate()
|
|
if server_out or server_err:
|
|
print(f"DEBUG: isotpserver stdout: {server_out}")
|
|
print(f"DEBUG: isotpserver stderr: {server_err}")
|
|
|
|
if recv_proc:
|
|
recv_proc.terminate()
|
|
recv_proc.wait()
|
|
|
|
if recv_master_fd:
|
|
os.close(recv_master_fd)
|
|
if recv_slave_fd: # Should be closed already, but for safety
|
|
os.close(recv_slave_fd)
|
|
|
|
dump_proc.terminate()
|
|
dump_proc.wait()
|
|
dump_out.seek(0)
|
|
print(f"DEBUG: candump output:\n{dump_out.read()}")
|
|
dump_out.close()
|