bcbd1bdb22
Test (Python) / test-python-gate (push) Blocked by required conditions
Test (TypeScript) / test-typescript-gate (push) Blocked by required conditions
CLI exit codes / cli-gate (push) Blocked by required conditions
Test (Install) / test-install-gate (push) Blocked by required conditions
Integ / integ-gate (push) Blocked by required conditions
CLI exit codes / Python CLI (push) Waiting to run
Integ / changes (push) Has been skipped
Test (Install) / python-minimal (3.11) (push) Waiting to run
Test (Install) / python-minimal (3.12) (push) Waiting to run
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Waiting to run
Integ / integ-ts (push) Waiting to run
Integ / integ (push) Waiting to run
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Waiting to run
Integ / integ-database (push) Waiting to run
Test (Install) / python-extra (email, mirage.resource.email) (push) Waiting to run
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Waiting to run
Integ / integ-ssh (push) Waiting to run
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
CLI exit codes / TypeScript CLI (push) Waiting to run
CLI exit codes / Cross-language snapshot interop (push) Waiting to run
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Waiting to run
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Waiting to run
Test (Python) / changes (push) Has been skipped
Integ / integ-database-ts (push) Waiting to run
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Waiting to run
Integ / integ-data (push) Waiting to run
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Waiting to run
Test (Python) / test (push) Waiting to run
Integ / integ-fuse (push) Waiting to run
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Waiting to run
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Waiting to run
Test (Python) / audit (push) Waiting to run
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Waiting to run
Integ / integ-ssh-ts (push) Waiting to run
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Waiting to run
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Waiting to run
Test (TypeScript) / changes (push) Has been skipped
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Waiting to run
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Waiting to run
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Waiting to run
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Waiting to run
Test (TypeScript) / test (push) Waiting to run
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Waiting to run
Test (TypeScript) / python-fs-shim (push) Waiting to run
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Waiting to run
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Waiting to run
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Waiting to run
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Waiting to run
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Waiting to run
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Waiting to run
Test (Install) / ts-minimal (push) Waiting to run
444 lines
14 KiB
Python
444 lines
14 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
import pytest
|
|
|
|
from .conftest import make_resource_ws, run, run_exit
|
|
|
|
FILES = {
|
|
"logs/app.log": (b"2026-01-01 INFO startup\n"
|
|
b"2026-01-02 ERROR connection refused\n"
|
|
b"2026-01-03 INFO request handled\n"
|
|
b"2026-01-04 WARN slow query\n"
|
|
b"2026-01-05 ERROR timeout\n"
|
|
b"2026-01-06 INFO shutdown\n"),
|
|
"logs/access.log": (b"GET /api/users 200\n"
|
|
b"POST /api/users 201\n"
|
|
b"GET /api/users 200\n"
|
|
b"DELETE /api/users/1 404\n"
|
|
b"GET /api/health 200\n"),
|
|
"data/scores.csv":
|
|
b"alice,90\nbob,75\ncharlie,90\nalice,85\nbob,95\n",
|
|
"data/words.txt":
|
|
b"hello\nworld\nhello\nfoo\nbar\nfoo\nhello\n",
|
|
"data/numbers.txt":
|
|
b"3\n1\n4\n1\n5\n9\n2\n6\n5\n3\n",
|
|
"src/main.py":
|
|
b"import os\nimport sys\nprint('hello')\n",
|
|
"src/utils.py":
|
|
b"def add(a, b):\n return a + b\n",
|
|
"config.json":
|
|
b'{"name": "mirage", "version": "1.0"}\n',
|
|
"empty.txt":
|
|
b"",
|
|
}
|
|
|
|
|
|
@pytest.fixture(params=["ram", "s3", "disk"])
|
|
def ws(request, tmp_path):
|
|
yield from make_resource_ws(request, tmp_path, FILES)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Nested loops
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_nested_for_with_file_ops(ws):
|
|
cmd = ("for d in logs data; do "
|
|
"for f in $(find /data/$d -type f | sort); do "
|
|
"echo \"$d: $f\"; done; done")
|
|
result = run(ws, cmd)
|
|
assert "logs: /data/logs/app.log" in result
|
|
assert "data: /data/data/scores.csv" in result
|
|
|
|
|
|
def test_for_with_if_and_grep(ws):
|
|
cmd = ("for f in $(find /data/logs -type f | sort); do "
|
|
"if grep -q ERROR $f; then echo \"ERRORS: $f\"; fi; done")
|
|
result = run(ws, cmd)
|
|
assert "ERRORS: /data/logs/app.log" in result
|
|
assert "ERRORS: /data/logs/access.log" not in result
|
|
|
|
|
|
def test_while_read_with_nested_if(ws):
|
|
cmd = ("find /data/data -type f | sort | "
|
|
"while read f; do "
|
|
"if [ \"$(wc -l < $f)\" -gt 3 ]; then "
|
|
"echo \"big: $f\"; "
|
|
"else echo \"small: $f\"; fi; done")
|
|
result = run(ws, cmd)
|
|
lines = result.strip().splitlines()
|
|
assert len(lines) == 3
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Multi-stage pipelines
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_sort_uniq_count_pipeline(ws):
|
|
cmd = "cat /data/data/words.txt | sort | uniq -c | sort -rn | head -n 1"
|
|
result = run(ws, cmd).split()
|
|
assert result[0] == "3"
|
|
assert result[1] == "hello"
|
|
|
|
|
|
def test_grep_pipe_cut_pipe_sort_pipe_uniq(ws):
|
|
cmd = ("cat /data/logs/access.log | cut -d ' ' -f 1 | sort | uniq -c "
|
|
"| sort -rn")
|
|
result = run(ws, cmd).strip()
|
|
lines = result.splitlines()
|
|
assert len(lines) == 3
|
|
first = lines[0].split()
|
|
assert first[0] == "3"
|
|
assert first[1] == "GET"
|
|
|
|
|
|
def test_five_stage_pipeline(ws):
|
|
cmd = ("cat /data/data/numbers.txt | sort -n | uniq | head -n 3 "
|
|
"| tr '\\n' ','")
|
|
result = run(ws, cmd)
|
|
assert result.startswith("1,2,3")
|
|
|
|
|
|
def test_grep_count_errors_across_files(ws):
|
|
cmd = "grep -c ERROR /data/logs/app.log"
|
|
result = run(ws, cmd).strip()
|
|
assert result == "2"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Command substitution in pipelines
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_command_sub_in_echo(ws):
|
|
cmd = "echo \"lines: $(wc -l < /data/data/words.txt)\""
|
|
result = run(ws, cmd).strip()
|
|
assert result == "lines: 7"
|
|
|
|
|
|
def test_nested_command_sub(ws):
|
|
cmd = "echo $(echo $(echo deep))"
|
|
result = run(ws, cmd).strip()
|
|
assert result == "deep"
|
|
|
|
|
|
def test_command_sub_in_for_values(ws):
|
|
cmd = ("for line in $(grep ERROR /data/logs/app.log | cut -d ' ' -f 1); "
|
|
"do echo \"date:$line\"; done")
|
|
result = run(ws, cmd)
|
|
assert "date:2026-01-02" in result
|
|
assert "date:2026-01-05" in result
|
|
|
|
|
|
def test_command_sub_in_test(ws):
|
|
cmd = ("if [ $(grep -c ERROR /data/logs/app.log) -gt 1 ]; then "
|
|
"echo many_errors; else echo few_errors; fi")
|
|
result = run(ws, cmd).strip()
|
|
assert result == "many_errors"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# While read with processing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_while_read_with_variable_transform(ws):
|
|
cmd = ("echo -e 'alice\\nbob\\ncharlie' | "
|
|
"while read name; do echo \"user:$name\"; done")
|
|
result = run(ws, cmd)
|
|
lines = result.strip().splitlines()
|
|
assert lines == ["user:alice", "user:bob", "user:charlie"]
|
|
|
|
|
|
def test_while_read_counter(ws):
|
|
cmd = ("export count=0; "
|
|
"cat /data/data/words.txt | sort -u | "
|
|
"while read w; do "
|
|
"export count=$((count+1)); echo \"$count:$w\"; done")
|
|
result = run(ws, cmd)
|
|
lines = result.strip().splitlines()
|
|
assert lines[0] == "1:bar"
|
|
|
|
|
|
def test_while_read_pipe_to_wc(ws):
|
|
cmd = ("grep ERROR /data/logs/app.log | "
|
|
"while read line; do echo \"ALERT: $line\"; done | wc -l")
|
|
result = run(ws, cmd).strip()
|
|
assert result == "2"
|
|
|
|
|
|
def test_while_read_with_grep_filter(ws):
|
|
cmd = ("cat /data/logs/access.log | "
|
|
"while read line; do echo $line; done | grep 200 | wc -l")
|
|
result = run(ws, cmd).strip()
|
|
assert result == "3"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Subshell and grouping
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_subshell_pipe_chain(ws):
|
|
cmd = "(echo hello; echo world) | sort | tr a-z A-Z"
|
|
result = run(ws, cmd)
|
|
lines = result.strip().splitlines()
|
|
assert lines == ["HELLO", "WORLD"]
|
|
|
|
|
|
def test_brace_group_redirect(ws):
|
|
cmd = "{ echo first; echo second; } > /data/out.txt; cat /data/out.txt"
|
|
result = run(ws, cmd)
|
|
assert result == "first\nsecond\n"
|
|
|
|
|
|
def test_subshell_variable_isolation(ws):
|
|
cmd = "export X=outer; (export X=inner; echo $X); echo $X"
|
|
result = run(ws, cmd)
|
|
lines = result.strip().splitlines()
|
|
assert lines == ["inner", "outer"]
|
|
|
|
|
|
def test_subshell_function_def_no_leak(ws):
|
|
cmd = "(only_inner() { echo inner; }); only_inner 2>/dev/null || echo gone"
|
|
assert run(ws, cmd).strip() == "gone"
|
|
|
|
|
|
def test_subshell_function_redef_isolation(ws):
|
|
cmd = "greet() { echo outer; }; (greet() { echo inner; }; greet); greet"
|
|
lines = run(ws, cmd).strip().splitlines()
|
|
assert lines == ["inner", "outer"]
|
|
|
|
|
|
def test_subshell_function_inherited(ws):
|
|
cmd = "greet() { echo hi; }; (greet)"
|
|
assert run(ws, cmd).strip() == "hi"
|
|
|
|
|
|
def test_subshell_positional_isolation(ws):
|
|
cmd = "set -- a b c; (set -- x); echo $# $1"
|
|
assert run(ws, cmd).strip() == "3 a"
|
|
|
|
|
|
def test_subshell_positional_inherited(ws):
|
|
cmd = "set -- a b; (echo $1 $2)"
|
|
assert run(ws, cmd).strip() == "a b"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Functions with pipelines
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_function_in_pipeline(ws):
|
|
cmd = "upper() { tr a-z A-Z; }; echo hello | upper"
|
|
result = run(ws, cmd).strip()
|
|
assert result == "HELLO"
|
|
|
|
|
|
def test_function_with_args_in_loop(ws):
|
|
cmd = ("classify() { case $1 in "
|
|
"*.py) echo \"python: $1\";; "
|
|
"*.txt) echo \"text: $1\";; "
|
|
"*.json) echo \"json: $1\";; "
|
|
"*.csv) echo \"csv: $1\";; "
|
|
"*.log) echo \"log: $1\";; "
|
|
"*) echo \"other: $1\";; esac; }; "
|
|
"find /data -type f | sort | "
|
|
"while read f; do classify $f; done")
|
|
result = run(ws, cmd)
|
|
assert "python: /data/src/main.py" in result
|
|
assert "json: /data/config.json" in result
|
|
assert "log: /data/logs/app.log" in result
|
|
assert "csv: /data/data/scores.csv" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Conditional chains (&&, ||)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_and_chain_with_grep(ws):
|
|
cmd = ("grep -q ERROR /data/logs/app.log && "
|
|
"echo 'has errors' || echo 'clean'")
|
|
result = run(ws, cmd).strip()
|
|
assert result == "has errors"
|
|
|
|
|
|
def test_or_chain_fallback(ws):
|
|
cmd = ("grep -q FATAL /data/logs/app.log && "
|
|
"echo 'has fatal' || echo 'no fatal'")
|
|
result = run(ws, cmd).strip()
|
|
assert result == "no fatal"
|
|
|
|
|
|
def test_chained_conditionals(ws):
|
|
cmd = ("grep -q ERROR /data/logs/app.log && "
|
|
"grep -q INFO /data/logs/app.log && "
|
|
"echo 'has both'")
|
|
result = run(ws, cmd).strip()
|
|
assert result == "has both"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Redirects combined with pipes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_pipe_with_output_redirect(ws):
|
|
cmd = ("grep ERROR /data/logs/app.log | sort > /data/errors.txt; "
|
|
"cat /data/errors.txt")
|
|
result = run(ws, cmd)
|
|
lines = result.strip().splitlines()
|
|
assert len(lines) == 2
|
|
assert lines == sorted(lines)
|
|
|
|
|
|
def test_append_redirect_in_loop(ws):
|
|
cmd = ("for x in one two three; do "
|
|
"echo $x >> /data/result.txt; done; cat /data/result.txt")
|
|
result = run(ws, cmd)
|
|
assert result == "one\ntwo\nthree\n"
|
|
|
|
|
|
def test_stdin_redirect_in_pipeline(ws):
|
|
cmd = "sort < /data/data/numbers.txt | uniq | wc -l"
|
|
result = run(ws, cmd).strip()
|
|
assert result == "7"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Process substitution
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_input_process_substitution(ws):
|
|
cmd = "cat <(echo hello)"
|
|
result = run(ws, cmd)
|
|
assert "hello" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sed and tr pipelines
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_sed_in_pipeline(ws):
|
|
cmd = "cat /data/logs/app.log | grep ERROR | sed 's/ERROR/CRITICAL/'"
|
|
result = run(ws, cmd)
|
|
assert "CRITICAL" in result
|
|
assert "ERROR" not in result
|
|
|
|
|
|
def test_tr_multiple_transforms(ws):
|
|
cmd = "echo 'Hello World' | tr A-Z a-z | tr ' ' '_'"
|
|
result = run(ws, cmd).strip()
|
|
assert result == "hello_world"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Complex real-world patterns
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_log_analysis_pipeline(ws):
|
|
cmd = ("cat /data/logs/app.log | grep -v INFO | "
|
|
"cut -d ' ' -f 2 | sort | uniq -c | sort -rn")
|
|
result = run(ws, cmd).strip()
|
|
lines = result.splitlines()
|
|
first = lines[0].split()
|
|
assert first[0] == "2"
|
|
assert first[1] == "ERROR"
|
|
|
|
|
|
def test_find_grep_count_pattern(ws):
|
|
cmd = ("find /data/src -name '*.py' -type f | sort | "
|
|
"while read f; do "
|
|
"echo \"$(grep -c import $f) $f\"; done")
|
|
result = run(ws, cmd)
|
|
assert "2 /data/src/main.py" in result
|
|
|
|
|
|
def test_csv_processing_pipeline(ws):
|
|
cmd = ("cat /data/data/scores.csv | sort -t, -k2 -rn | head -n 1 "
|
|
"| cut -d, -f1")
|
|
result = run(ws, cmd).strip()
|
|
assert result == "bob"
|
|
|
|
|
|
def test_word_frequency_full_pipeline(ws):
|
|
cmd = ("cat /data/data/words.txt | sort | uniq -c | sort -rn | "
|
|
"while read count word; do "
|
|
"echo \"$word appears $count times\"; done | head -n 2")
|
|
result = run(ws, cmd)
|
|
lines = result.strip().splitlines()
|
|
assert "hello appears 3 times" in lines[0]
|
|
assert "foo appears 2 times" in lines[1]
|
|
|
|
|
|
def test_while_read_with_command_sub_body(ws):
|
|
cmd = ("find /data -name '*.py' -type f | sort | "
|
|
"while read f; do "
|
|
"echo \"$f: $(wc -l < $f) lines\"; done")
|
|
result = run(ws, cmd)
|
|
assert "/data/src/main.py: 3 lines" in result
|
|
assert "/data/src/utils.py: 2 lines" in result
|
|
|
|
|
|
def test_multiline_script_with_function_and_loop(ws):
|
|
cmd = ("count_matches() { grep -c $1 $2; }; "
|
|
"find /data/logs -type f | sort | "
|
|
"while read f; do "
|
|
"echo \"$f: $(count_matches ERROR $f) errors\"; done")
|
|
result = run(ws, cmd)
|
|
assert "/data/logs/app.log: 2 errors" in result
|
|
assert "/data/logs/access.log: 0 errors" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_empty_file_in_pipeline(ws):
|
|
cmd = "cat /data/empty.txt | wc -l"
|
|
result = run(ws, cmd).strip()
|
|
assert result == "0"
|
|
|
|
|
|
def test_while_read_empty_input(ws):
|
|
cmd = ("cat /data/empty.txt | "
|
|
"while read line; do echo \"got: $line\"; done; echo done")
|
|
result = run(ws, cmd).strip()
|
|
assert result == "done"
|
|
|
|
|
|
def test_deeply_nested_command_sub(ws):
|
|
cmd = "echo $(echo $(echo $(echo nested)))"
|
|
result = run(ws, cmd).strip()
|
|
assert result == "nested"
|
|
|
|
|
|
def test_pipe_exit_code_last_command(ws):
|
|
code = run_exit(ws, "echo hello | grep world")
|
|
assert code != 0
|
|
|
|
|
|
def test_pipe_exit_code_success(ws):
|
|
code = run_exit(ws, "echo hello | grep hello")
|
|
assert code == 0
|