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.
Files changed (81) hide show
  1. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/workflows/test.yml +25 -3
  2. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/PKG-INFO +1 -1
  3. marsh_lib-0.3.2/docs/v0.3.2-test-portability.md +73 -0
  4. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/pyproject.toml +1 -1
  5. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/modifier_functions/readers.py +2 -2
  6. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/processor_functions/printers.py +3 -2
  7. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/processor_functions/redirections.py +1 -1
  8. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.circleci/config.yml +0 -0
  9. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  10. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  11. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/ISSUE_TEMPLATE/other.md +0 -0
  12. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/pull_request_template.md +0 -0
  13. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/workflows/docker-integration.yml +0 -0
  14. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/workflows/release-rehearsal.yml +0 -0
  15. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/workflows/release.yml +0 -0
  16. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.github/workflows/ssh-integration.yml +0 -0
  17. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/.gitignore +0 -0
  18. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/LICENSE +0 -0
  19. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/README.md +0 -0
  20. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/Taskfile.yml +0 -0
  21. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/docs/P0_REPOSITORY_AUDIT.md +0 -0
  22. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/docs/P6_UX_COMPOSITION_EVALUATION.md +0 -0
  23. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/docs/api-migration.md +0 -0
  24. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/docs/architecture.md +0 -0
  25. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/docs/workflow-guide.md +0 -0
  26. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/run_act.sh +0 -0
  27. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/run_pytest.sh +0 -0
  28. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/run_tests_with_sysbox.sh +0 -0
  29. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/__init__.py +0 -0
  30. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/bash/__init__.py +0 -0
  31. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/bash/bash_factory.py +0 -0
  32. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/bash/bash_grammar.py +0 -0
  33. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/bash/bash_runner_decorators.py +0 -0
  34. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/bash/bash_script.py +0 -0
  35. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/constants.py +0 -0
  36. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/__init__.py +0 -0
  37. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/authenticator.py +0 -0
  38. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/cache.py +0 -0
  39. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/cmd_run_decorator.py +0 -0
  40. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/cmd_runner_spec.py +0 -0
  41. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/command_grammar.py +0 -0
  42. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/configuration.py +0 -0
  43. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/connector.py +0 -0
  44. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/conveyor.py +0 -0
  45. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/dag_bridge.py +0 -0
  46. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/diagnostics.py +0 -0
  47. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/domain.py +0 -0
  48. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/executor.py +0 -0
  49. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/expression.py +0 -0
  50. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/observability.py +0 -0
  51. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/policies.py +0 -0
  52. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/providers.py +0 -0
  53. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/runtime.py +0 -0
  54. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/script.py +0 -0
  55. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/serialization.py +0 -0
  56. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/core/validation.py +0 -0
  57. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/dag/__init__.py +0 -0
  58. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/dag/dag.py +0 -0
  59. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/dag/node.py +0 -0
  60. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/dag/startable.py +0 -0
  61. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/docker/__init__.py +0 -0
  62. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/docker/docker_command_grammar.py +0 -0
  63. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/docker/docker_executor.py +0 -0
  64. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/exceptions.py +0 -0
  65. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/logger.py +0 -0
  66. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/modifier_functions/__init__.py +0 -0
  67. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/modifier_functions/case_conversion.py +0 -0
  68. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/powershell/__init__.py +0 -0
  69. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/processor_functions/__init__.py +0 -0
  70. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/processor_functions/raisers.py +0 -0
  71. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/providers/__init__.py +0 -0
  72. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/runtime/__init__.py +0 -0
  73. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/signals.py +0 -0
  74. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/ssh/__init__.py +0 -0
  75. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/ssh/ssh_command_grammar.py +0 -0
  76. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/ssh/ssh_connector.py +0 -0
  77. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/ssh/ssh_factory.py +0 -0
  78. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/utils/__init__.py +0 -0
  79. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/utils/output_streams.py +0 -0
  80. {marsh_lib-0.3.1 → marsh_lib-0.3.2}/src/marsh/workflow/__init__.py +0 -0
  81. {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@v2
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.1
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.1"
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