285 lines
10 KiB
Python
285 lines
10 KiB
Python
# SPDX-License-Identifier: GPL-2.0-only
|
|
#
|
|
# Copyright (c) 2025 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 as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
|
|
import pytest
|
|
import subprocess
|
|
import os
|
|
import time
|
|
import signal
|
|
import re
|
|
|
|
# --- Helper Functions & Classes ---
|
|
|
|
def run_cangen(bin_path, interface, args):
|
|
"""
|
|
Runs cangen to generate traffic.
|
|
Waits for it to finish (assumes -n is used in args).
|
|
"""
|
|
cmd = [os.path.join(bin_path, "cangen"), interface] + args
|
|
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
class CanbusloadMonitor:
|
|
"""
|
|
Context manager to run canbusload.
|
|
Captures stdout/stderr.
|
|
Ensures process is killed to prevent stalls.
|
|
"""
|
|
def __init__(self, bin_path, args):
|
|
self.cmd = [os.path.join(bin_path, "canbusload")] + args
|
|
self.process = None
|
|
self.output = ""
|
|
self.error = ""
|
|
|
|
def __enter__(self):
|
|
# run with setsid to allow killing the whole process group later
|
|
self.process = subprocess.Popen(
|
|
self.cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
preexec_fn=os.setsid
|
|
)
|
|
time.sleep(0.5) # Wait for tool to start and initialize
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
if self.process:
|
|
if self.process.poll() is None:
|
|
# canbusload mentions using CTRL-C (SIGINT) to terminate.
|
|
# SIGTERM might be ignored or handled differently.
|
|
os.killpg(os.getpgid(self.process.pid), signal.SIGINT)
|
|
try:
|
|
# Wait a bit for it to exit gracefully
|
|
self.process.wait(timeout=1.0)
|
|
except subprocess.TimeoutExpired:
|
|
# Force kill if it's stuck (e.g. buffer deadlock)
|
|
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
|
|
|
|
# Read remaining output
|
|
self.output, self.error = self.process.communicate()
|
|
|
|
# --- Tests for canbusload ---
|
|
|
|
def test_help_option(bin_path):
|
|
"""
|
|
Description:
|
|
Tests the -h option to ensure the usage message is displayed.
|
|
|
|
Manual reproduction:
|
|
$ canbusload -h
|
|
> monitor CAN bus load.
|
|
> Usage: canbusload [options] <CAN interface>+
|
|
"""
|
|
result = subprocess.run(
|
|
[os.path.join(bin_path, "canbusload"), "-h"],
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
assert "Usage: canbusload" in result.stdout or "Usage: canbusload" in result.stderr
|
|
assert "monitor CAN bus load" in result.stdout or "monitor CAN bus load" in result.stderr
|
|
|
|
def test_basic_monitoring(bin_path, can_interface):
|
|
"""
|
|
Description:
|
|
Tests basic monitoring of a CAN interface with a specified bitrate.
|
|
Verifies that the interface name appears in the output and frames are counted.
|
|
|
|
Manual reproduction:
|
|
1. Open a terminal and run:
|
|
$ canbusload vcan0@500000
|
|
2. In another terminal, generate traffic:
|
|
$ cangen vcan0 -n 10
|
|
3. Observe the output updating with frame counts.
|
|
"""
|
|
bitrate = "500000"
|
|
target = f"{can_interface}@{bitrate}"
|
|
|
|
with CanbusloadMonitor(bin_path, [target]) as monitor:
|
|
# Generate some traffic so canbusload has something to report
|
|
# Use -g to add a small gap, preventing potential buffer flooding issues on very fast systems
|
|
run_cangen(bin_path, can_interface, ["-n", "10", "-g", "10"])
|
|
time.sleep(1.0) # Wait for next update cycle
|
|
|
|
assert can_interface in monitor.output
|
|
# Check if we saw some frames (regex for at least 1 frame)
|
|
# Output format roughly: can0@500k 10 ...
|
|
assert re.search(r'\s+[1-9][0-9]*\s+', monitor.output), "No frames detected in output"
|
|
|
|
def test_time_option_t(bin_path, can_interface):
|
|
"""
|
|
Description:
|
|
Tests the -t option which shows the current time on the first line.
|
|
|
|
Manual reproduction:
|
|
$ canbusload -t vcan0@500000
|
|
> canbusload 2025-01-01 12:00:00 ...
|
|
"""
|
|
target = f"{can_interface}@500000"
|
|
|
|
with CanbusloadMonitor(bin_path, ["-t", target]) as monitor:
|
|
time.sleep(0.5)
|
|
|
|
# Check for YYYY-MM-DD format in the output header
|
|
assert re.search(r'\d{4}-\d{2}-\d{2}', monitor.output), "Timestamp not found with -t option"
|
|
|
|
def test_bargraph_option_b(bin_path, can_interface):
|
|
"""
|
|
Description:
|
|
Tests the -b option which displays a bargraph of the bus load.
|
|
|
|
Manual reproduction:
|
|
1. Run:
|
|
$ canbusload -b vcan0@500000
|
|
2. Generate heavy traffic:
|
|
$ cangen vcan0 -g 0 -n 100
|
|
3. Observe the ASCII bar graph (e.g., |XXXX....|).
|
|
"""
|
|
target = f"{can_interface}@500000"
|
|
|
|
with CanbusloadMonitor(bin_path, ["-b", target]) as monitor:
|
|
# Generate burst of traffic to spike load
|
|
run_cangen(bin_path, can_interface, ["-n", "50", "-g", "0"])
|
|
time.sleep(1.0)
|
|
|
|
# Check for bargraph delimiters
|
|
assert "|" in monitor.output
|
|
# Check for bargraph content (X for data, . for empty)
|
|
# Note: With low load it might just be dots, but the bar structure |...| should exist
|
|
assert re.search(r'\|[XRT\.]+\|', monitor.output), "Bargraph structure not found"
|
|
|
|
def test_colorize_option_c(bin_path, can_interface):
|
|
"""
|
|
Description:
|
|
Tests the -c option for colorized output.
|
|
Checks for the presence of ANSI escape codes in the output.
|
|
|
|
Manual reproduction:
|
|
$ canbusload -c vcan0@500000
|
|
> Output should be colored (visible in compatible terminal).
|
|
"""
|
|
target = f"{can_interface}@500000"
|
|
|
|
with CanbusloadMonitor(bin_path, ["-c", target]) as monitor:
|
|
run_cangen(bin_path, can_interface, ["-n", "1"])
|
|
time.sleep(0.5)
|
|
|
|
# Check for ANSI escape character (ASCII 27 / \x1b)
|
|
assert "\x1b[" in monitor.output, "ANSI escape codes for color not found"
|
|
|
|
def test_redraw_option_r(bin_path, can_interface):
|
|
"""
|
|
Description:
|
|
Tests the -r option which redraws the terminal (like 'top').
|
|
Checks for clear screen/cursor movement escape sequences.
|
|
|
|
Manual reproduction:
|
|
$ canbusload -r vcan0@500000
|
|
> Screen should refresh instead of scrolling.
|
|
"""
|
|
target = f"{can_interface}@500000"
|
|
|
|
with CanbusloadMonitor(bin_path, ["-r", target]) as monitor:
|
|
time.sleep(0.5)
|
|
|
|
# Usually implies clearing screen or moving cursor home: \x1b[H or \x1b[2J
|
|
assert "\x1b[" in monitor.output, "Cursor movement/redraw codes not found"
|
|
|
|
def test_exact_calculation_e(bin_path, can_interface):
|
|
"""
|
|
Description:
|
|
Tests the -e option for exact calculation of stuffed bits.
|
|
Ensures the tool runs without error and outputs the 'exact' indicator if applicable
|
|
or simply produces valid output.
|
|
|
|
Manual reproduction:
|
|
$ canbusload -e vcan0@500000
|
|
> Header might indicate different mode or output values change slightly.
|
|
"""
|
|
target = f"{can_interface}@500000"
|
|
|
|
with CanbusloadMonitor(bin_path, ["-e", target]) as monitor:
|
|
run_cangen(bin_path, can_interface, ["-n", "10"])
|
|
time.sleep(0.5)
|
|
|
|
# Primarily checking that it runs successfully and produces output
|
|
assert can_interface in monitor.output
|
|
assert re.search(r'\s+[1-9][0-9]*\s+', monitor.output)
|
|
|
|
def test_ignore_bitstuffing_i(bin_path, can_interface):
|
|
"""
|
|
Description:
|
|
Tests the -i option to ignore bitstuffing in bandwidth calculation.
|
|
|
|
Manual reproduction:
|
|
$ canbusload -i vcan0@500000
|
|
"""
|
|
target = f"{can_interface}@500000"
|
|
|
|
with CanbusloadMonitor(bin_path, ["-i", target]) as monitor:
|
|
run_cangen(bin_path, can_interface, ["-n", "10"])
|
|
time.sleep(0.5)
|
|
|
|
assert can_interface in monitor.output
|
|
|
|
def test_multiple_interfaces(bin_path, can_interface):
|
|
"""
|
|
Description:
|
|
Tests monitoring multiple interfaces simultaneously.
|
|
Note: Since we usually only have one 'can_interface' fixture,
|
|
we will simulate the syntax. If the second interface doesn't exist,
|
|
canbusload might still run or show error for that line, but we check
|
|
parsing.
|
|
|
|
To robustly test, we use the same interface twice with different bitrates
|
|
(if supported by tool) or just check that the command line is accepted.
|
|
|
|
Manual reproduction:
|
|
$ canbusload vcan0@500000 vcan0@250000
|
|
> Should list vcan0 twice.
|
|
"""
|
|
# Using the same interface twice to ensure they exist.
|
|
target1 = f"{can_interface}@500000"
|
|
target2 = f"{can_interface}@250000"
|
|
|
|
with CanbusloadMonitor(bin_path, [target1, target2]) as monitor:
|
|
run_cangen(bin_path, can_interface, ["-n", "10"])
|
|
time.sleep(1.0)
|
|
|
|
# Check that the interface appears at least twice in the output lines
|
|
# (ignoring the header line)
|
|
count = monitor.output.count(can_interface)
|
|
# 1 in command echo (if any), 2 in status lines.
|
|
# Since we are capturing output, it likely appears multiple times across updates.
|
|
# We just ensure it's there.
|
|
assert count >= 2, "Multiple interfaces did not appear in output"
|
|
|
|
def test_missing_bitrate_error(bin_path, can_interface):
|
|
"""
|
|
Description:
|
|
Tests that omitting the bitrate results in an error/usage message.
|
|
The bitrate is mandatory.
|
|
|
|
Manual reproduction:
|
|
$ canbusload vcan0
|
|
> Should print usage or error.
|
|
"""
|
|
# Running without @bitrate
|
|
result = subprocess.run(
|
|
[os.path.join(bin_path, "canbusload"), can_interface],
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
# Expect failure code or error message
|
|
assert result.returncode != 0 or "Usage" in result.stdout or "Usage" in result.stderr
|