Skip to content

Testing

Opt-in test helper: invoke runs a command line in-process and returns its status and captured output. import duho.testing to use it; core duho never imports this module. See the testing guide.

duho.testing

Test helper: run a duho command line in-process and capture what it did.

Opt-in: core never imports this module (import duho.testing to use it).

__all__ = ['Result', 'invoke'] module-attribute

Result

Bases: _Observed

What :func:invoke observed: exit status and the captured streams.

It is the three-item tuple (status, stdout, stderr); stdout_bytes is the standard output as written, bytes included, and stdout its UTF-8 decoding (errors="replace").

__new__(status, stdout, stderr, stdout_bytes=b'')

Source code in src/duho/testing.py
34
35
36
37
38
39
def __new__(
    cls, status: int, stdout: str, stderr: str, stdout_bytes: bytes = b""
) -> Result:
    self = super().__new__(cls, status, stdout, stderr)
    self.stdout_bytes = stdout_bytes
    return self

invoke(root, argv=(), *, env=None, stdin=None, **app_kwargs)

Run root with argv in-process and return a :class:Result.

With no app_kwargs this is duho.main(root, argv); with any, it is duho.app(root, argv=argv, **app_kwargs). Standard output and error are captured; standard output is a UTF-8 text stream over a byte buffer, so a command may write to sys.stdout.buffer too and Result.stdout_bytes holds everything in order; env is applied to os.environ for the call and restored; stdin is the text the command reads (empty when None).

A SystemExit (--help, a usage error, sys.exit) becomes the status; a returned None is 0. Text passed to SystemExit is written to the captured stderr with status 1. Any other exception propagates, with the environment and streams restored.

Source code in src/duho/testing.py
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def invoke(
    root: type,
    argv: _ty.Sequence[str] = (),
    *,
    env: _ty.Optional[_ty.Mapping[str, str]] = None,
    stdin: _ty.Optional[str] = None,
    **app_kwargs: _ty.Any,
) -> Result:
    """Run ``root`` with ``argv`` in-process and return a :class:`Result`.

    With no ``app_kwargs`` this is ``duho.main(root, argv)``; with any, it is
    ``duho.app(root, argv=argv, **app_kwargs)``. Standard output and error are
    captured; standard output is a UTF-8 text stream over a byte buffer, so a
    command may write to ``sys.stdout.buffer`` too and ``Result.stdout_bytes``
    holds everything in order; ``env`` is applied to ``os.environ`` for the call and restored;
    ``stdin`` is the text the command reads (empty when ``None``).

    A ``SystemExit`` (``--help``, a usage error, ``sys.exit``) becomes the
    ``status``; a returned ``None`` is ``0``. Text passed to ``SystemExit``
    is written to the captured stderr with status ``1``. Any other exception
    propagates, with the environment and streams restored.
    """
    raw = _io.BytesIO()
    out = _io.TextIOWrapper(
        raw, encoding="utf-8", errors="replace", newline="\n", write_through=True
    )
    err = _io.StringIO()
    old_stdin = _sys.stdin
    _sys.stdin = _io.StringIO(stdin or "")
    try:
        # An ExitStack, not a parenthesized `with`: that form is 3.10 grammar.
        with _contextlib.ExitStack() as stack:
            stack.enter_context(_environ(env))
            stack.enter_context(_contextlib.redirect_stdout(out))
            stack.enter_context(_contextlib.redirect_stderr(err))
            try:
                if app_kwargs:
                    code = _app(root, argv=list(argv), **app_kwargs)
                else:
                    code = _main(root, list(argv))
            except SystemExit as exc:
                code = exc.code
            status = _status(code, err)
    finally:
        _sys.stdin = old_stdin
    out.flush()
    out.detach()
    data = raw.getvalue()
    return Result(status, data.decode("utf-8", "replace"), err.getvalue(), data)