marsh-lib 0.3.1__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/workflows/test.yml +25 -3
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/PKG-INFO +1 -1
- marsh_lib-0.3.2/docs/v0.3.2-test-portability.md +73 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/pyproject.toml +1 -1
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/modifier_functions/readers.py +2 -2
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/processor_functions/printers.py +3 -2
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/processor_functions/redirections.py +1 -1
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.circleci/config.yml +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/ISSUE_TEMPLATE/other.md +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/pull_request_template.md +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/workflows/docker-integration.yml +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/workflows/release-rehearsal.yml +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/workflows/release.yml +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/workflows/ssh-integration.yml +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.gitignore +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/LICENSE +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/README.md +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/Taskfile.yml +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/docs/P0_REPOSITORY_AUDIT.md +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/docs/P6_UX_COMPOSITION_EVALUATION.md +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/docs/api-migration.md +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/docs/architecture.md +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/docs/workflow-guide.md +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/run_act.sh +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/run_pytest.sh +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/run_tests_with_sysbox.sh +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/bash/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/bash/bash_factory.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/bash/bash_grammar.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/bash/bash_runner_decorators.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/bash/bash_script.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/constants.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/authenticator.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/cache.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/cmd_run_decorator.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/cmd_runner_spec.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/command_grammar.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/configuration.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/connector.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/conveyor.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/dag_bridge.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/diagnostics.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/domain.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/executor.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/expression.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/observability.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/policies.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/providers.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/runtime.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/script.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/serialization.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/validation.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/dag/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/dag/dag.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/dag/node.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/dag/startable.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/docker/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/docker/docker_command_grammar.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/docker/docker_executor.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/exceptions.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/logger.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/modifier_functions/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/modifier_functions/case_conversion.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/powershell/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/processor_functions/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/processor_functions/raisers.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/providers/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/runtime/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/signals.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/ssh/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/ssh/ssh_command_grammar.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/ssh/ssh_connector.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/ssh/ssh_factory.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/utils/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/utils/output_streams.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/workflow/__init__.py +0 -0
- {marsh_lib-0.3.1 → marsh_lib-0.3.2}/uv.lock +0 -0
|
@@ -4,7 +4,7 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
branches: ["*"]
|
|
6
6
|
pull_request:
|
|
7
|
-
branches: ["main"]
|
|
7
|
+
branches: ["develop", "main"]
|
|
8
8
|
workflow_dispatch:
|
|
9
9
|
|
|
10
10
|
permissions:
|
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
runs-on: ubuntu-latest
|
|
17
17
|
steps:
|
|
18
18
|
- name: Checkout code
|
|
19
|
-
uses: actions/checkout@
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
20
|
|
|
21
21
|
- name: Install uv
|
|
22
22
|
uses: astral-sh/setup-uv@v10.2.0
|
|
@@ -27,7 +27,6 @@ jobs:
|
|
|
27
27
|
run: |
|
|
28
28
|
uv sync --all-groups
|
|
29
29
|
|
|
30
|
-
# TODO: Setup Docker for testing docker components (and possible ssh components).
|
|
31
30
|
- name: Run pytest
|
|
32
31
|
run: |
|
|
33
32
|
cd src
|
|
@@ -45,3 +44,26 @@ jobs:
|
|
|
45
44
|
with:
|
|
46
45
|
coverageFile: coverage.xml
|
|
47
46
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
47
|
+
|
|
48
|
+
windows_portability:
|
|
49
|
+
name: Windows portability (Python 3.12)
|
|
50
|
+
runs-on: windows-latest
|
|
51
|
+
steps:
|
|
52
|
+
- name: Checkout code
|
|
53
|
+
uses: actions/checkout@v4
|
|
54
|
+
|
|
55
|
+
- name: Install uv
|
|
56
|
+
uses: astral-sh/setup-uv@v10.2.0
|
|
57
|
+
with:
|
|
58
|
+
python-version: '3.12'
|
|
59
|
+
|
|
60
|
+
- name: Install dependencies
|
|
61
|
+
run: uv sync --all-groups
|
|
62
|
+
|
|
63
|
+
- name: Run portable pytest suite
|
|
64
|
+
run: >
|
|
65
|
+
uv run pytest -vv --disable-warnings --tb=short
|
|
66
|
+
--ignore=tests/bash
|
|
67
|
+
--ignore=tests/docker
|
|
68
|
+
--ignore=tests/ssh
|
|
69
|
+
tests/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: marsh-lib
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Lightweight, extensible Python library for building, managing, and executing command workflows.
|
|
5
5
|
Project-URL: Repository, https://github.com/CedricAnover/marsh
|
|
6
6
|
Author-email: Cedric Anover <cedric.anover@hotmail.com>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# v0.3.2 Test Portability Audit
|
|
2
|
+
|
|
3
|
+
Date: 2026-10-04
|
|
4
|
+
Release: v0.3.2
|
|
5
|
+
Base branch: develop
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
Make the test suite platform-independent without changing Marsh execution semantics.
|
|
10
|
+
|
|
11
|
+
## Portability boundary
|
|
12
|
+
|
|
13
|
+
### Portable unit tests
|
|
14
|
+
|
|
15
|
+
These tests must not depend on a host shell executable when shell/process behavior is not the subject:
|
|
16
|
+
|
|
17
|
+
- `tests/core/test_executor.py`
|
|
18
|
+
- callback behavior
|
|
19
|
+
- timeout behavior
|
|
20
|
+
- invalid callback return behavior
|
|
21
|
+
- mocked subprocess lifecycle
|
|
22
|
+
- Core/runtime tests that use `sys.executable` for genuine Python subprocess behavior.
|
|
23
|
+
|
|
24
|
+
### Genuine subprocess tests
|
|
25
|
+
|
|
26
|
+
A real subprocess is appropriate when process lifecycle/exit/timeout behavior is the behavior under test.
|
|
27
|
+
|
|
28
|
+
Current examples:
|
|
29
|
+
|
|
30
|
+
- `tests/core/test_runtime.py` uses `sys.executable` for deterministic Python subprocesses and timeout/cancellation coverage.
|
|
31
|
+
|
|
32
|
+
Rule: use `sys.executable` rather than a shell convenience command.
|
|
33
|
+
|
|
34
|
+
### Provider/OS integration tests
|
|
35
|
+
|
|
36
|
+
These intentionally retain provider-specific or OS-specific behavior and must stay outside the portable Windows unit gate:
|
|
37
|
+
|
|
38
|
+
- `tests/bash/` — Bash provider/grammar/script behavior.
|
|
39
|
+
- `tests/docker/` — Docker provider/integration behavior; commands such as `bash`, `echo`, and `sleep` are exercised inside containers.
|
|
40
|
+
- `tests/ssh/` — SSH/provider acceptance and remote shell behavior.
|
|
41
|
+
- `tests/core/test_py_command_grammar.py` — explicit command-grammar construction for configured shell commands; this tests command construction, not local Windows execution.
|
|
42
|
+
|
|
43
|
+
## Issue #90 and sibling findings
|
|
44
|
+
|
|
45
|
+
`tests/core/test_executor.py` contained unit tests that selected Unix shell commands (`bash`, `echo`, `sleep`) even though the tests were validating executor callback/timeout/result semantics. Those dependencies make the tests host-shell dependent.
|
|
46
|
+
|
|
47
|
+
The v0.3.2 fix is to mock `subprocess.Popen` for those semantics tests. Production `LocalCommandExecutor` remains unchanged and continues to use `shell=False`.
|
|
48
|
+
|
|
49
|
+
## Policy
|
|
50
|
+
|
|
51
|
+
1. Mock process creation when subprocess behavior itself is not the subject.
|
|
52
|
+
2. Use `sys.executable` for genuine Python subprocess tests.
|
|
53
|
+
3. Do not use `echo`, `bash`, `sh`, `sleep`, or similar commands merely as convenient test fixtures.
|
|
54
|
+
4. Keep provider/OS integration tests explicitly separated from portable unit tests.
|
|
55
|
+
5. Do not add a subprocess-testing dependency without evidence that the existing stdlib/pytest tooling is insufficient.
|
|
56
|
+
|
|
57
|
+
## CI boundary
|
|
58
|
+
|
|
59
|
+
- CircleCI Linux Python 3.10/3.11/3.12: broad compatibility.
|
|
60
|
+
- GitHub Actions Windows Python 3.12: native portability gate.
|
|
61
|
+
- GitHub Actions Ubuntu + Docker: Docker integration.
|
|
62
|
+
- Existing SSH integration: SSH/provider acceptance.
|
|
63
|
+
|
|
64
|
+
Native Windows is the portability gate. Linux Docker is not a substitute for native Windows.
|
|
65
|
+
|
|
66
|
+
## Exit evidence
|
|
67
|
+
|
|
68
|
+
The audit is complete when:
|
|
69
|
+
- OS-dependent tests have an explicit classification;
|
|
70
|
+
- executor unit tests contain no accidental host-shell dependency;
|
|
71
|
+
- genuine subprocess tests use a portable executable;
|
|
72
|
+
- CI explicitly separates portable tests from provider/OS integration;
|
|
73
|
+
- no production execution semantics were changed for portability.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "marsh-lib"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Lightweight, extensible Python library for building, managing, and executing command workflows."
|
|
9
9
|
authors = [{ name = "Cedric Anover", email = "cedric.anover@hotmail.com" }]
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
def read_file(inp_stdout: bytes, inp_stderr: bytes, file_path: str, encoding='utf-8') -> tuple[bytes, bytes]:
|
|
2
2
|
try:
|
|
3
|
-
with open(file_path, 'r') as file:
|
|
3
|
+
with open(file_path, 'r', encoding=encoding) as file:
|
|
4
4
|
return file.read().encode(encoding), b""
|
|
5
|
-
except (FileNotFoundError, PermissionError, IsADirectoryError, OSError, UnicodeEncodeError, IOError) as e:
|
|
5
|
+
except (FileNotFoundError, PermissionError, IsADirectoryError, OSError, UnicodeEncodeError, UnicodeDecodeError, IOError) as e:
|
|
6
6
|
return b"", str(e).encode(encoding)
|
|
7
7
|
|
|
8
8
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pprint
|
|
2
2
|
import logging
|
|
3
|
+
import sys
|
|
3
4
|
|
|
4
5
|
from ..utils.output_streams import mask_sensitive_data
|
|
5
6
|
from ..logger import create_console_logger
|
|
@@ -17,10 +18,10 @@ def print_output_stream(inp_stdout: bytes,
|
|
|
17
18
|
|
|
18
19
|
if output_stream == "stdout":
|
|
19
20
|
if inp_stdout.strip():
|
|
20
|
-
print(inp_stdout.decode(encoding).strip(), *args, **kwargs)
|
|
21
|
+
print(inp_stdout.decode(encoding).strip(), *args, file=sys.stdout, **kwargs)
|
|
21
22
|
else:
|
|
22
23
|
if inp_stderr.strip():
|
|
23
|
-
print(inp_stderr.decode(encoding).strip(), *args, **kwargs)
|
|
24
|
+
print(inp_stderr.decode(encoding).strip(), *args, file=sys.stderr, **kwargs)
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def print_stdout(inp_stdout: bytes, inp_stderr: bytes, *args, encoding='utf-8', **kwargs) -> None:
|
|
@@ -14,7 +14,7 @@ def redirect_output_stream(inp_stdout: bytes,
|
|
|
14
14
|
if output_stream not in ["stdout", "stderr"]:
|
|
15
15
|
raise ValueError("Output stream must be 'stdout' or 'stderr'.")
|
|
16
16
|
|
|
17
|
-
with open(file_path, mode) as file:
|
|
17
|
+
with open(file_path, mode, encoding=encoding) as file:
|
|
18
18
|
if output_stream == "stdout":
|
|
19
19
|
file.write(inp_stdout.decode(encoding).strip())
|
|
20
20
|
else:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|