# 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 select import shutil import re # --- Helper Functions --- class CanSnifferMonitor: """ Context manager to run cansniffer. Captures stdout/stderr non-blocking way. """ def __init__(self, bin_path, interface, args=None): # CRITICAL: Use stdbuf -o0 to force unbuffered stdout. # cansniffer uses printf, which is fully buffered when writing to a pipe. # Without stdbuf, we would detect empty output until 4KB of data accumulates. if not shutil.which("stdbuf"): pytest.fail("stdbuf utility (coreutils) is required for testing cansniffer TUI") self.cmd = ["stdbuf", "-o0", os.path.join(bin_path, "cansniffer"), interface] if args: self.cmd.extend(args) self.process = None self.output_buffer = "" def __enter__(self): # We need bufsize=0 or unbuffered to get output in real-time self.process = subprocess.Popen( self.cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=0, preexec_fn=os.setsid ) time.sleep(0.5) # Wait for initialization (clearing screen etc) return self def __exit__(self, exc_type, exc_val, exc_tb): if self.process: try: os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) except ProcessLookupError: pass def send_input(self, text): """Sends commands to cansniffer stdin.""" if self.process and self.process.stdin: self.process.stdin.write(text) self.process.stdin.flush() time.sleep(0.5) # Allow processing time (increased for stability) def read_output(self, timeout=1.0): """ Reads available output from stdout without blocking forever. """ collected = "" start = time.time() while time.time() - start < timeout: # Check if there is data to read reads = [self.process.stdout.fileno()] ret = select.select(reads, [], [], 0.1) if ret[0]: # Read chunks chunk = self.process.stdout.read(1024) if not chunk: break collected += chunk else: # If we have collected something and no new data is coming fast, break early # to speed up tests, unless we are waiting for specific timeout if collected and (time.time() - start > 0.3): break self.output_buffer += collected return collected def clear_buffer(self): """Discards currently available output.""" self.read_output(timeout=0.2) # --- Tests for cansniffer --- def test_help_option(bin_path): """Test -? option (cansniffer uses -? for help).""" # Note: The help text says '-?' prints help. result = subprocess.run([os.path.join(bin_path, "cansniffer"), "-?"], capture_output=True, text=True) # cansniffer usually returns error code because ? is invalid opt for standard parsers assert "Usage: cansniffer" in result.stdout or "Usage: cansniffer" in result.stderr def test_basic_sniffing(bin_path, can_interface): """Test if cansniffer shows traffic.""" with CanSnifferMonitor(bin_path, can_interface) as sniffer: # Generate traffic: ID 123 subprocess.run([os.path.join(bin_path, "cansend"), can_interface, "123#112233"], check=True) # Read output out = sniffer.read_output(timeout=2.0) # Check if 123 appears. assert "123" in out def test_filtering_add(bin_path, can_interface): """Test adding specific ID filter (+ID).""" # Start with -q (quiet, all IDs deactivated) with CanSnifferMonitor(bin_path, can_interface, args=["-q"]) as sniffer: # 1. Send ID 123 -> Should NOT appear (quiet mode) subprocess.run([os.path.join(bin_path, "cansend"), can_interface, "123#AA"], check=True) out = sniffer.read_output(timeout=1.0) assert "123" not in out # 2. Add filter +123 via stdin sniffer.send_input("+123\n") # 3. Send ID 123 again -> Should appear now subprocess.run([os.path.join(bin_path, "cansend"), can_interface, "123#BB"], check=True) out = sniffer.read_output(timeout=2.0) assert "123" in out def test_filtering_remove(bin_path, can_interface): """Test removing specific ID filter (-ID).""" # Start normal mode (sniffs all) with CanSnifferMonitor(bin_path, can_interface) as sniffer: # 1. Send ID 456 -> Should appear subprocess.run([os.path.join(bin_path, "cansend"), can_interface, "456#11"], check=True) out = sniffer.read_output(timeout=1.0) assert "456" in out # 2. Remove 456 via stdin sniffer.send_input("-456\n") # Clear buffer to ignore previous output about 456 sniffer.clear_buffer() # Send a different ID to force a screen refresh / activity on a visible ID # This ensures cansniffer produces NEW output subprocess.run([os.path.join(bin_path, "cansend"), can_interface, "789#AA"], check=True) time.sleep(0.2) # 3. Send ID 456 again with NEW unique data (CC) # Using CC is safer than 22 as numbers might appear in timestamps subprocess.run([os.path.join(bin_path, "cansend"), can_interface, "456#CC"], check=True) # Read subsequent output out = sniffer.read_output(timeout=1.5) # Verify 789 is there (sanity check that we captured output) assert "789" in out # Verify 456 (and specifically its new data CC) is NOT in the new block. assert "CC" not in out def test_binary_mode_b(bin_path, can_interface): """Test binary mode output (-b).""" with CanSnifferMonitor(bin_path, can_interface, args=["-b"]) as sniffer: subprocess.run([os.path.join(bin_path, "cansend"), can_interface, "123#DEADBEEF"], check=True) out = sniffer.read_output(timeout=1.0) # In binary mode (-b), data bytes are often visualized as bits/dots. # But the ID "123" should still be visible. assert "123" in out def test_color_mode_c(bin_path, can_interface): """Test color mode (-c).""" # Color output depends on terminal capabilities usually, but cansniffer -c forces it. with CanSnifferMonitor(bin_path, can_interface, args=["-c"]) as sniffer: subprocess.run([os.path.join(bin_path, "cansend"), can_interface, "123#11"], check=True) out = sniffer.read_output(timeout=1.0) # Color mode uses ANSI escape sequences (e.g. \033[...m) # Note: In Python string literals, \x1b is ESC. assert "\x1b[" in out or "\033[" in out def test_clear_screen_space(bin_path, can_interface): """Test clearing screen ().""" with CanSnifferMonitor(bin_path, can_interface) as sniffer: # Give it a moment to startup time.sleep(0.5) # Clear screen command sniffer.send_input("\n") # Just enter sometimes works, or Space+Enter sniffer.send_input(" \n") # If it clears, it emits ANSI clear sequence "\033[2J" or "\033[H" (Home) out = sniffer.read_output(timeout=1.0) # Check for Common ANSI escape sequences for clearing/home # \033[2J = Clear Screen, \033[H = Cursor Home assert "\x1b[2J" in out or "\x1b[H" in out or "\x1b[" in out def test_timeout_t(bin_path, can_interface): """Test timeout for ID display (-t).""" # -t