504 lines
17 KiB
Python
504 lines
17 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 tempfile
|
|
import signal
|
|
import sys
|
|
import struct
|
|
import datetime
|
|
import stat
|
|
import select
|
|
|
|
# Helper function to check for binaries
|
|
def require_binaries(bin_path, binaries):
|
|
for binary in binaries:
|
|
if not os.path.exists(os.path.join(bin_path, binary)):
|
|
pytest.skip(f"Binary '{binary}' not found in {bin_path}")
|
|
|
|
def create_test_file(filepath, size_bytes):
|
|
"""
|
|
Python implementation of isobusfs_create_test_file.sh
|
|
Generates a file with a specific XOR pattern (0xdeadbeef).
|
|
"""
|
|
pattern = 0xdeadbeef
|
|
# Each iteration writes 4 bytes
|
|
iterations = size_bytes // 4
|
|
|
|
with open(filepath, 'wb') as f:
|
|
# Write in chunks to improve performance for larger files
|
|
chunk_size = 4096 # iterations per chunk
|
|
for i in range(0, iterations, chunk_size):
|
|
chunk_end = min(i + chunk_size, iterations)
|
|
data = bytearray()
|
|
for counter in range(i, chunk_end):
|
|
# bash: printf "%08x" $((counter ^ pattern)) | xxd -r -p
|
|
# This results in Big-Endian binary representation
|
|
val = counter ^ pattern
|
|
data.extend(struct.pack('>I', val))
|
|
f.write(data)
|
|
|
|
def create_test_infrastructure(root):
|
|
"""
|
|
Python implementation of isobusfs_create_test_dirs.sh
|
|
Creates the complex directory structure, special files, and timestamps.
|
|
"""
|
|
print(f"DEBUG: Generating test infrastructure in {root}")
|
|
|
|
# 1. Create Directory Hierarchy
|
|
# NOTE: Changed MCMC0683 to MCMC0000 to match the server's default fallback
|
|
# when no local name is provided via -n argument.
|
|
dirs = [
|
|
"dir1/dir2/dir3/dir4",
|
|
"dir1/dir2/dir3/dir5",
|
|
"MCMC0000/msd_dir1/msd_dir2/~/~tilde_dir",
|
|
"dir1/~/~",
|
|
"dir1/dir2/special_chars_*?/",
|
|
"dir1/dir2/unicode_名字",
|
|
]
|
|
for d in dirs:
|
|
os.makedirs(os.path.join(root, d), exist_ok=True)
|
|
|
|
# 2. Create simple text file
|
|
with open(os.path.join(root, "dir1/dir2/file0"), "w") as f:
|
|
f.write("hello\n")
|
|
|
|
# 3. Create binary test files (1KB and 1MB)
|
|
create_test_file(os.path.join(root, "dir1/dir2/file1k"), 1024)
|
|
create_test_file(os.path.join(root, "dir1/dir2/file1m"), 1048576)
|
|
|
|
# 4. Create 300 files with long names
|
|
# Suffix reconstructed based on visual pattern (alphabet repeated ~8.5 times ending in 'p')
|
|
alphabet = "abcdefghijklmnopqrstuvwxyz"
|
|
long_suffix = (alphabet * 8) + alphabet[:16] # Results in 224 chars
|
|
|
|
for count in range(1, 301):
|
|
fname = f"long_name_{count}_{long_suffix}"
|
|
with open(os.path.join(root, fname), 'w') as f:
|
|
pass
|
|
|
|
# 5. Create special permission files
|
|
base_perm_dir = os.path.join(root, "dir1/dir2")
|
|
|
|
# hidden_file
|
|
with open(os.path.join(base_perm_dir, "hidden_file"), 'w') as f:
|
|
pass
|
|
|
|
# readonly_file (chmod 444)
|
|
ro_path = os.path.join(base_perm_dir, "readonly_file")
|
|
with open(ro_path, 'w') as f:
|
|
pass
|
|
os.chmod(ro_path, 0o444)
|
|
|
|
# executable_file (chmod +x -> 755 typically)
|
|
exe_path = os.path.join(base_perm_dir, "executable_file")
|
|
with open(exe_path, 'w') as f:
|
|
pass
|
|
current_mode = os.stat(exe_path).st_mode
|
|
os.chmod(exe_path, current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
|
|
# no_read_permission_file (chmod 000)
|
|
no_read_path = os.path.join(base_perm_dir, "no_read_permission_file")
|
|
with open(no_read_path, 'w') as f:
|
|
pass
|
|
os.chmod(no_read_path, 0o000)
|
|
|
|
# 6. Date/Timestamp Problems
|
|
# Note: timestamps might be limited by the host OS (Y2038 on 32-bit systems)
|
|
date_dirs = {
|
|
"y2000_problem": "2000-01-01 00:00:00",
|
|
"y2038_problem": "2038-01-19 03:14:07",
|
|
"y1979_problem": "1979-12-31 23:59:59",
|
|
"y1980_problem": "1980-01-01 00:00:00",
|
|
"y2107_problem": "2107-12-31 23:59:59",
|
|
"y2108_problem": "2108-01-01 00:00:00",
|
|
}
|
|
|
|
for dirname, date_str in date_dirs.items():
|
|
dir_path = os.path.join(base_perm_dir, dirname)
|
|
os.makedirs(dir_path, exist_ok=True)
|
|
try:
|
|
dt = datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
|
|
ts = dt.timestamp()
|
|
# Set access and modification time
|
|
os.utime(dir_path, (ts, ts))
|
|
except (OverflowError, OSError, ValueError):
|
|
print(f"WARNING: Could not set timestamp {date_str} for {dirname} on this platform.")
|
|
|
|
def test_isobusfs_selftest(bin_path, can_interface):
|
|
"""
|
|
Test ISOBUS File Server interaction using the client's 'selftest' command.
|
|
Includes generation of complex file infrastructure.
|
|
|
|
Description:
|
|
1. Create a temporary directory.
|
|
2. Populate it with complex file structure (long names, deep dirs, special permissions).
|
|
3. Start isobusfs-srv.
|
|
4. Start isobusfs-cli and run 'selftest'.
|
|
5. Verify Test 1-7 PASS explicitly.
|
|
6. Verify Test 8 PASS or prove successful data transfer (partial pass).
|
|
7. If tests fail, run internal 'dmesg' command on client.
|
|
|
|
Manual Reproduction:
|
|
1. Run isobusfs_create_test_dirs.sh inside a test folder.
|
|
2. ./isobusfs-srv -i vcan0 -a 80 -v vol1:/path/to/folder -l 3
|
|
3. ./isobusfs-cli -i vcan0 -a 90 -r 80 -l 3
|
|
4. Type 'selftest'
|
|
"""
|
|
server_bin = "isobusfs-srv"
|
|
client_bin = "isobusfs-cli"
|
|
require_binaries(bin_path, [server_bin, client_bin])
|
|
|
|
server_exe = os.path.join(bin_path, server_bin)
|
|
client_exe = os.path.join(bin_path, client_bin)
|
|
|
|
# Use a temporary directory as the volume root
|
|
with tempfile.TemporaryDirectory() as tmp_vol_dir:
|
|
# 1. Generate Infrastructure
|
|
create_test_infrastructure(tmp_vol_dir)
|
|
|
|
# Define addresses (hex)
|
|
srv_addr = "80"
|
|
cli_addr = "90"
|
|
vol_name = "vol1"
|
|
|
|
# Construct Server Command
|
|
server_cmd = [
|
|
server_exe,
|
|
"-i", can_interface,
|
|
"-a", srv_addr,
|
|
"-v", f"{vol_name}:{tmp_vol_dir}",
|
|
"-l", "3"
|
|
]
|
|
|
|
print(f"DEBUG: Starting Server: {' '.join(server_cmd)}")
|
|
|
|
# Start Server
|
|
server_proc = subprocess.Popen(
|
|
server_cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
|
|
client_proc = None
|
|
|
|
try:
|
|
# Wait for server initialization
|
|
time.sleep(1)
|
|
|
|
if server_proc.poll() is not None:
|
|
_, err = server_proc.communicate()
|
|
pytest.fail(f"isobusfs-srv failed to start. Error: {err.decode('utf-8', errors='replace')}")
|
|
|
|
# Construct Client Command
|
|
client_cmd = [
|
|
client_exe,
|
|
"-i", can_interface,
|
|
"-a", cli_addr,
|
|
"-r", srv_addr,
|
|
"-l", "3"
|
|
]
|
|
|
|
print(f"DEBUG: Starting Client: {' '.join(client_cmd)}")
|
|
|
|
# Start Client
|
|
client_proc = subprocess.Popen(
|
|
client_cmd,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
bufsize=0
|
|
)
|
|
|
|
# Interact with Client
|
|
print("DEBUG: Sending 'selftest' command...")
|
|
client_proc.stdin.write("selftest\n")
|
|
client_proc.stdin.flush()
|
|
|
|
# Allow time for tests (20s)
|
|
time.sleep(20)
|
|
|
|
# After tests, request internal dmesg to check for errors
|
|
print("DEBUG: Sending 'dmesg' command to client...")
|
|
client_proc.stdin.write("dmesg\n")
|
|
client_proc.stdin.flush()
|
|
|
|
# Give dmesg a moment to output
|
|
time.sleep(1)
|
|
|
|
client_proc.terminate()
|
|
|
|
try:
|
|
outs, errs = client_proc.communicate(timeout=1)
|
|
except subprocess.TimeoutExpired:
|
|
client_proc.kill()
|
|
outs, errs = client_proc.communicate()
|
|
|
|
print("-" * 20 + " Client Output " + "-" * 20)
|
|
print(outs)
|
|
print("-" * 20 + " Client Errors " + "-" * 20)
|
|
print(errs)
|
|
print("-" * 50)
|
|
|
|
# Verification logic
|
|
failures = []
|
|
|
|
# Tests 1-7 MUST pass
|
|
mandatory_tests = [
|
|
"Test 1: PASSED",
|
|
"Test 2: PASSED",
|
|
"Test 3: PASSED",
|
|
"Test 4: PASSED",
|
|
"Test 5: PASSED",
|
|
"Test 6: PASSED",
|
|
"Test 7: PASSED",
|
|
"Test 8: PASSED",
|
|
]
|
|
|
|
for test in mandatory_tests:
|
|
if test not in outs:
|
|
failures.append(test.replace(": PASSED", ""))
|
|
|
|
if failures:
|
|
pytest.fail(f"The following ISOBUS FS tests failed: {', '.join(failures)}. See output above for details and internal dmesg log.")
|
|
|
|
finally:
|
|
if server_proc.poll() is None:
|
|
server_proc.terminate()
|
|
try:
|
|
server_out, server_err = server_proc.communicate(timeout=1)
|
|
except subprocess.TimeoutExpired:
|
|
server_proc.kill()
|
|
server_out, server_err = server_proc.communicate()
|
|
|
|
print("-" * 20 + " Server Output " + "-" * 20)
|
|
# Manually decode output, replacing invalid characters
|
|
if server_out:
|
|
print(server_out.decode('utf-8', errors='replace'))
|
|
if server_err:
|
|
print(server_err.decode('utf-8', errors='replace'))
|
|
print("-" * 50)
|
|
|
|
if client_proc and client_proc.poll() is None:
|
|
client_proc.kill()
|
|
|
|
def test_isobusfs_interactive_commands(bin_path, can_interface):
|
|
"""
|
|
Test the interactive command shell of isobusfs-cli.
|
|
|
|
Covers:
|
|
- help: Help text display
|
|
- ls/ll: Directory listing
|
|
- cd: Directory navigation
|
|
- pwd: Working directory reporting (ISOBUS path format checks)
|
|
- get: File download
|
|
- dmesg: Log buffer access (executed at the end)
|
|
- exit: Clean exit
|
|
- quit: Clean exit (tested in separate session)
|
|
"""
|
|
server_bin = "isobusfs-srv"
|
|
client_bin = "isobusfs-cli"
|
|
require_binaries(bin_path, [server_bin, client_bin])
|
|
|
|
# FIX 1: Use absolute paths to find binaries even when CWD changes
|
|
server_exe = os.path.abspath(os.path.join(bin_path, server_bin))
|
|
client_exe = os.path.abspath(os.path.join(bin_path, client_bin))
|
|
|
|
# Helper function for truly interactive communication
|
|
def interact(proc, cmd=None, expect_prompt=True, timeout=5):
|
|
"""
|
|
Sends a command and reads output until the prompt 'isobusfs> ' appears.
|
|
Using binary mode (bytes) to avoid buffering issues.
|
|
"""
|
|
if cmd:
|
|
proc.stdin.write(cmd.encode('utf-8') + b"\n")
|
|
proc.stdin.flush()
|
|
|
|
if not expect_prompt:
|
|
return ""
|
|
|
|
output = b""
|
|
start_time = time.time()
|
|
|
|
while time.time() - start_time < timeout:
|
|
# Check if stdout has data ready to read
|
|
rlist, _, _ = select.select([proc.stdout], [], [], 0.1)
|
|
if rlist:
|
|
# Read 1 byte at a time to safely detect the prompt boundary
|
|
char = proc.stdout.read(1)
|
|
if not char: # EOF
|
|
break
|
|
output += char
|
|
if output.endswith(b"isobusfs> "):
|
|
return output.decode('utf-8', errors='replace')
|
|
|
|
# Check if process died
|
|
if proc.poll() is not None:
|
|
break
|
|
|
|
# If we timed out but got some output, return it for debugging/assertion
|
|
return output.decode('utf-8', errors='replace')
|
|
|
|
# Use temporary directories
|
|
with tempfile.TemporaryDirectory() as srv_dir, tempfile.TemporaryDirectory() as cli_dir:
|
|
# 1. Prepare Server Data
|
|
create_test_infrastructure(srv_dir)
|
|
|
|
# 2. Start Server
|
|
srv_addr = "80"
|
|
cli_addr = "95"
|
|
vol_name = "testvol"
|
|
|
|
server_cmd = [
|
|
server_exe,
|
|
"-i", can_interface,
|
|
"-a", srv_addr,
|
|
"-v", f"{vol_name}:{srv_dir}",
|
|
"-l", "3"
|
|
]
|
|
|
|
print(f"DEBUG: Starting Server: {' '.join(server_cmd)}")
|
|
server_proc = subprocess.Popen(
|
|
server_cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE
|
|
)
|
|
|
|
client_proc = None
|
|
|
|
try:
|
|
time.sleep(1)
|
|
if server_proc.poll() is not None:
|
|
_, err = server_proc.communicate()
|
|
pytest.fail(f"Server failed to start: {err.decode('utf-8', errors='replace')}")
|
|
|
|
client_cmd = [
|
|
client_exe,
|
|
"-i", can_interface,
|
|
"-a", cli_addr,
|
|
"-r", srv_addr,
|
|
"-l", "3"
|
|
]
|
|
|
|
print(f"DEBUG: Running Client Session 1 in {cli_dir}")
|
|
# FIX 2: Use bufsize=0 and binary streams for precise control
|
|
client_proc = subprocess.Popen(
|
|
client_cmd,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
cwd=cli_dir,
|
|
bufsize=0
|
|
)
|
|
|
|
# --- Session 1: Main Workflow ---
|
|
|
|
# 1. Initial Prompt (wait for startup)
|
|
out = interact(client_proc)
|
|
assert "Interactive mode" in out
|
|
|
|
# 2. Help
|
|
out = interact(client_proc, "help")
|
|
assert "exit - exit interactive mode" in out
|
|
|
|
# 3. Pwd (Root)
|
|
# FIX 3: Expect ISOBUS style path: \\<volume_name>
|
|
out = interact(client_proc, "pwd")
|
|
assert f"\\\\{vol_name}" in out
|
|
|
|
# 4. ls
|
|
out = interact(client_proc, "ls")
|
|
assert "dir1" in out
|
|
assert "long_name_1_" in out # Verify we still see some long names
|
|
|
|
# 5. cd dir1
|
|
out = interact(client_proc, "cd dir1")
|
|
|
|
# 6. pwd (check change)
|
|
out = interact(client_proc, "pwd")
|
|
assert f"\\\\{vol_name}\\dir1" in out
|
|
|
|
# 7. ll
|
|
out = interact(client_proc, "ll")
|
|
assert "dir2" in out
|
|
|
|
# 8. cd dir2
|
|
out = interact(client_proc, "cd dir2")
|
|
|
|
# 9. pwd
|
|
out = interact(client_proc, "pwd")
|
|
assert f"\\\\{vol_name}\\dir1\\dir2" in out
|
|
|
|
# 10. ls
|
|
out = interact(client_proc, "ls")
|
|
assert "file0" in out
|
|
|
|
# 11. get file0
|
|
out = interact(client_proc, "get file0")
|
|
assert "File transfer completed" in out
|
|
|
|
# Verify file content
|
|
downloaded_file = os.path.join(cli_dir, "file0")
|
|
assert os.path.exists(downloaded_file), "Command 'get file0' failed."
|
|
with open(downloaded_file, "r") as f:
|
|
assert f.read() == "hello\n"
|
|
|
|
# 12. Dmesg (Moved to end to prevent buffer output interference)
|
|
out = interact(client_proc, "dmesg")
|
|
|
|
# 13. Exit
|
|
client_proc.stdin.write(b"exit\n")
|
|
client_proc.stdin.flush()
|
|
try:
|
|
client_proc.wait(timeout=2)
|
|
except subprocess.TimeoutExpired:
|
|
client_proc.kill()
|
|
pytest.fail("Client did not exit cleanly after 'exit' command")
|
|
|
|
# FIX 4: Accept 252 (which is -EINTR / -4 cast to unsigned 8-bit)
|
|
assert client_proc.returncode in [0, -2, 252]
|
|
|
|
# --- Session 2: Test 'quit' alias ---
|
|
print("DEBUG: Running Client Session 2 to test 'quit'")
|
|
client_proc = subprocess.Popen(
|
|
client_cmd,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
bufsize=0
|
|
)
|
|
|
|
# Consume initial prompt
|
|
interact(client_proc)
|
|
|
|
# Send quit
|
|
client_proc.stdin.write(b"quit\n")
|
|
client_proc.stdin.flush()
|
|
client_proc.wait(timeout=2)
|
|
|
|
assert client_proc.returncode in [0, -2, 252]
|
|
|
|
finally:
|
|
# Cleanup
|
|
if server_proc.poll() is None:
|
|
server_proc.terminate()
|
|
server_proc.wait(timeout=1)
|
|
|
|
if client_proc and client_proc.poll() is None:
|
|
client_proc.kill()
|