Files
wehub-resource-sync c3bf08ac8d
K8s Workspace Integration Tests / k8s-workspace-tests (push) Has been cancelled
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Web UI / check (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:27 +08:00

330 lines
13 KiB
Python

# -*- coding: utf-8 -*-
"""Test cases for :class:`LocalBackend` and the backend helpers.
Exercises the three abstract primitives (``exec_shell``, ``read_file``,
``write_file``) plus the derived filesystem helpers (``file_exists``,
``is_dir``, ``list_dir``, ``stat_mtime``, ``delete_path``) of the
host-local backend, and the module-level ``normalize_newlines`` helper.
``LocalBackend`` is designed to run on every platform (it spawns
programs from an argv list without a shell and implements the
filesystem helpers with native ``os.*`` calls), so the bulk of this
module runs on Windows too. Only the handful of cases that genuinely
rely on a POSIX shell / POSIX-only utilities are skipped on Windows.
"""
import os
import sys
import tempfile
import unittest
from unittest.async_case import IsolatedAsyncioTestCase
from agentscope.tool import ExecResult, LocalBackend
from agentscope.tool._builtin._backend import _normalize_newlines
_IS_WINDOWS = sys.platform == "win32"
class TestNormalizeNewlines(unittest.TestCase):
"""Unit tests for the ``normalize_newlines`` helper (pure, no I/O)."""
def test_crlf_collapsed_to_lf(self) -> None:
"""Windows ``\\r\\n`` is collapsed to a single ``\\n``."""
self.assertEqual(
_normalize_newlines("a\r\nb\r\nc"),
"a\nb\nc",
)
def test_lone_cr_collapsed_to_lf(self) -> None:
"""Classic-Mac lone ``\\r`` is collapsed to ``\\n``."""
self.assertEqual(_normalize_newlines("a\rb\rc"), "a\nb\nc")
def test_mixed_endings(self) -> None:
"""A mix of ``\\r\\n``, ``\\r`` and ``\\n`` normalizes uniformly."""
self.assertEqual(
_normalize_newlines("a\r\nb\rc\nd"),
"a\nb\nc\nd",
)
def test_plain_lf_unchanged(self) -> None:
"""Text already using ``\\n`` is returned unchanged."""
self.assertEqual(_normalize_newlines("a\nb\nc"), "a\nb\nc")
def test_no_double_collapse(self) -> None:
"""``\\r\\n`` becomes exactly one ``\\n`` (not two)."""
self.assertEqual(_normalize_newlines("a\r\n\r\nb"), "a\n\nb")
class TestLocalBackendExec(IsolatedAsyncioTestCase):
"""Test cases for ``LocalBackend.exec_shell``."""
async def asyncSetUp(self) -> None:
"""Build a fresh backend per test."""
self.backend = LocalBackend()
async def test_exec_returns_stdout(self) -> None:
"""A program's stdout/exit code are captured into ``ExecResult``.
Uses the current interpreter so the test is portable across
platforms (no reliance on ``echo`` / shell builtins).
"""
result = await self.backend.exec_shell(
[sys.executable, "-c", "print('hello world')"],
)
self.assertIsInstance(result, ExecResult)
self.assertTrue(result.ok())
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.stdout.decode().strip(), "hello world")
self.assertEqual(result.stderr, b"")
async def test_exec_captures_stderr_and_exit_code(self) -> None:
"""A non-zero exit and stderr bytes are reported faithfully."""
result = await self.backend.exec_shell(
[
sys.executable,
"-c",
"import sys; sys.stderr.write('boom'); sys.exit(3)",
],
)
self.assertFalse(result.ok())
self.assertEqual(result.exit_code, 3)
self.assertEqual(result.stderr.decode().strip(), "boom")
async def test_exec_argv_not_split_by_shell(self) -> None:
"""Arguments are passed verbatim (no shell word-splitting/globbing).
A single argument containing spaces and shell metacharacters must
reach the program intact, proving no shell is interposed.
"""
tricky = "a b $(echo x) | & ; '\""
result = await self.backend.exec_shell(
[sys.executable, "-c", "import sys; print(sys.argv[1])", tricky],
)
self.assertTrue(result.ok())
self.assertEqual(result.stdout.decode().rstrip("\r\n"), tricky)
async def test_exec_cwd_is_respected(self) -> None:
"""``cwd`` sets the subprocess working directory."""
with tempfile.TemporaryDirectory() as tmp:
result = await self.backend.exec_shell(
[sys.executable, "-c", "import os; print(os.getcwd())"],
cwd=tmp,
)
self.assertTrue(result.ok())
self.assertEqual(
os.path.realpath(result.stdout.decode().strip()),
os.path.realpath(tmp),
)
async def test_missing_executable_returns_127(self) -> None:
"""An unspawnable executable yields exit code 127 (not an exception).
Mirrors a shell's "command not found" so callers see a normal
non-zero ``ExecResult``.
"""
result = await self.backend.exec_shell(
["this_executable_does_not_exist_xyz", "--nope"],
)
self.assertEqual(result.exit_code, 127)
self.assertEqual(result.stdout, b"")
self.assertNotEqual(result.stderr, b"")
async def test_timeout_returns_minus_one(self) -> None:
"""A command exceeding ``timeout`` is killed and reports -1.
The sentinel ``ExecResult(exit_code=-1, stderr=b"timed out")`` is
what Grep/Glob check for, so it is asserted exactly.
"""
result = await self.backend.exec_shell(
[sys.executable, "-c", "import time; time.sleep(10)"],
timeout=0.2,
)
self.assertEqual(result.exit_code, -1)
self.assertEqual(result.stderr, b"timed out")
class TestLocalBackendFileIO(IsolatedAsyncioTestCase):
"""Test cases for ``read_file`` / ``write_file`` round-trips."""
async def asyncSetUp(self) -> None:
"""Build a backend and a temp dir per test."""
# pylint: disable=consider-using-with
self.backend = LocalBackend()
self.temp_dir = tempfile.TemporaryDirectory()
async def asyncTearDown(self) -> None:
"""Drop the temp dir."""
self.temp_dir.cleanup()
async def test_write_then_read_roundtrip(self) -> None:
"""Bytes written are read back verbatim."""
path = os.path.join(self.temp_dir.name, "file.txt")
payload = b"hello\nworld\n"
await self.backend.write_file(path, payload)
self.assertEqual(await self.backend.read_file(path), payload)
async def test_write_creates_parent_dirs(self) -> None:
"""``write_file`` creates missing parent directories."""
path = os.path.join(self.temp_dir.name, "a", "b", "c", "file.txt")
await self.backend.write_file(path, b"x")
self.assertTrue(os.path.exists(path))
self.assertEqual(await self.backend.read_file(path), b"x")
async def test_write_preserves_binary_and_crlf(self) -> None:
"""Raw bytes (incl. ``\\r\\n`` and NULs) survive the round-trip.
The backend deals in raw bytes; newline normalization happens
later in the text layer, never here.
"""
path = os.path.join(self.temp_dir.name, "bin.dat")
payload = b"a\r\nb\x00\xffc"
await self.backend.write_file(path, payload)
self.assertEqual(await self.backend.read_file(path), payload)
async def test_read_missing_file_raises(self) -> None:
"""Reading a non-existent file raises ``FileNotFoundError``."""
path = os.path.join(self.temp_dir.name, "nope.txt")
with self.assertRaises(FileNotFoundError):
await self.backend.read_file(path)
class TestLocalBackendFilesystemHelpers(IsolatedAsyncioTestCase):
"""Test cases for the derived filesystem helpers (native ``os.*``)."""
async def asyncSetUp(self) -> None:
"""Build a backend and a temp dir per test."""
# pylint: disable=consider-using-with
self.backend = LocalBackend()
self.temp_dir = tempfile.TemporaryDirectory()
async def asyncTearDown(self) -> None:
"""Drop the temp dir."""
self.temp_dir.cleanup()
async def test_file_exists(self) -> None:
"""``file_exists`` is True for files and dirs, False otherwise."""
path = os.path.join(self.temp_dir.name, "f.txt")
await self.backend.write_file(path, b"x")
self.assertTrue(await self.backend.file_exists(path))
self.assertTrue(await self.backend.file_exists(self.temp_dir.name))
self.assertFalse(
await self.backend.file_exists(
os.path.join(self.temp_dir.name, "missing"),
),
)
async def test_is_dir(self) -> None:
"""``is_dir`` distinguishes directories from files."""
path = os.path.join(self.temp_dir.name, "f.txt")
await self.backend.write_file(path, b"x")
self.assertTrue(await self.backend.is_dir(self.temp_dir.name))
self.assertFalse(await self.backend.is_dir(path))
self.assertFalse(
await self.backend.is_dir(
os.path.join(self.temp_dir.name, "missing"),
),
)
async def test_list_dir_shallow(self) -> None:
"""Non-recursive ``list_dir`` returns immediate child base names."""
for name in ("a.txt", "b.txt"):
await self.backend.write_file(
os.path.join(self.temp_dir.name, name),
b"x",
)
os.makedirs(os.path.join(self.temp_dir.name, "sub"))
entries = await self.backend.list_dir(self.temp_dir.name)
self.assertEqual(sorted(entries), ["a.txt", "b.txt", "sub"])
async def test_list_dir_recursive(self) -> None:
"""Recursive ``list_dir`` returns file paths underneath the root."""
await self.backend.write_file(
os.path.join(self.temp_dir.name, "top.txt"),
b"x",
)
await self.backend.write_file(
os.path.join(self.temp_dir.name, "sub", "nested.txt"),
b"x",
)
entries = await self.backend.list_dir(
self.temp_dir.name,
recursive=True,
)
basenames = sorted(os.path.basename(e) for e in entries)
self.assertEqual(basenames, ["nested.txt", "top.txt"])
async def test_stat_mtime(self) -> None:
"""``stat_mtime`` returns a float for an existing path, None else."""
path = os.path.join(self.temp_dir.name, "f.txt")
await self.backend.write_file(path, b"x")
mtime = await self.backend.stat_mtime(path)
self.assertIsInstance(mtime, float)
self.assertIsNone(
await self.backend.stat_mtime(
os.path.join(self.temp_dir.name, "missing"),
),
)
async def test_delete_path_file(self) -> None:
"""``delete_path`` removes a single file."""
path = os.path.join(self.temp_dir.name, "f.txt")
await self.backend.write_file(path, b"x")
await self.backend.delete_path(path)
self.assertFalse(os.path.exists(path))
async def test_delete_path_tree(self) -> None:
"""``delete_path`` removes a directory tree recursively."""
nested = os.path.join(self.temp_dir.name, "d", "e")
os.makedirs(nested)
await self.backend.write_file(
os.path.join(nested, "f.txt"),
b"x",
)
target = os.path.join(self.temp_dir.name, "d")
await self.backend.delete_path(target)
self.assertFalse(os.path.exists(target))
async def test_delete_path_missing_is_noop(self) -> None:
"""Deleting a non-existent path is a silent no-op (like rm -rf)."""
# Must not raise.
await self.backend.delete_path(
os.path.join(self.temp_dir.name, "missing"),
)
@unittest.skipIf(
_IS_WINDOWS,
"POSIX shell (/bin/sh) is not available on Windows",
)
class TestLocalBackendShellWrapping(IsolatedAsyncioTestCase):
"""Cases that explicitly use ``sh -c`` (POSIX-only, skipped on Windows).
The backend primitive never invokes a shell itself; these tests cover
the documented escape hatch where a *caller* wraps a command line as
``["/bin/sh", "-c", line]`` to use pipes / redirects / ``&&``.
"""
async def asyncSetUp(self) -> None:
"""Build a backend per test."""
self.backend = LocalBackend()
async def test_sh_c_pipeline(self) -> None:
"""A piped command line runs when wrapped in ``sh -c``."""
result = await self.backend.exec_shell(
["/bin/sh", "-c", "printf 'a\\nb\\na\\n' | sort | uniq"],
)
self.assertTrue(result.ok())
self.assertEqual(result.stdout.decode().split(), ["a", "b"])
async def test_sh_c_and_chain(self) -> None:
"""``&&`` chaining works through ``sh -c``."""
result = await self.backend.exec_shell(
["/bin/sh", "-c", "true && echo chained"],
)
self.assertTrue(result.ok())
self.assertEqual(result.stdout.decode().strip(), "chained")
if __name__ == "__main__":
unittest.main()