Files
Zoo Queenandhanzo-dev 5ad982e555 fix(lint): resolve all ruff + black errors across all packages
Fix 35 ruff errors (E722, F841, B904, B905, B007, E741, S202, S310,
S306, S110, TID251, UP042, F401) and run black on 526 files.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-02-21 23:32:32 -08:00

436 lines
13 KiB
Python

"""Comprehensive tests for Shellflow DSL parser and execution."""
import time
import asyncio
from typing import Any
import pytest
from hanzo_tools.shell.shellflow import (
parse,
compile,
to_sexp,
_tokenize,
from_sexp,
_normalize,
to_commands,
render_ascii,
)
class TestTokenizer:
"""Tests for shellflow tokenization."""
def test_simple_command(self):
assert _tokenize("ls -la") == ["ls -la"]
def test_sequential_commands(self):
tokens = _tokenize("A ; B ; C")
assert tokens == ["A", ";", "B", ";", "C"]
def test_parallel_block(self):
tokens = _tokenize("{ A & B & C }")
assert tokens == ["{ A & B & C }"]
def test_mixed_syntax(self):
tokens = _tokenize("A ; { B & C } ; D")
assert tokens == ["A", ";", "{ B & C }", ";", "D"]
def test_nested_braces(self):
tokens = _tokenize("A ; { B ; { C & D } } ; E")
assert tokens == ["A", ";", "{ B ; { C & D } }", ";", "E"]
def test_empty_input(self):
assert _tokenize("") == []
assert _tokenize(" ") == []
def test_commands_with_semicolons_in_strings(self):
# Commands containing semicolons in quoted strings
tokens = _tokenize('echo "a;b" ; echo c')
assert len(tokens) == 3 # echo "a;b", ;, echo c
def test_commands_with_pipes(self):
tokens = _tokenize("cat file | grep pattern ; echo done")
assert tokens == ["cat file | grep pattern", ";", "echo done"]
def test_commands_with_and_or(self):
tokens = _tokenize("cmd1 && cmd2 || cmd3 ; cmd4")
assert tokens == ["cmd1 && cmd2 || cmd3", ";", "cmd4"]
class TestParser:
"""Tests for shellflow parsing to AST."""
def test_simple_command(self):
ast = parse("ls -la")
assert ast == {"type": "do", "steps": ["ls -la"]}
def test_sequential(self):
ast = parse("A ; B ; C")
assert ast == {"type": "do", "steps": ["A", "B", "C"]}
def test_parallel(self):
ast = parse("{ A & B & C }")
assert ast == {"type": "all", "steps": ["A", "B", "C"]}
def test_mixed_dag(self):
ast = parse("setup ; { task1 & task2 } ; cleanup")
assert ast == {
"type": "do",
"steps": [
"setup",
{"type": "all", "steps": ["task1", "task2"]},
"cleanup",
],
}
def test_complex_dag(self):
ast = parse("A ; { B & C & D } ; { E & F } ; G")
assert ast == {
"type": "do",
"steps": [
"A",
{"type": "all", "steps": ["B", "C", "D"]},
{"type": "all", "steps": ["E", "F"]},
"G",
],
}
def test_empty_input(self):
ast = parse("")
assert ast == {"type": "do", "steps": []}
def test_whitespace_handling(self):
ast = parse(" A ; B ; C ")
assert ast == {"type": "do", "steps": ["A", "B", "C"]}
def test_single_parallel_task(self):
ast = parse("{ A }")
# Single item in parallel should be normalized
assert ast["steps"] == ["A"] or ast == "A"
def test_real_world_example(self):
src = "mkdir -p dist ; { cp a.txt dist/ & cp b.txt dist/ & cp c.txt dist/ } ; zip -r out.zip dist/"
ast = parse(src)
assert ast["type"] == "do"
assert len(ast["steps"]) == 3
assert ast["steps"][0] == "mkdir -p dist"
assert ast["steps"][1]["type"] == "all"
assert len(ast["steps"][1]["steps"]) == 3
assert ast["steps"][2] == "zip -r out.zip dist/"
class TestNormalization:
"""Tests for AST normalization."""
def test_flatten_nested_do(self):
ast = {"type": "do", "steps": [{"type": "do", "steps": ["A", "B"]}, "C"]}
normalized = _normalize(ast)
assert normalized == {"type": "do", "steps": ["A", "B", "C"]}
def test_flatten_nested_all(self):
ast = {"type": "all", "steps": [{"type": "all", "steps": ["A", "B"]}, "C"]}
normalized = _normalize(ast)
assert normalized == {"type": "all", "steps": ["A", "B", "C"]}
def test_singleton_do(self):
ast = {"type": "do", "steps": ["A"]}
normalized = _normalize(ast)
# Singleton should be kept as-is for consistency
assert "A" in str(normalized)
def test_empty_steps(self):
ast = {"type": "do", "steps": []}
normalized = _normalize(ast)
assert normalized == {"type": "do", "steps": []}
def test_deeply_nested(self):
ast = {
"type": "do",
"steps": [
{"type": "do", "steps": [{"type": "do", "steps": ["A"]}]},
"B",
],
}
normalized = _normalize(ast)
assert "A" in str(normalized)
assert "B" in str(normalized)
class TestSexpConversion:
"""Tests for S-expression conversion."""
def test_to_sexp_simple(self):
ast = {"type": "do", "steps": ["A", "B", "C"]}
sexp = to_sexp(ast)
assert sexp == ["do", "A", "B", "C"]
def test_to_sexp_nested(self):
ast = {
"type": "do",
"steps": ["A", {"type": "all", "steps": ["B", "C"]}, "D"],
}
sexp = to_sexp(ast)
assert sexp == ["do", "A", ["all", "B", "C"], "D"]
def test_from_sexp_simple(self):
sexp = ["do", "A", "B", "C"]
ast = from_sexp(sexp)
assert ast == {"type": "do", "steps": ["A", "B", "C"]}
def test_from_sexp_nested(self):
sexp = ["do", "A", ["all", "B", "C"], "D"]
ast = from_sexp(sexp)
assert ast == {
"type": "do",
"steps": ["A", {"type": "all", "steps": ["B", "C"]}, "D"],
}
def test_roundtrip(self):
original = {
"type": "do",
"steps": ["A", {"type": "all", "steps": ["B", "C"]}, "D"],
}
sexp = to_sexp(original)
recovered = from_sexp(sexp)
assert recovered == original
class TestToCommands:
"""Tests for conversion to executor commands format."""
def test_sequential(self):
ast = {"type": "do", "steps": ["A", "B", "C"]}
cmds = to_commands(ast)
assert cmds == ["A", "B", "C"]
def test_parallel(self):
ast = {"type": "all", "steps": ["A", "B", "C"]}
cmds = to_commands(ast)
assert cmds == [["A", "B", "C"]]
def test_mixed(self):
ast = {
"type": "do",
"steps": ["A", {"type": "all", "steps": ["B", "C"]}, "D"],
}
cmds = to_commands(ast)
assert cmds == ["A", ["B", "C"], "D"]
def test_complex_dag(self):
ast = {
"type": "do",
"steps": [
"setup",
{"type": "all", "steps": ["task1", "task2", "task3"]},
{"type": "all", "steps": ["cleanup1", "cleanup2"]},
"done",
],
}
cmds = to_commands(ast)
assert cmds == [
"setup",
["task1", "task2", "task3"],
["cleanup1", "cleanup2"],
"done",
]
class TestCompile:
"""Tests for compile convenience function."""
def test_compile_to_ast(self):
result = compile("A ; B ; C", format="ast")
assert result == {"type": "do", "steps": ["A", "B", "C"]}
def test_compile_to_sexp(self):
result = compile("A ; B ; C", format="sexp")
assert result == ["do", "A", "B", "C"]
def test_compile_to_commands(self):
result = compile("A ; { B & C } ; D", format="commands")
assert result == ["A", ["B", "C"], "D"]
def test_compile_to_ascii(self):
result = compile("A ; B", format="ascii")
assert "[do]" in result
assert "A" in result
assert "B" in result
def test_compile_invalid_format(self):
with pytest.raises(ValueError):
compile("A", format="invalid")
class TestRenderAscii:
"""Tests for ASCII rendering."""
def test_simple(self):
ast = {"type": "do", "steps": ["A", "B"]}
output = render_ascii(ast)
assert "[do]" in output
assert "A" in output
assert "B" in output
def test_nested(self):
ast = {
"type": "do",
"steps": ["A", {"type": "all", "steps": ["B", "C"]}, "D"],
}
output = render_ascii(ast)
assert "[do]" in output
assert "[all]" in output
class TestEdgeCases:
"""Edge case and error handling tests."""
def test_many_sequential(self):
src = " ; ".join([f"cmd{i}" for i in range(100)])
ast = parse(src)
assert len(ast["steps"]) == 100
def test_many_parallel(self):
src = "{ " + " & ".join([f"cmd{i}" for i in range(100)]) + " }"
ast = parse(src)
assert len(ast["steps"]) == 100
def test_deeply_nested_parallel(self):
src = "A ; { B & { C & D } } ; E"
ast = parse(src)
# Should handle nested parallel blocks
assert ast["type"] == "do"
def test_unicode_commands(self):
ast = parse("echo 你好 ; echo мир ; echo 🚀")
assert len(ast["steps"]) == 3
def test_long_commands(self):
long_cmd = "echo " + "x" * 10000
ast = parse(long_cmd)
assert long_cmd in ast["steps"][0] or ast["steps"][0] == long_cmd
class TestPerformance:
"""Performance benchmarks."""
def test_parse_speed_simple(self):
"""Should parse simple commands very fast."""
src = "ls -la"
start = time.perf_counter()
for _ in range(10000):
parse(src)
elapsed = time.perf_counter() - start
ops_per_sec = 10000 / elapsed
print(f"\nSimple parse: {ops_per_sec:.0f} ops/sec")
assert ops_per_sec > 10000 # At least 10k ops/sec
def test_parse_speed_mixed(self):
"""Should parse mixed DAGs reasonably fast."""
src = "mkdir -p dist ; { cp a dist/ & cp b dist/ & cp c dist/ } ; zip out.zip dist/"
start = time.perf_counter()
for _ in range(1000):
parse(src)
elapsed = time.perf_counter() - start
ops_per_sec = 1000 / elapsed
print(f"\nMixed DAG parse: {ops_per_sec:.0f} ops/sec")
assert ops_per_sec > 1000 # At least 1k ops/sec
def test_compile_speed(self):
"""Should compile to commands fast."""
src = "A ; { B & C & D } ; E"
start = time.perf_counter()
for _ in range(10000):
compile(src, format="commands")
elapsed = time.perf_counter() - start
ops_per_sec = 10000 / elapsed
print(f"\nFull compile: {ops_per_sec:.0f} ops/sec")
assert ops_per_sec > 5000 # At least 5k ops/sec
def test_large_dag(self):
"""Should handle large DAGs efficiently."""
# 100 sequential with 10 parallel each
parts = []
for i in range(100):
parallel = "{ " + " & ".join([f"task{i}_{j}" for j in range(10)]) + " }"
parts.append(parallel)
src = " ; ".join(parts)
start = time.perf_counter()
ast = parse(src)
cmds = to_commands(ast)
elapsed = time.perf_counter() - start
print(f"\nLarge DAG (100x10): {elapsed * 1000:.2f}ms")
assert elapsed < 0.1 # Should complete in under 100ms
assert len(cmds) == 100
@pytest.mark.asyncio
class TestIntegration:
"""Integration tests with actual shell execution."""
async def test_execute_sequential(self):
from hanzo_tools.shell import ZshTool
zsh = ZshTool()
result = await zsh.call(None, command="echo A ; echo B ; echo C")
assert "A" in result
assert "B" in result
assert "C" in result
async def test_execute_parallel(self):
from hanzo_tools.shell import ZshTool
zsh = ZshTool()
result = await zsh.call(None, command="{ echo A & echo B & echo C }")
assert "A" in result
assert "B" in result
assert "C" in result
async def test_execute_mixed(self):
from hanzo_tools.shell import ZshTool
zsh = ZshTool()
result = await zsh.call(
None, command="echo start ; { echo A & echo B } ; echo end"
)
assert "start" in result
assert "A" in result
assert "B" in result
assert "end" in result
async def test_execute_with_bash(self):
from hanzo_tools.shell import BashTool
bash = BashTool()
# Use proper bash syntax (no zsh-style { } backgrounding)
result = await bash.call(
None, command="echo start && echo A && echo B && echo end"
)
assert "start" in result
assert "end" in result
async def test_parallel_execution_time(self):
"""Parallel should be faster than sequential for sleep commands."""
from hanzo_tools.shell import ZshTool
zsh = ZshTool()
# Parallel: should take ~0.1s
start = time.perf_counter()
await zsh.call(None, command="{ sleep 0.1 & sleep 0.1 & sleep 0.1 }")
parallel_time = time.perf_counter() - start
# Sequential: should take ~0.3s
start = time.perf_counter()
await zsh.call(None, command="sleep 0.1 ; sleep 0.1 ; sleep 0.1")
sequential_time = time.perf_counter() - start
print(f"\nParallel: {parallel_time:.2f}s, Sequential: {sequential_time:.2f}s")
# Parallel should be at least 2x faster
assert parallel_time < sequential_time * 0.7
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])