"""Tests involving running Xonsh in subproc.
This requires Xonsh installed in venv or otherwise available on PATH
"""

import os
import re
import subprocess as sp
import sys
import tempfile
from pathlib import Path

import pytest

import xonsh
from tests.xintegration.conftest import (
    check_run_xonsh,
    run_xonsh,
    skip_if_no_make,
    skip_if_no_sleep,
)
from xonsh.dirstack import with_pushd
from xonsh.pytest.tools import (
    ON_DARWIN,
    ON_TRAVIS,
    ON_WINDOWS,
    VER_FULL,
    skip_if_on_bsd,
    skip_if_on_darwin,
    skip_if_on_msys,
    skip_if_on_unix,
    skip_if_on_windows,
)

#
# The following list contains a (stdin, stdout, returncode) tuples
#

tests_path = str(Path(__file__).absolute().parent.parent)

ALL_PLATFORMS = [
    # conch in action
    (
        """
print(isinstance(@, type(__xonsh__.interface)))
""",
        "True\n",
        0,
    ),
    (
        """
$CONCH=24
with @.env.swap(CONCH=42):
    print(@.imp.json.loads('{"@":"~"}'), $CONCH)
""",
        "{'@': '~'} 42\n",
        0,
    ),
    (
        """
@aliases.register
@@.imp.xonsh.tools.unthreadable
def _mycmd(args, stdin=None):
    return 'ok'
mycmd
""",
        "ok",
        0,
    ),
    # test calling a function alias
    (
        """
def _f():
    print('hello')

aliases['f'] = _f
f
""",
        "hello\n",
        0,
    ),
    # test redirecting a function alias to a file
    (
        """
def _f():
    print('Wow Mom!')

aliases['f'] = _f

import tempfile
temp_path = tempfile.mktemp()

f > @(temp_path)

with open(temp_path) as tttt:
    s = tttt.read().strip()
print('REDIRECTED OUTPUT: ' + s)
""",
        "REDIRECTED OUTPUT: Wow Mom!\n",
        0,
    ),
    # test redirecting a function alias from stderr -> stdout
    (
        """
def _f(args, stdin, stdout, stderr):
    print('The Truth is Out There', file=stderr)

aliases['f'] = _f
f e>o
""",
        "The Truth is Out There\n",
        0,
    ),
    # test redirecting to a python substitution
    (
        """
def _f():
    print('Wow Mom!')
aliases['f'] = _f

temp_path = @.imp.tempfile.mktemp()

f > @(temp_path)
with open(temp_path) as tttt:
    s = tttt.read().strip()
print('REDIRECTED OUTPUT: ' + s)
""",
        "REDIRECTED OUTPUT: Wow Mom!\n",
        0,
    ),
    # test redirecting to a python substitution with p-string
    (
        """
def _f():
    print('Wow Mom!')
aliases['f'] = _f
temp_path = @.imp.tempfile.mktemp()
f > @(@.imp.pathlib.Path(temp_path))
with open(temp_path) as tttt:
    s = tttt.read().strip()
print('REDIRECTED OUTPUT: ' + s)
""",
        "REDIRECTED OUTPUT: Wow Mom!\n",
        0,
    ),
    # test system exit in function alias
    (
        """
import sys
def _f():
    sys.exit(42)

aliases['f'] = _f
print(![f].returncode)
""",
        "42\n",
        0,
    ),
    # test uncaptured streaming alias,
    # order actually printed in is non-deterministic
    (
        """
def _test_stream(args, stdin, stdout, stderr):
    print('hallo on stream', file=stderr)
    print('hallo on stream', file=stdout)
    return 1

aliases['test-stream'] = _test_stream
x = ![test-stream]
print(x.returncode)
""",
        "hallo on stream\nhallo on stream\n1\n",
        0,
    ),
    # test captured streaming alias
    (
        """
def _test_stream(args, stdin, stdout, stderr):
    print('hallo on err', file=stderr)
    print('hallo on out', file=stdout)
    return 1

aliases['test-stream'] = _test_stream
x = !(test-stream)
print(x.returncode)
""",
        "1\n",
        0,
    ),
    # test captured streaming alias without stderr
    (
        """
def _test_stream(args, stdin, stdout, stderr):
    print('hallo on err', file=stderr)
    print('hallo on out', file=stdout)
    return 1

aliases['test-stream'] = _test_stream
with __xonsh__.env.swap(XONSH_SUBPROC_CAPTURED_PRINT_STDERR=True):
    x = !(test-stream)
    print(x.returncode)
""",
        "hallo on err\n1\n",
        0,
    ),
    # test piping aliases
    (
        """
def dummy(args, inn, out, err):
    out.write('hey!')
    return 0

def dummy2(args, inn, out, err):
    s = inn.read()
    out.write(s.upper())
    return 0

aliases['d'] = dummy
aliases['d2'] = dummy2
d | d2
""",
        "HEY!",
        0,
    ),
    # test output larger than most pipe buffers
    (
        """
def _g(args, stdin=None):
    for i in range(1000):
        print('x' * 100)

aliases['g'] = _g
g
""",
        (("x" * 100) + "\n") * 1000,
        0,
    ),
    # test piping 'real' command
    (
        f"""
import tempfile
temp_path = tempfile.mktemp()

with open(temp_path, 'w') as fp:
    fp.write("Wow mom!\\n")

pathcat = str(p{tests_path!r}.absolute() / 'bin' / 'cat')
pathwc = str(p{tests_path!r}.absolute() / 'bin' / 'wc')

![{sys.executable} @(pathcat) @(temp_path) | {sys.executable} @(pathwc)]
""",
        " 1  2 10 <stdin>\n" if ON_WINDOWS else " 1  2 9 <stdin>\n",
        0,
    ),
    # test double  piping 'real' command
    (
        f"""
import tempfile
temp_path = tempfile.mktemp()

with open(temp_path, 'w') as fp:
    fp.write("Wow mom!\\n")

pathcat = str(p{tests_path!r}.absolute() / 'bin' / 'cat')
pathwc = str(p{tests_path!r}.absolute() / 'bin' / 'wc')

![{sys.executable} @(pathcat) @(temp_path) | {sys.executable} @(pathwc) | {sys.executable} @(pathwc)]
""",
        " 1  4 18 <stdin>\n" if ON_WINDOWS else " 1  4 16 <stdin>\n",
        0,
    ),
    # test unthreadable alias (which should trigger a ProcPoxy call)
    (
        """
from xonsh.tools import unthreadable

@unthreadable
def _f():
    return 'hello\\n'

aliases['f'] = _f
f
""",
        "hello\n",
        0,
    ),
    # test system exit in unthreadable alias (see #5689)
    (
        """
from xonsh.tools import unthreadable

@unthreadable
def _f():
    import sys
    sys.exit(42)

aliases['f'] = _f
print(![f].returncode)
""",
        "42\n",
        0,
    ),
    # test ambiguous globs
    (
        """
import os

def _echo(args):
    print(' '.join(args))
aliases['echo'] = _echo

files = ['Actually_test.tst', 'Actually.tst', 'Complete_test.tst', 'Complete.tst']

# touch the file
for f in files:
    with open(f, 'w'):
        pass

# echo the files
echo *.tst and echo *_test.tst
echo *_test.tst
echo *_test.tst and echo *.tst

# remove the files
for f in files:
    os.remove(f)
""",
        "Actually.tst Actually_test.tst Complete.tst Complete_test.tst\n"
        "Actually_test.tst Complete_test.tst\n"
        "Actually_test.tst Complete_test.tst\n"
        "Actually_test.tst Complete_test.tst\n"
        "Actually.tst Actually_test.tst Complete.tst Complete_test.tst\n",
        0,
    ),
    #
    # test ambiguous line continuations
    #
    (
        """
def _echo(args):
    print(' '.join(args))
aliases['echo'] = _echo

echo --option1 \\
--option2
echo missing \\
EOL""",
        "--option1 --option2\nmissing EOL\n",
        0,
    ),
    #
    # test @$() with aliases
    #
    (
        """
aliases['ls'] = 'spam spam sausage spam'

echo @$(which ls)
""",
        "spam spam sausage spam\n",
        0,
    ),
    (
        """
$THREAD_SUBPROCS = False
aliases['ls'] = 'spam spam sausage spam'

echo @$(which ls)
""",
        "spam spam sausage spam\n",
        0,
    ),
    (
        """
$XONSH_SUBPROC_OUTPUT_FORMAT = 'list_lines'
aliases['ls'] = 'spam spam sausage spam'

echo @$(which ls)
""",
        "spam spam sausage spam\n",
        0,
    ),
    #
    # test @$() without leading/trailig WS
    #
    (
        """
def _echo(args):
    print(' '.join(args))
aliases['echo'] = _echo

echo foo_@$(echo spam)_bar
""",
        "foo_spam_bar\n",
        0,
    ),
    #
    # test @$() outer product
    #
    (
        """
def _echo(args):
    print(' '.join(args))
aliases['echo'] = _echo

echo foo_@$(echo spam sausage)_bar
""",
        "foo_spam_bar foo_sausage_bar\n",
        0,
    ),
    #
    # test redirection
    #
    (
        f"""
import tempfile
temp_path = tempfile.mktemp()

pathcat = str(p{tests_path!r}.absolute() / 'bin' / 'cat')
echo Just the place for a snark. >@(temp_path)
{sys.executable} @(pathcat) @(temp_path)
""",
        "Just the place for a snark.\n",
        0,
    ),
    #
    # Test completion registration and subproc stack
    #
    (
        """
def _f():
    def j():
        pass

    global aliases
    aliases['j'] = j

    def completions(pref, *args):
        return set(['hello', 'world'])

    completer add j completions "start"


_f()
del _f

""",
        "",
        0,
    ),
    #
    # test single check_output
    #
    (
        """
def _echo(args):
    print(' '.join(args))
aliases['echo'] = _echo

from xonsh.api.subprocess import check_output

print(check_output(["echo", "hello"]).decode("utf8"))
""",
        "hello\n",
        0,
    ),
    #
    # test contextvars
    #
    (
        """
import sys

if sys.version_info[:2] >= (3, 7):
    with open("sourced-file.xsh", "w") as f:
        f.write('''
from contextvars import ContextVar

var = ContextVar('var', default='spam')
var.set('foo')
        ''')

    source sourced-file.xsh

    print("Var " + var.get())

    import os
    os.remove('sourced-file.xsh')
else:
    print("Var foo")
""",
        "Var foo\n",
        0,
    ),
    #
    # test env with class
    #
    (
        """
class Cls:
    def __init__(self, var):
        self.var = var
    def __repr__(self):
        return self.var

$VAR = Cls("hello")
print($VAR)
echo $VAR
""",
        "hello\nhello\n",
        0,
    ),
    #
    # test logical subprocess operators
    #
    (
        """
def _echo(args):
    print(' '.join(args))
aliases['echo'] = _echo

echo --version and echo a
echo --version && echo a
echo --version or echo a
echo --version || echo a
echo -+version and echo a
echo -+version && echo a
echo -+version or echo a
echo -+version || echo a
echo -~version and echo a
echo -~version && echo a
echo -~version or echo a
echo -~version || echo a
""",
        """--version
a
--version
a
--version
--version
-+version
a
-+version
a
-+version
-+version
-~version
a
-~version
a
-~version
-~version
""",
        0,
    ),
]

UNIX_TESTS = [
    # testing alias stack: lambda function
    (
        """
def _echo():
    echo hello

aliases['echo'] = _echo
echo
""",
        "hello\n",
        0,
    ),
    # testing alias stack: ExecAlias
    (
        """
aliases['echo'] = "echo @('hello')"
echo
""",
        "hello\n",
        0,
    ),
    # testing alias stack: callable alias (ExecAlias) + no binary location + infinite loop
    (
        """
aliases['first'] = "second @(1)"
aliases['second'] = "first @(1)"
first
""",
        lambda out: 'Recursive calls to "first" alias.' in out,
        0,
    ),
    # testing alias stack: parallel threaded callable aliases.
    # This breaks if the __ALIAS_STACK variables leak between threads.
    pytest.param(
        (
            """
from time import sleep
aliases['a'] = lambda: print(1, end="") or sleep(0.2) or print(1, end="")
aliases['b'] = 'a'
a | a
a | a
a | b | a
a | a | b | b
""",
            "1" * 2 * 4,
            0,
        ),
        # TODO: investigate errors on Python 3.13
        marks=pytest.mark.skipif(
            VER_FULL > (3, 12) and not ON_WINDOWS,
            reason="broken pipes on Python 3.13 likely due to changes in threading behavior",
        ),
    ),
    # test $SHLVL
    (
        f"""
# test parsing of $SHLVL

$SHLVL = "1"
echo $SHLVL # == 1

$SHLVL = 1
echo $SHLVL # == 1

$SHLVL = "-13"
echo $SHLVL # == 0

$SHLVL = "error"
echo $SHLVL # == 0

$SHLVL = 999
echo $SHLVL # == 999

$SHLVL = 1000
echo $SHLVL # == 1

# sourcing a script should maintain $SHLVL

$SHLVL = 5
touch temp_shlvl_test.sh
source-bash temp_shlvl_test.sh
rm temp_shlvl_test.sh
echo $SHLVL # == 5

# creating a subshell should increment the child's $SHLVL and maintain the parents $SHLVL

$SHLVL = 5
{sys.executable} -m xonsh --no-rc -c r'echo $SHLVL' # == 6
echo $SHLVL # == 5

# replacing the current process with another process should derease $SHLVL
# (so that if the new process is a shell, $SHLVL is maintained)

$SHLVL = 5
xexec {sys.executable} -c 'import os; print(os.environ["SHLVL"])' # == 4
""",
        # The script's ``source-bash temp_shlvl_test.sh`` step is incidental
        # to the $SHLVL test and emits an xonsh ``Source failed`` warning on
        # systems where ``source-bash`` actually invokes a real Bash (the
        # touched file is empty/syntax-invalid). On systems without Bash on
        # PATH (FreeBSD poudriere build jails, slim CI containers) the
        # warning never appears. Strip those incidental warning lines before
        # comparing so the test exercises only what its name says: SHLVL
        # parsing and inheritance.
        lambda out: (
            "\n".join(
                ln for ln in out.splitlines() if not ln.startswith("xonsh: error:")
            )
            + "\n"
            == "1\n1\n0\n0\n999\n1\n5\n6\n5\n4\n"
        ),
        0,
    ),
    # test $() inside piped callable alias
    (
        rf"""
def _callme(args):
    result = $({sys.executable} -c 'print("tree");print("car")')
    print(result[::-1])
    print('one\ntwo\nthree')

aliases['callme'] = _callme
callme | grep t
""",
        """eert
two
three
""",
        0,
    ),
    # test ![] inside piped callable alias
    (
        rf"""
def _callme(args):
    {sys.executable} -c 'print("tree");print("car")'
    print('one\ntwo\nthree')

aliases['callme'] = _callme
callme | grep t
""",
        """tree
two
three
""",
        0,
    ),
    # test $[] inside piped callable alias
    pytest.param(
        (
            r"""
def _callme(args):
    $[python -c 'print("tree");print("car")']
    print('one\ntwo\nthree')

aliases['callme'] = _callme
callme | grep t
""",
            """tree
two
three
""",
            0,
        ),
        marks=pytest.mark.xfail(reason="$[] does not send stdout through the pipe"),
    ),
    # test $XONSH_BUILTINS_TO_CMD: bare `id` (a Python builtin) runs as command
    (
        """
$XONSH_BUILTINS_TO_CMD = True
id
""",
        lambda out: "uid=" in out,
        0,
    ),
]

if not ON_WINDOWS:
    ALL_PLATFORMS = tuple(ALL_PLATFORMS) + tuple(UNIX_TESTS)


@pytest.mark.parametrize("case", ALL_PLATFORMS)
@pytest.mark.flaky(reruns=4, reruns_delay=2)
def test_script(case):
    script, exp_out, exp_rtn = case
    if ON_DARWIN:
        script = script.replace("tests/bin", str(Path(__file__).parent.parent / "bin"))
    out, err, rtn = run_xonsh(script)
    out = out.replace("bash: no job control in this shell\n", "")
    if callable(exp_out):
        assert exp_out(out), (
            f"CASE:\nscript=***\n{script}\n***,\nExpected: {exp_out!r},\nActual: {out!r}"
        )
    else:
        assert exp_out == out
    assert exp_rtn == rtn


ALL_PLATFORMS_STDERR = [
    # test redirecting a function alias
    (
        """
def _f(args, stdin, stdout):
    print('Wow Mom!', file=stdout)

aliases['f'] = _f
f o>e
""",
        "Wow Mom!\n",
        0,
    )
]


@pytest.mark.parametrize("case", ALL_PLATFORMS_STDERR)
def test_script_stderr(case):
    script, exp_err, exp_rtn = case
    out, err, rtn = run_xonsh(script, stderr=sp.PIPE)
    assert exp_err == err
    assert exp_rtn == rtn


@skip_if_on_windows
@pytest.mark.parametrize(
    "cmd, fmt, exp",
    [
        ("pwd", None, lambda: os.getcwd() + "\n"),
        ("echo WORKING", None, "WORKING\n"),
        ("ls -f", lambda out: out.splitlines().sort(), os.listdir().sort()),
        (
            f"$FOO='foo' $BAR=2 {sys.executable} -m xonsh --no-rc -c r'echo -n $FOO$BAR'",
            None,
            "foo2",
        ),
    ],
)
def test_single_command_no_windows(cmd, fmt, exp):
    check_run_xonsh(cmd, fmt, exp)


@skip_if_on_windows
@pytest.mark.parametrize(
    "script, expected",
    [
        # Inline `$VAR=@.env.DELETE_VAR cmd` masks the variable for the
        # immediate subprocess even when it is set in the session env.
        # The inline-prefix form requires no whitespace around `=`.
        (
            f"$XONSH_DELETE_VAR_TEST = 'leaked_value'\n"
            f"$XONSH_DELETE_VAR_TEST=@.env.DELETE_VAR {sys.executable} -c "
            f"\"import os; print('XONSH_DELETE_VAR_TEST' not in os.environ)\"\n",
            "True\n",
        ),
        # A callable alias can mask a variable for any subprocess it
        # spawns by writing the sentinel into its overlay `env`
        # parameter — the canonical example from the issue.
        # `@aliases.register` strips the leading underscore, so the
        # registered alias name is `check`, not `_check`.
        (
            f"""
$XONSH_DELETE_VAR_TEST = 'leaked_value'

@aliases.register
def _check(env):
    env['XONSH_DELETE_VAR_TEST'] = @.env.DELETE_VAR
    {sys.executable} -c "import os; print('XONSH_DELETE_VAR_TEST' not in os.environ)"

check
""",
            "True\n",
        ),
    ],
)
def test_env_delete_var(script, expected):
    out, err, rtn = run_xonsh(script)
    assert out == expected, err
    assert rtn == 0, err


@skip_if_on_windows
def test_stdin_script_reopens_tty_for_children():
    """When xonsh reads a script from stdin and /dev/tty is available,
    it should reopen fd 0 on /dev/tty so child processes see a real
    terminal instead of the exhausted pipe.

    The test first checks whether /dev/tty is reachable from a piped
    subprocess — in CI / headless environments it is not, so the test
    skips gracefully.
    """
    # Guard: can a piped child open /dev/tty at all?
    probe = sp.run(
        [sys.executable, "-c", "import os; os.open('/dev/tty', os.O_RDONLY)"],
        stdin=sp.PIPE,
        capture_output=True,
    )
    if probe.returncode != 0:
        pytest.skip("/dev/tty not available in this environment")

    script = f"{sys.executable} -c 'import os; print(os.isatty(0))'\n"
    out, err, rtn = run_xonsh(script, stderr=sp.PIPE)
    assert out.strip() == "True", f"expected child stdin to be a TTY, got: {out!r}"
    assert rtn == 0


def test_script_local_import(tmp_path):
    """xonsh script-file should add script dir to sys.path like CPython does."""
    pkg_dir = tmp_path / "pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text("")
    (pkg_dir / "mod.py").write_text("X = 42\n")
    script = tmp_path / "run.py"
    script.write_text("import pkg.mod\nprint(pkg.mod.X)\n")
    out, err, rtn = run_xonsh(
        None,
        stdin=None,
        args=["--no-rc", str(script)],
        stderr=sp.PIPE,
    )
    assert rtn == 0, f"stderr: {err}"
    assert out.strip() == "42"


def test_eof_syntax_error():
    """Ensures syntax errors for EOF appear on last line."""
    script = "x = 1\na = (1, 0\n"
    out, err, rtn = run_xonsh(script, stderr=sp.PIPE)
    assert "line 0" not in err
    assert "EOF in multi-line statement" in err and "line 2" in err


def test_open_quote_syntax_error():
    script = (
        "# header padding — keeps the unclosed quote on line 5\n\n"
        'echo "This is line 3"\n'
        'print ("This is line 4")\n'
        'x = "This is a string where I forget the closing quote on line 5\n'
        'echo "This is line 6"\n'
    )
    out, err, rtn = run_xonsh(script, stderr=sp.PIPE)
    assert """('code: "This is line 3"',)""" not in err
    assert "line 5" in err
    assert "SyntaxError:" in err


_bad_case = pytest.mark.skipif(
    ON_DARWIN or ON_WINDOWS or ON_TRAVIS, reason="bad platforms"
)


def test_atdollar_no_output():
    # see issue 1521
    script = """
def _echo(args):
    print(' '.join(args))
aliases['echo'] = _echo
@$(echo)
"""
    out, err, rtn = run_xonsh(script, stderr=sp.PIPE)
    assert "command is empty" in err


def test_empty_command():
    script = "$['']\n"
    out, err, rtn = run_xonsh(script, stderr=sp.PIPE)
    assert "command is empty" in err


@_bad_case
def test_printfile():
    check_run_xonsh("printfile.xsh", None, "printfile.xsh\n")


@_bad_case
def test_printname():
    check_run_xonsh("printname.xsh", None, "__main__\n")


@_bad_case
def test_sourcefile():
    check_run_xonsh("sourcefile.xsh", None, "printfile.xsh\n")


@_bad_case
@pytest.mark.parametrize(
    "cmd, fmt, exp",
    [
        # test subshell wrapping
        (
            """
import tempfile
temp_path = tempfile.mktemp()

with open(temp_path, 'w') as fp:
    fp.write("Wow mom!\\n")

(wc) < @(temp_path)
""",
            None,
            " 1  2 9 <stdin>\n",
        ),
        # test subshell statement wrapping
        (
            """
import tempfile
temp_path = tempfile.mktemp()

with open(temp_path, 'w') as fp:
    fp.write("Wow mom!\\n")

(wc;) < @(temp_path)
""",
            None,
            " 1  2 9 <stdin>\n",
        ),
    ],
)
def test_subshells(cmd, fmt, exp):
    check_run_xonsh(cmd, fmt, exp)


@skip_if_on_windows
@pytest.mark.parametrize("cmd, exp", [("pwd", lambda: os.getcwd() + "\n")])
def test_redirect_out_to_file(cmd, exp, tmpdir):
    outfile = tmpdir.mkdir("xonsh_test_dir").join("xonsh_test_file")
    command = f"{cmd} > {outfile}\n"
    out, _, _ = run_xonsh(command)
    content = outfile.read()
    if callable(exp):
        exp = exp()
    assert content == exp


@skip_if_no_make
@skip_if_no_sleep
@skip_if_on_windows
@pytest.mark.xfail(strict=False)  # TODO: fixme (super flaky on OSX)
def test_xonsh_no_close_fds():
    # see issue https://github.com/xonsh/xonsh/issues/2984
    makefile = (
        "default: all\n"
        "all:\n"
        "\t$(MAKE) s\n"
        "s:\n"
        "\t$(MAKE) a b\n"
        "a:\n"
        "\tsleep 1\n"
        "b:\n"
        "\tsleep 1\n"
    )
    with tempfile.TemporaryDirectory() as d, with_pushd(d):
        with open("Makefile", "w") as f:
            f.write(makefile)
        out = sp.check_output(["make", "-sj2", "SHELL=xonsh"], universal_newlines=True)
        assert "warning" not in out


@pytest.mark.parametrize(
    "cmd, fmt, exp",
    [
        ("cat tttt | wc", lambda x: x > "", True),
    ],  # noqa E231 (black removes space)
)
def test_pipe_between_subprocs(cmd, fmt, exp):
    """verify pipe between subprocesses doesn't throw an exception"""
    check_run_xonsh(cmd, fmt, exp)


@skip_if_on_windows
def test_negative_exit_codes_fail():
    # see issue 3309
    script = 'python -c "import os; os.abort()" && echo OK\n'
    out, err, rtn = run_xonsh(script)
    assert "OK" != out
    assert "OK" != err


@pytest.mark.parametrize(
    "cmd, exp",
    [
        ("echo '&'", "&\n"),
        ("echo foo'&'", "foo'&'\n"),
        ("echo foo '&'", "foo &\n"),
        ("echo foo '&' bar", "foo & bar\n"),
    ],
)
def test_ampersand_argument(cmd, exp):
    script = f"""
def _echo(args):
    print(' '.join(args))
aliases['echo'] = _echo
{cmd}
"""
    out, _, _ = run_xonsh(script)
    assert out == exp


@pytest.mark.parametrize(
    "cmd, exp",
    [
        ("echo '>'", ">\n"),
        ("echo '2>'", "2>\n"),
        ("echo '2>1'", "2>1\n"),
    ],
)
def test_redirect_argument(cmd, exp):
    script = f"""
def _echo(args):
    print(' '.join(args))
aliases['echo'] = _echo
{cmd}
"""
    out, _, _ = run_xonsh(script)
    assert out == exp


# issue 3402
@skip_if_on_windows
@pytest.mark.parametrize(
    "cmd, exp_rtn",
    [
        ("2+2", 0),
        ("import sys; sys.exit(0)", 0),
        ("import sys; sys.exit(100)", 100),
        ("@('exit')", 0),
        ("exit 100", 100),
        ("exit unknown", 1),
        ("exit()", 0),
        ("exit(100)", 100),
        ("__xonsh__.exit=0", 0),
        ("__xonsh__.exit=100", 100),
        ("raise Exception()", 1),
        ("raise SystemExit(100)", 100),
        ("sh -c 'exit 0'", 0),
        ("sh -c 'exit 1'", 1),
    ],
)
def test_single_command_return_code(cmd, exp_rtn):
    _, _, rtn = run_xonsh(cmd, single_command=True)
    assert rtn == exp_rtn


# issue 6426 — ``exit N`` must stop execution, not just record the rc.
@skip_if_on_windows
@pytest.mark.parametrize(
    "cmd, exp_out, exp_rtn",
    [
        ("echo 1; exit 2; echo 3", "1\n", 2),
        ("print(1); exit 2; print(3)", "1\n", 2),
        ("exit 0; echo no", "", 0),
        ("exit 2", "", 2),
        ("echo 1 && exit 2 && echo 3", "1\n", 2),
        ("__xonsh__.exit=5; echo no", "", 5),
        (
            "@aliases.register\n"
            "def _a():\n"
            "    print(1)\n"
            "    exit 2\n"
            "    print(2)\n"
            "a\n"
            "print(3)\n",
            "1\n",
            2,
        ),
    ],
)
def test_exit_aborts_execution(cmd, exp_out, exp_rtn):
    out, _, rtn = run_xonsh(cmd, single_command=True)
    assert out == exp_out
    assert rtn == exp_rtn


@skip_if_on_msys
@skip_if_on_windows
@skip_if_on_darwin
@skip_if_on_bsd
def test_argv0():
    # The check script uses /proc/<pid>/cmdline, which is only available on
    # Linux's procfs. macOS and the BSDs have no /proc by default (and
    # FreeBSD's optional linprocfs is rarely mounted), so the helper has no
    # portable way to read argv[0] there.
    check_run_xonsh("checkargv0.xsh", None, "OK\n")


@pytest.mark.parametrize("interactive", [True, False])
def test_loading_correctly(monkeypatch, interactive):
    # Ensure everything loads correctly in interactive mode (e.g. #4289)
    monkeypatch.setenv("SHELL_TYPE", "prompt_toolkit")
    monkeypatch.setenv("XONSH_LOGIN", "1")
    monkeypatch.setenv("XONSH_INTERACTIVE", "1")
    out, err, ret = run_xonsh(
        "import xonsh; echo -n AAA @(xonsh.__file__) BBB",
        interactive=interactive,
        single_command=True,
    )
    assert not err
    assert ret == 0
    our_xonsh = (
        xonsh.__file__
    )  # make sure xonsh didn't fail and fallback to the system shell
    assert f"AAA {our_xonsh} BBB" in out  # ignore tty warnings/prompt text


@pytest.mark.parametrize(
    "cmd",
    [
        "x = 0; (lambda: x)()",
        "x = 0; [x for _ in [0]]",
    ],
)
def test_exec_function_scope(cmd):
    _, _, rtn = run_xonsh(cmd, single_command=True)
    assert rtn == 0


@skip_if_on_unix
def test_run_currentfolder(monkeypatch):
    """Ensure we can run an executable in the current folder
    only when using an explicit path prefix (e.g. .\\file.bat).
    Bare names without a path prefix must NOT run from CWD,
    matching POSIX shell behaviour.
    """
    batfile = Path(__file__).parent.parent / "bin" / "hello_world.bat"
    monkeypatch.chdir(batfile.parent)

    # With explicit path prefix: should work
    cmd = f".\\{batfile.name}"
    out, _, _ = run_xonsh(cmd, stdout=sp.PIPE, stderr=sp.PIPE, path=os.environ["PATH"])
    assert out.strip() == "hello world"

    # Without path prefix: should NOT run from CWD
    cmd_bare = batfile.name
    out, _, _ = run_xonsh(
        cmd_bare, stdout=sp.PIPE, stderr=sp.PIPE, path=os.environ["PATH"]
    )
    assert "hello world" not in out.strip().lower()


@skip_if_on_unix
def test_run_dynamic_on_path():
    """Ensure we can run an executable which is added to the path
    after xonsh is loaded
    """
    batfile = Path(__file__).parent.parent / "bin" / "hello_world.bat"
    cmd = f"$PATH.add(r'{batfile.parent}');![hello_world.bat]"
    out, _, _ = run_xonsh(cmd, path=os.environ["PATH"])
    assert out.strip() == "hello world"


@skip_if_on_unix
def test_run_fail_not_on_path():
    """Test that xonsh fails to run an executable when not on path."""
    cmd = "hello_world.bat"
    out, _, _ = run_xonsh(cmd, stdout=sp.PIPE, stderr=sp.PIPE, path=os.environ["PATH"])
    assert out != "Hello world"


ALIASES_THREADABLE_PRINT_CASES = [
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = False
$XONSH_SHOW_TRACEBACK = False
aliases['f'] = lambda: 1/0
echo f1f1f1 ; f ; echo f2f2f2
""",
        "^f1f1f1\nException in thread.*FuncAlias.*\nZeroDivisionError.*\nf2f2f2\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = True
$XONSH_SHOW_TRACEBACK = False
aliases['f'] = lambda: 1/0
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nException in thread.*\nZeroDivisionError: .*\nsubprocess.CalledProcessError.*\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = True
$XONSH_SHOW_TRACEBACK = True
aliases['f'] = lambda: 1/0
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nException in thread.*\nTraceback.*\nZeroDivisionError: .*\nsubprocess.CalledProcessError.*\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = False
$XONSH_SHOW_TRACEBACK = True
aliases['f'] = lambda: 1/0
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nException in thread.*FuncAlias.*\nTraceback.*\nZeroDivisionError.*\nf2f2f2\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = False
$XONSH_SHOW_TRACEBACK = False
aliases['f'] = lambda: (None, "I failed", 2)
echo f1f1f1 ; f ; echo f2f2f2
""",
        "^f1f1f1\nI failed\nf2f2f2\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = True
$XONSH_SHOW_TRACEBACK = False
aliases['f'] = lambda: (None, "I failed", 2)
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nI failed\nsubprocess.CalledProcessError.*\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = True
$XONSH_SHOW_TRACEBACK = True
aliases['f'] = lambda: (None, "I failed", 2)
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nI failed.*\nTraceback.*\nsubprocess.CalledProcessError.*\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = False
$XONSH_SHOW_TRACEBACK = True
aliases['f'] = lambda: (None, "I failed", 2)
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nI failed\nf2f2f2\n$",
    ),
]

ALIASES_UNTHREADABLE_PRINT_CASES = [
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = False
$XONSH_SHOW_TRACEBACK = False
aliases['f'] = lambda: 1/0
aliases['f'].__xonsh_threadable__ = False
echo f1f1f1 ; f ; echo f2f2f2
""",
        "^f1f1f1\nException in.*FuncAlias.*\nZeroDivisionError.*\nf2f2f2\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = True
$XONSH_SHOW_TRACEBACK = False
aliases['f'] = lambda: 1/0
aliases['f'].__xonsh_threadable__ = False
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nException in.*\nZeroDivisionError: .*\nsubprocess.CalledProcessError.*\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = True
$XONSH_SHOW_TRACEBACK = True
aliases['f'] = lambda: 1/0
aliases['f'].__xonsh_threadable__ = False
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nException in.*\nTraceback.*\nZeroDivisionError: .*\nsubprocess.CalledProcessError.*\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = False
$XONSH_SHOW_TRACEBACK = True
aliases['f'] = lambda: 1/0
aliases['f'].__xonsh_threadable__ = False
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nException in.*FuncAlias.*\nTraceback.*\nZeroDivisionError.*\nf2f2f2\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = False
$XONSH_SHOW_TRACEBACK = False
aliases['f'] = lambda: (None, "I failed", 2)
aliases['f'].__xonsh_threadable__ = False
echo f1f1f1 ; f ; echo f2f2f2
""",
        "^f1f1f1\nI failed\nf2f2f2\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = True
$XONSH_SHOW_TRACEBACK = False
aliases['f'] = lambda: (None, "I failed", 2)
aliases['f'].__xonsh_threadable__ = False
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nI failed\nsubprocess.CalledProcessError.*\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = True
$XONSH_SHOW_TRACEBACK = True
aliases['f'] = lambda: (None, "I failed", 2)
aliases['f'].__xonsh_threadable__ = False
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nI failed.*\nTraceback.*\nsubprocess.CalledProcessError.*\n$",
    ),
    (
        """
$XONSH_SUBPROC_CMD_RAISE_ERROR = False
$XONSH_SHOW_TRACEBACK = True
aliases['f'] = lambda: (None, "I failed", 2)
aliases['f'].__xonsh_threadable__ = False
echo f1f1f1 ; f ; echo f2f2f2
""",
        "f1f1f1\nI failed\nf2f2f2\n$",
    ),
]


@skip_if_on_windows
@pytest.mark.parametrize(
    "case", ALIASES_THREADABLE_PRINT_CASES + ALIASES_UNTHREADABLE_PRINT_CASES
)
def test_aliases_print(case):
    cmd, match = case
    out, err, ret = run_xonsh(cmd=cmd, single_command=False)
    assert re.match(match, out, re.MULTILINE | re.DOTALL), (
        f"\nFailed:\n```\n{cmd.strip()}\n```,\nresult: {out!r}\nexpected: {match!r}."
    )


@skip_if_on_windows
@pytest.mark.parametrize("interactive", [True, False])
def test_raise_subproc_error_with_show_traceback(monkeypatch, interactive):
    out, err, ret = run_xonsh(
        "$COLOR_RESULTS=False\n$XONSH_SUBPROC_CMD_RAISE_ERROR=False\n$XONSH_SHOW_TRACEBACK=False\nls nofile",
        interactive=interactive,
        single_command=True,
    )
    assert ret != 0
    assert re.match("ls.*No such file or directory\n", out)

    out, err, ret = run_xonsh(
        "$COLOR_RESULTS=False\n$XONSH_SUBPROC_CMD_RAISE_ERROR=True\n$XONSH_SHOW_TRACEBACK=False\nls nofile",
        interactive=interactive,
        single_command=True,
    )
    assert ret != 0
    assert re.match(
        "ls:.*No such file or directory\nsubprocess.CalledProcessError: Command '\\['ls', 'nofile'\\]' returned non-zero exit status .*",
        out,
        re.MULTILINE | re.DOTALL,
    )

    out, err, ret = run_xonsh(
        "$COLOR_RESULTS=False\n$XONSH_SUBPROC_CMD_RAISE_ERROR=True\n$XONSH_SHOW_TRACEBACK=True\nls nofile",
        interactive=interactive,
        single_command=True,
    )
    assert ret != 0
    assert re.match(
        "ls.*No such file or directory.*Traceback .*\nsubprocess.CalledProcessError: Command '\\['ls', 'nofile'\\]' returned non-zero exit status .*",
        out,
        re.MULTILINE | re.DOTALL,
    )

    out, err, ret = run_xonsh(
        "$COLOR_RESULTS=False\n$XONSH_SUBPROC_CMD_RAISE_ERROR=False\n$XONSH_SHOW_TRACEBACK=True\nls nofile",
        interactive=interactive,
        single_command=True,
    )
    assert ret != 0
    assert re.match("ls.*No such file or directory\n", out)


def test_main_d():
    out, err, ret = run_xonsh(cmd="print($XONSH_HISTORY_BACKEND)", single_command=True)
    assert out == "json\n"

    out, err, ret = run_xonsh(
        args=["--no-rc", "-DXONSH_HISTORY_BACKEND='dummy'"],
        cmd="print($XONSH_HISTORY_BACKEND)",
        single_command=True,
    )
    assert out == "dummy\n"


@pytest.mark.flaky(reruns=3, reruns_delay=1)
def test_catching_system_exit():
    stdin_cmd = "__import__('sys').exit(2)\n"
    out, err, ret = run_xonsh(
        cmd=None, stdin_cmd=stdin_cmd, interactive=True, single_command=False, timeout=3
    )
    assert ret > 0


@skip_if_on_windows
@pytest.mark.flaky(reruns=3, reruns_delay=1)
def test_catching_exit_signal():
    stdin_cmd = "sleep 0.2; kill -SIGHUP @(__import__('os').getpid())\n"
    out, err, ret = run_xonsh(
        cmd=None, stdin_cmd=stdin_cmd, interactive=True, single_command=False, timeout=3
    )
    assert ret > 0


@skip_if_on_windows
def test_forwarding_sighup(tmpdir):
    """We want to make sure that SIGHUP is forwarded to subprocesses when
    received, so we spin up a Bash process that waits for SIGHUP and then
    writes `SIGHUP` to a file, then exits. Then we check the content of
    that file to ensure that the Bash process really did get SIGHUP."""
    outfile = tmpdir.mkdir("xonsh_test_dir").join("sighup_test.out")

    # Use ``sh -c`` rather than ``bash -c``: ``trap '…' HUP`` is POSIX
    # so /bin/sh works, and bash isn't guaranteed on every install (e.g.
    # FreeBSD poudriere build jails ship only the base system).
    stdin_cmd = f"""
sleep 0.2
(sleep 1 && kill -SIGHUP @(__import__('os').getppid())) &
sh -c "trap 'echo SIGHUP > {outfile}; exit 0' HUP; sleep 30 & wait $!"
"""
    proc = run_xonsh(
        cmd=None,
        stdin_cmd=stdin_cmd,
        stderr=sp.PIPE,
        interactive=True,
        single_command=False,
        blocking=False,
    )
    proc.wait(timeout=5)
    # if this raises FileNotFoundError, then the Bash subprocess probably did not get SIGHUP
    assert outfile.read_text("utf-8").strip() == "SIGHUP"


@skip_if_on_windows
def test_on_postcommand_waiting(tmpdir):
    """Ensure that running a subcommand in the on_postcommand hook doesn't
    block xonsh from exiting when there is a running foreground process."""
    outdir = tmpdir.mkdir("xonsh_test_dir")

    stdin_cmd = f"""
sleep 0.2
@events.on_postcommand
def postcmd_hook(**kwargs):
    touch {outdir}/sighup_test_postcommand

(sleep 1 && kill -SIGHUP @(__import__('os').getppid())) &
sh -c "trap '' HUP; sleep 30"
"""
    proc = run_xonsh(
        cmd=None,
        stdin_cmd=stdin_cmd,
        stderr=sp.PIPE,
        interactive=True,
        single_command=False,
        blocking=False,
    )
    proc.wait(timeout=5)


@skip_if_on_windows
def test_suspended_captured_process_pipeline():
    """See also test_specs.py:test_specs_with_suspended_captured_process_pipeline"""
    stdin_cmd = f"!({sys.executable} -c 'import os, signal, time; time.sleep(0.2); os.kill(os.getpid(), signal.SIGTTIN)')\n"
    out, err, ret = run_xonsh(
        cmd=None, stdin_cmd=stdin_cmd, interactive=True, single_command=False, timeout=5
    )
    match = ".*suspended=True.*"
    assert re.match(match, out, re.MULTILINE | re.DOTALL), (
        f"\nFailed:\n```\n{stdin_cmd.strip()}\n```,\nresult: {out!r}\nexpected: {match!r}."
    )


@skip_if_on_windows
@pytest.mark.flaky(reruns=3, reruns_delay=1)
def test_captured_subproc_is_not_affected_next_command():
    """Testing #5769."""
    stdin_cmd = (
        "t = __xonsh__.imp.time.time()\n"
        "p = !(sleep 2)\n"
        "print('OK_'+'TEST' if __xonsh__.imp.time.time() - t < 1 else 'FAIL_'+'TEST')\n"
        "t = __xonsh__.imp.time.time()\n"
        "echo 1\n"
        "print('OK_'+'TEST' if __xonsh__.imp.time.time() - t < 1 else 'FAIL_'+'TEST')\n"
    )
    out, err, ret = run_xonsh(
        cmd=None,
        stdin_cmd=stdin_cmd,
        interactive=True,
        single_command=False,
        timeout=10,
    )
    assert not re.match(".*FAIL_TEST.*", out, re.MULTILINE | re.DOTALL), (
        "The second command after running captured subprocess shouldn't wait the end of the first one."
    )


@skip_if_on_windows
@pytest.mark.flaky(reruns=3, reruns_delay=1)
def test_spec_decorator_alias():
    """Testing spec modifier alias with `@` in the alias name."""
    stdin_cmd = (
        "from xonsh.procs.specs import SpecAttrDecoratorAlias as mod\n"
        'aliases["@dict"] = mod({"output_format": lambda lines: eval("\\n".join(lines))})\n'
        "d = $(@dict echo '{\"a\":42}')\n"
        "print('Answer =', d['a'])\n"
    )
    out, err, ret = run_xonsh(
        cmd=None,
        stdin_cmd=stdin_cmd,
        interactive=True,
        single_command=False,
        timeout=10,
    )
    assert "Answer = 42" in out


@pytest.mark.parametrize(
    "cmd,exp",
    [
        [
            "-i",
            ".*CONFIG_XONSH_RC_XSH.*HOME_XONSHRC.*CONFIG_XONSH_RCD.*CONFIG_XONSH_PY_RCD.*",
        ],
        ["--rc rc.xsh", ".*RCXSH.*"],
        ["-i --rc rc.xsh", ".*RCXSH.*"],
        [
            "-c print('CMD')",
            ".*CONFIG_XONSH_RC_XSH.*CONFIG_XONSH_RCD.*CONFIG_XONSH_PY_RCD.*CMD.*",
        ],
        [
            "-i -c print('CMD')",
            ".*CONFIG_XONSH_RC_XSH.*HOME_XONSHRC.*CONFIG_XONSH_RCD.*CONFIG_XONSH_PY_RCD.*CMD.*",
        ],
        [
            "script.xsh",
            ".*CONFIG_XONSH_RC_XSH.*CONFIG_XONSH_RCD.*CONFIG_XONSH_PY_RCD.*SCRIPT.*",
        ],
        [
            "-i script.xsh",
            ".*CONFIG_XONSH_RC_XSH.*HOME_XONSHRC.*CONFIG_XONSH_RCD.*CONFIG_XONSH_PY_RCD.*SCRIPT.*",
        ],
        ["--rc rc.xsh -- script.xsh", ".*RCXSH.*SCRIPT.*"],
        ["-i --rc rc.xsh -- script.xsh", ".*RCXSH.*SCRIPT.*"],
        ["--no-rc --rc rc.xsh -- script.xsh", ".*SCRIPT.*"],
        ["-i --no-rc --rc rc.xsh -- script.xsh", ".*SCRIPT.*"],
    ],
)
def test_xonshrc(tmpdir, cmd, exp):
    # ~/.xonshrc
    home = tmpdir.mkdir("home")
    (home / ".xonshrc").write_text("echo HOME_XONSHRC", encoding="utf8")
    home_xonsh_rc_path = str(  # crossplatform path
        (Path(home) / ".xonshrc").expanduser()
    )

    # ~/.config/xonsh/rc.xsh
    home_config_xonsh = tmpdir.mkdir("home_config_xonsh")
    (home_config_xonsh_rc_xsh := home_config_xonsh / "rc.xsh").write_text(
        "echo CONFIG_XONSH_RC_XSH", encoding="utf8"
    )

    # ~/.config/xonsh/rc.d/
    home_config_xonsh_rcd = tmpdir.mkdir("home_config_xonsh_rcd")
    (home_config_xonsh_rcd / "rcd1.xsh").write_text(
        "echo CONFIG_XONSH_RCD", encoding="utf8"
    )
    (home_config_xonsh_rcd / "rcd2.py").write_text(
        "__xonsh__.print(__xonsh__.subproc_captured_stdout(['echo', 'CONFIG_XONSH_PY_RCD']))",
        encoding="utf8",
    )

    # ~/home/rc.xsh
    (rc_xsh := home / "rc.xsh").write_text("echo RCXSH", encoding="utf8")
    (script_xsh := home / "script.xsh").write_text("echo SCRIPT_XSH", encoding="utf8")

    # Construct $XONSHRC and $XONSHRC_DIR.
    xonshrc_files = [
        str(home_config_xonsh_rc_xsh),
        str(home_xonsh_rc_path),
    ]
    xonshrc_dir = [str(home_config_xonsh_rcd)]

    args = [
        f'-DHOME="{str(home)}"',
        f'-DXONSHRC="{os.pathsep.join(xonshrc_files)}"',
        f'-DXONSHRC_DIR="{os.pathsep.join(xonshrc_dir)}"',
    ]
    env = {"HOME": str(home)}

    cmd = cmd.replace("rc.xsh", str(rc_xsh)).replace("script.xsh", str(script_xsh))
    args = args + cmd.split()

    # xonsh
    out, err, ret = run_xonsh(
        cmd=None,
        args=args,
        env=env,
    )

    assert re.match(
        exp,
        out,
        re.MULTILINE | re.DOTALL,
    ), f"Case: xonsh {cmd},\nExpected: {exp!r},\nResult: {out!r},\nargs={args!r}"


@skip_if_on_windows
def test_shebang_cr(tmpdir):
    testdir = tmpdir.mkdir("xonsh_test_dir")
    testfile = "shebang_cr.xsh"
    expected_out = "I'm xonsh with shebang␍"
    (f := testdir / testfile).write_text(
        f"""#!/usr/bin/env -S {sys.executable} -m xonsh\r\nprint("{expected_out}")""",
        encoding="utf8",
    )
    os.chmod(f, 0o777)
    command = f"cd {testdir}; ./{testfile}\n"
    # The shebang spawns a fresh ``python -m xonsh`` whose cwd is ``testdir``.
    # That subprocess can't import xonsh from a source-tree layout unless we
    # forward the repo root via PYTHONPATH (CI / dev setups without a global
    # ``pip install`` would fail otherwise).
    xonsh_root = str(Path(__file__).resolve().parents[2])
    out, err, rtn = run_xonsh(command, env={"PYTHONPATH": xonsh_root})
    assert out == f"{expected_out}\n"
