vault88 0.1.0__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 (50) hide show
  1. vault88-0.1.0/PKG-INFO +168 -0
  2. vault88-0.1.0/README.md +149 -0
  3. vault88-0.1.0/pyproject.toml +66 -0
  4. vault88-0.1.0/setup.cfg +4 -0
  5. vault88-0.1.0/tests/test_args.py +65 -0
  6. vault88-0.1.0/tests/test_console.py +160 -0
  7. vault88-0.1.0/tests/test_environment.py +82 -0
  8. vault88-0.1.0/tests/test_junit_reporter.py +281 -0
  9. vault88-0.1.0/tests/test_loader.py +266 -0
  10. vault88-0.1.0/tests/test_loaders.py +56 -0
  11. vault88-0.1.0/tests/test_log_archive_reporter.py +165 -0
  12. vault88-0.1.0/tests/test_logger.py +91 -0
  13. vault88-0.1.0/tests/test_main.py +207 -0
  14. vault88-0.1.0/tests/test_positive_path.py +155 -0
  15. vault88-0.1.0/tests/test_runner_edge_cases.py +134 -0
  16. vault88-0.1.0/tests/test_ssh.py +211 -0
  17. vault88-0.1.0/tests/test_test_construct.py +188 -0
  18. vault88-0.1.0/tests/test_webdriver.py +95 -0
  19. vault88-0.1.0/vault88/__about__.py +8 -0
  20. vault88-0.1.0/vault88/__init__.py +31 -0
  21. vault88-0.1.0/vault88/__main__.py +5 -0
  22. vault88-0.1.0/vault88/args.py +38 -0
  23. vault88-0.1.0/vault88/constructs/__init__.py +10 -0
  24. vault88-0.1.0/vault88/constructs/environment.py +70 -0
  25. vault88-0.1.0/vault88/constructs/result.py +28 -0
  26. vault88-0.1.0/vault88/constructs/stage.py +14 -0
  27. vault88-0.1.0/vault88/constructs/test.py +137 -0
  28. vault88-0.1.0/vault88/core/__init__.py +9 -0
  29. vault88-0.1.0/vault88/core/environment_loader.py +15 -0
  30. vault88-0.1.0/vault88/core/environment_loaders/__init__.py +4 -0
  31. vault88-0.1.0/vault88/core/environment_loaders/api_loader.py +19 -0
  32. vault88-0.1.0/vault88/core/environment_loaders/json_file_loader.py +18 -0
  33. vault88-0.1.0/vault88/core/loader.py +169 -0
  34. vault88-0.1.0/vault88/core/logger.py +174 -0
  35. vault88-0.1.0/vault88/core/reporter.py +25 -0
  36. vault88-0.1.0/vault88/core/reporters/__init__.py +0 -0
  37. vault88-0.1.0/vault88/core/reporters/junit_reporter.py +67 -0
  38. vault88-0.1.0/vault88/core/reporters/log_archive_reporter.py +29 -0
  39. vault88-0.1.0/vault88/core/runner.py +88 -0
  40. vault88-0.1.0/vault88/main.py +166 -0
  41. vault88-0.1.0/vault88/utils/__init__.py +9 -0
  42. vault88-0.1.0/vault88/utils/console.py +193 -0
  43. vault88-0.1.0/vault88/utils/ssh.py +88 -0
  44. vault88-0.1.0/vault88/utils/webdriver.py +45 -0
  45. vault88-0.1.0/vault88.egg-info/PKG-INFO +168 -0
  46. vault88-0.1.0/vault88.egg-info/SOURCES.txt +48 -0
  47. vault88-0.1.0/vault88.egg-info/dependency_links.txt +1 -0
  48. vault88-0.1.0/vault88.egg-info/entry_points.txt +2 -0
  49. vault88-0.1.0/vault88.egg-info/requires.txt +3 -0
  50. vault88-0.1.0/vault88.egg-info/top_level.txt +1 -0
vault88-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: vault88
3
+ Version: 0.1.0
4
+ Summary: Python automated test framework
5
+ Author-email: Brad Murdoch <brad.murdoch@me.ca>
6
+ License: MIT
7
+ Project-URL: Documentation, https://gitlab.glider-eng.com/python-packages/vault88/-/wikis/home
8
+ Project-URL: Issues, https://gitlab.glider-eng.com/python-packages/vault88/-/work_items
9
+ Project-URL: Source, https://gitlab.glider-eng.com/python-packages/vault88
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: junit-xml==1.9
17
+ Requires-Dist: paramiko<4,>=3.0
18
+ Requires-Dist: selenium<5,>=4.0
19
+
20
+ # vault88
21
+
22
+ [![pipeline status](https://gitlab.glider-eng.com/python-packages/vault88/badges/main/pipeline.svg)](https://gitlab.glider-eng.com/python-packages/vault88/-/pipelines)
23
+ [![coverage report](https://gitlab.glider-eng.com/python-packages/vault88/badges/main/coverage.svg)](https://gitlab.glider-eng.com/python-packages/vault88/-/commits/main)
24
+
25
+ **vault88** is a Python automated test framework designed for structured hardware and software validation in CI/CD pipelines. Tests are written as Python classes, organised into stages, and results can be published through configurable reporters.
26
+
27
+ ## Architecture
28
+
29
+ | Component | Role |
30
+ |---|---|
31
+ | `Loader` | Discovers test classes from files or directories; supports tag-based include/exclude filtering |
32
+ | `Logger` | Centralised logging with rotating file handler and optional verbose (debug) output |
33
+ | `Runner` | Instantiates and executes a test class through its full lifecycle: `selfCheck` → `setup` → `run` → `teardown` |
34
+ | `Reporter` | Pluggable reporters loaded from the config file; built-in reporters include JUnit XML and log archive |
35
+
36
+ Test results are grouped into named **stages** within each test, and each result records an action, expected value, actual value, outcome, timestamp, and optional requirement references.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install vault88
42
+ ```
43
+
44
+ Or with [uv](https://docs.astral.sh/uv/):
45
+
46
+ ```bash
47
+ uv add vault88
48
+ ```
49
+
50
+ ## Writing a test
51
+
52
+ Subclass `Test` and implement the four lifecycle methods:
53
+
54
+ ```python
55
+ from vault88 import Test
56
+
57
+ class MyTest(Test):
58
+ name = "My Test"
59
+ tags = ["smoke"]
60
+
61
+ def selfCheck(self) -> bool:
62
+ # Return False to skip the test entirely
63
+ return True
64
+
65
+ def setup(self) -> bool:
66
+ # Prepare resources; return False to abort
67
+ return True
68
+
69
+ def run(self) -> None:
70
+ self.newStage("Basic checks")
71
+ self.assertEqual("Verify result", expected=42, actual=compute())
72
+
73
+ def teardown(self) -> None:
74
+ pass
75
+ ```
76
+
77
+ Available assertion helpers: `assertEqual`, `assertNotEqual`, `assertTrue`, `assertFalse`, `assertGreaterThan`, `assertLessThan`, `assertGreaterThanOrEqual`, `assertLessThanOrEqual`, `assertIn`, `assertNotIn`, `assertIsNone`, `assertIsNotNone`.
78
+
79
+ ## Running tests
80
+
81
+ ```bash
82
+ vault88 <testpath> [options]
83
+ ```
84
+
85
+ | Option | Description |
86
+ |---|---|
87
+ | `testpath` | Path to a test file or directory |
88
+ | `-t TAG [TAG ...]` | Only run tests that have **at least one** of these tags |
89
+ | `-nt TAG [TAG ...]` | Exclude tests that have any of these tags |
90
+ | `-r` | Recursively search directories for tests |
91
+ | `-l FILE` | Log file path (default: `./test_execution.log`) |
92
+ | `-v` | Verbose / debug logging |
93
+ | `--env PATH_OR_URL` | Load an environment from a JSON file or HTTP(S) URL |
94
+ | `--junit` | Generate a `testresults.xml` JUnit report in the current directory |
95
+
96
+ ## Configuration
97
+
98
+ Reporters are configured in `/etc/vault88.conf` (INI format). Each `[REPORTER<n>]` section loads one reporter:
99
+
100
+ ```ini
101
+ [REPORTER1]
102
+ module = vault88.core.reporters.junit_reporter
103
+ class = JUnitReporter
104
+ enabled = true
105
+ suite_name = My Project Tests
106
+
107
+ [REPORTER2]
108
+ module = vault88.core.reporters.log_archive_reporter
109
+ class = LogArchiveReporter
110
+ enabled = true
111
+ output_dir = ./logs
112
+ ```
113
+
114
+ ## Development
115
+
116
+ Dependencies are managed with [uv](https://docs.astral.sh/uv/). Install the project with test dependencies:
117
+
118
+ ```bash
119
+ uv sync
120
+ ```
121
+
122
+ ### Running the test suite
123
+
124
+ ```bash
125
+ uv run pytest
126
+ ```
127
+
128
+ Run with coverage:
129
+
130
+ ```bash
131
+ uv run pytest --cov=vault88 --cov-report=term-missing
132
+ ```
133
+
134
+ Run tests in parallel:
135
+
136
+ ```bash
137
+ uv run pytest -n auto
138
+ ```
139
+
140
+ ### Linting with ruff
141
+
142
+ Check for issues:
143
+
144
+ ```bash
145
+ uv run ruff check .
146
+ ```
147
+
148
+ Auto-fix fixable issues:
149
+
150
+ ```bash
151
+ uv run ruff check --fix .
152
+ ```
153
+
154
+ Check formatting:
155
+
156
+ ```bash
157
+ uv run ruff format --check .
158
+ ```
159
+
160
+ Apply formatting:
161
+
162
+ ```bash
163
+ uv run ruff format .
164
+ ```
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,149 @@
1
+ # vault88
2
+
3
+ [![pipeline status](https://gitlab.glider-eng.com/python-packages/vault88/badges/main/pipeline.svg)](https://gitlab.glider-eng.com/python-packages/vault88/-/pipelines)
4
+ [![coverage report](https://gitlab.glider-eng.com/python-packages/vault88/badges/main/coverage.svg)](https://gitlab.glider-eng.com/python-packages/vault88/-/commits/main)
5
+
6
+ **vault88** is a Python automated test framework designed for structured hardware and software validation in CI/CD pipelines. Tests are written as Python classes, organised into stages, and results can be published through configurable reporters.
7
+
8
+ ## Architecture
9
+
10
+ | Component | Role |
11
+ |---|---|
12
+ | `Loader` | Discovers test classes from files or directories; supports tag-based include/exclude filtering |
13
+ | `Logger` | Centralised logging with rotating file handler and optional verbose (debug) output |
14
+ | `Runner` | Instantiates and executes a test class through its full lifecycle: `selfCheck` → `setup` → `run` → `teardown` |
15
+ | `Reporter` | Pluggable reporters loaded from the config file; built-in reporters include JUnit XML and log archive |
16
+
17
+ Test results are grouped into named **stages** within each test, and each result records an action, expected value, actual value, outcome, timestamp, and optional requirement references.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install vault88
23
+ ```
24
+
25
+ Or with [uv](https://docs.astral.sh/uv/):
26
+
27
+ ```bash
28
+ uv add vault88
29
+ ```
30
+
31
+ ## Writing a test
32
+
33
+ Subclass `Test` and implement the four lifecycle methods:
34
+
35
+ ```python
36
+ from vault88 import Test
37
+
38
+ class MyTest(Test):
39
+ name = "My Test"
40
+ tags = ["smoke"]
41
+
42
+ def selfCheck(self) -> bool:
43
+ # Return False to skip the test entirely
44
+ return True
45
+
46
+ def setup(self) -> bool:
47
+ # Prepare resources; return False to abort
48
+ return True
49
+
50
+ def run(self) -> None:
51
+ self.newStage("Basic checks")
52
+ self.assertEqual("Verify result", expected=42, actual=compute())
53
+
54
+ def teardown(self) -> None:
55
+ pass
56
+ ```
57
+
58
+ Available assertion helpers: `assertEqual`, `assertNotEqual`, `assertTrue`, `assertFalse`, `assertGreaterThan`, `assertLessThan`, `assertGreaterThanOrEqual`, `assertLessThanOrEqual`, `assertIn`, `assertNotIn`, `assertIsNone`, `assertIsNotNone`.
59
+
60
+ ## Running tests
61
+
62
+ ```bash
63
+ vault88 <testpath> [options]
64
+ ```
65
+
66
+ | Option | Description |
67
+ |---|---|
68
+ | `testpath` | Path to a test file or directory |
69
+ | `-t TAG [TAG ...]` | Only run tests that have **at least one** of these tags |
70
+ | `-nt TAG [TAG ...]` | Exclude tests that have any of these tags |
71
+ | `-r` | Recursively search directories for tests |
72
+ | `-l FILE` | Log file path (default: `./test_execution.log`) |
73
+ | `-v` | Verbose / debug logging |
74
+ | `--env PATH_OR_URL` | Load an environment from a JSON file or HTTP(S) URL |
75
+ | `--junit` | Generate a `testresults.xml` JUnit report in the current directory |
76
+
77
+ ## Configuration
78
+
79
+ Reporters are configured in `/etc/vault88.conf` (INI format). Each `[REPORTER<n>]` section loads one reporter:
80
+
81
+ ```ini
82
+ [REPORTER1]
83
+ module = vault88.core.reporters.junit_reporter
84
+ class = JUnitReporter
85
+ enabled = true
86
+ suite_name = My Project Tests
87
+
88
+ [REPORTER2]
89
+ module = vault88.core.reporters.log_archive_reporter
90
+ class = LogArchiveReporter
91
+ enabled = true
92
+ output_dir = ./logs
93
+ ```
94
+
95
+ ## Development
96
+
97
+ Dependencies are managed with [uv](https://docs.astral.sh/uv/). Install the project with test dependencies:
98
+
99
+ ```bash
100
+ uv sync
101
+ ```
102
+
103
+ ### Running the test suite
104
+
105
+ ```bash
106
+ uv run pytest
107
+ ```
108
+
109
+ Run with coverage:
110
+
111
+ ```bash
112
+ uv run pytest --cov=vault88 --cov-report=term-missing
113
+ ```
114
+
115
+ Run tests in parallel:
116
+
117
+ ```bash
118
+ uv run pytest -n auto
119
+ ```
120
+
121
+ ### Linting with ruff
122
+
123
+ Check for issues:
124
+
125
+ ```bash
126
+ uv run ruff check .
127
+ ```
128
+
129
+ Auto-fix fixable issues:
130
+
131
+ ```bash
132
+ uv run ruff check --fix .
133
+ ```
134
+
135
+ Check formatting:
136
+
137
+ ```bash
138
+ uv run ruff format --check .
139
+ ```
140
+
141
+ Apply formatting:
142
+
143
+ ```bash
144
+ uv run ruff format .
145
+ ```
146
+
147
+ ## License
148
+
149
+ MIT
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vault88"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name = "Brad Murdoch", email = "brad.murdoch@me.ca" }
10
+ ]
11
+ description = "Python automated test framework"
12
+ readme = "README.md"
13
+ requires-python = ">=3.12"
14
+ license = { text = "MIT" }
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Programming Language :: Python",
18
+ "Programming Language :: Python :: 3.12",
19
+ "License :: OSI Approved :: MIT License"
20
+ ]
21
+ dependencies = [
22
+ "junit-xml==1.9",
23
+ "paramiko>=3.0,<4",
24
+ "selenium>=4.0,<5",
25
+ ]
26
+
27
+ [project.urls]
28
+ Documentation = "https://gitlab.glider-eng.com/python-packages/vault88/-/wikis/home"
29
+ Issues = "https://gitlab.glider-eng.com/python-packages/vault88/-/work_items"
30
+ Source = "https://gitlab.glider-eng.com/python-packages/vault88"
31
+
32
+ [project.scripts]
33
+ vault88 = "vault88.main:main"
34
+
35
+ [dependency-groups]
36
+ test = [
37
+ "coverage[toml]>=7.13.4,<8",
38
+ "pytest>=9.0.2,<10",
39
+ "pytest-cov>=7.0.0,<8",
40
+ "pytest-xdist>=3.8.0,<4",
41
+ "ruff>=0.15.2,<1"
42
+ ]
43
+
44
+ [tool.uv]
45
+ default-groups = ["test"]
46
+
47
+ [[tool.uv.index]]
48
+ name = "nexus"
49
+ url = "https://nexus.glider-eng.com/repository/pypi_hosted/simple/"
50
+ publish-url = "https://nexus.glider-eng.com/repository/pypi_hosted/"
51
+ explicit = true
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+
56
+ [tool.coverage.run]
57
+ branch = true
58
+ parallel = true
59
+ omit = []
60
+
61
+ [tool.ruff]
62
+ line-length = 100
63
+ target-version = "py312"
64
+
65
+ [tool.ruff.lint]
66
+ select = ["E", "F", "I"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,65 @@
1
+ """Tests for CLI argument parsing"""
2
+
3
+ from vault88.args import parseArgs
4
+
5
+
6
+ class TestParseArgs:
7
+ def test_minimal_required_arg(self, monkeypatch):
8
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/"])
9
+ args = parseArgs()
10
+ assert args.testpath == "tests/"
11
+
12
+ def test_tags_flag(self, monkeypatch):
13
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/", "-t", "smoke", "fast"])
14
+ args = parseArgs()
15
+ assert args.tags == ["smoke", "fast"]
16
+
17
+ def test_nottags_flag(self, monkeypatch):
18
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/", "-nt", "slow"])
19
+ args = parseArgs()
20
+ assert args.nottags == ["slow"]
21
+
22
+ def test_recursive_flag(self, monkeypatch):
23
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/", "-r"])
24
+ args = parseArgs()
25
+ assert args.recursive is True
26
+
27
+ def test_recursive_default_is_false(self, monkeypatch):
28
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/"])
29
+ args = parseArgs()
30
+ assert args.recursive is False
31
+
32
+ def test_log_flag(self, monkeypatch):
33
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/", "-l", "/tmp/run.log"])
34
+ args = parseArgs()
35
+ assert args.log == "/tmp/run.log"
36
+
37
+ def test_log_default(self, monkeypatch):
38
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/"])
39
+ args = parseArgs()
40
+ assert args.log == "./test_execution.log"
41
+
42
+ def test_junit_flag(self, monkeypatch):
43
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/", "--junit"])
44
+ args = parseArgs()
45
+ assert args.junit is True
46
+
47
+ def test_verbose_flag(self, monkeypatch):
48
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/", "-v"])
49
+ args = parseArgs()
50
+ assert args.verbose is True
51
+
52
+ def test_env_flag(self, monkeypatch):
53
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/", "--env", "http://env.local/api"])
54
+ args = parseArgs()
55
+ assert args.env == "http://env.local/api"
56
+
57
+ def test_tags_default_is_none(self, monkeypatch):
58
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/"])
59
+ args = parseArgs()
60
+ assert args.tags is None
61
+
62
+ def test_env_default_is_none(self, monkeypatch):
63
+ monkeypatch.setattr("sys.argv", ["vault88", "tests/"])
64
+ args = parseArgs()
65
+ assert args.env is None
@@ -0,0 +1,160 @@
1
+ """Tests for console.py truncation and overflow guards"""
2
+
3
+ from vault88.constructs.result import Result, ResultOutcome
4
+ from vault88.utils.console import (
5
+ _box_line,
6
+ _visible_truncate,
7
+ result_box_lines,
8
+ strip_ansi,
9
+ )
10
+
11
+ _GREEN = "\033[32m"
12
+ _RESET = "\033[0m"
13
+ _WIDTH = 80
14
+
15
+
16
+ def _result(
17
+ action="act", expected="exp", actual="act", outcome=ResultOutcome.PASSED, requirements=None
18
+ ):
19
+ return Result(action, expected, actual, outcome, requirements or [])
20
+
21
+
22
+ def _display_width(line: str) -> int:
23
+ return len(strip_ansi(line))
24
+
25
+
26
+ class TestVisibleTruncate:
27
+ def test_zero_width_returns_empty(self):
28
+ assert _visible_truncate("hello", 0) == ""
29
+
30
+ def test_negative_width_returns_empty(self):
31
+ assert _visible_truncate("hello", -5) == ""
32
+
33
+ def test_text_shorter_than_max_unchanged(self):
34
+ assert _visible_truncate("hi", 10) == "hi"
35
+
36
+ def test_text_exactly_at_max_unchanged(self):
37
+ assert _visible_truncate("hello", 5) == "hello"
38
+
39
+ def test_plain_text_truncated_with_ellipsis(self):
40
+ result = _visible_truncate("abcdef", 4)
41
+ assert result == "abc…"
42
+
43
+ def test_truncated_visible_length_equals_max_width(self):
44
+ result = _visible_truncate("abcdefghij", 6)
45
+ assert len(strip_ansi(result)) == 6
46
+
47
+ def test_ansi_codes_not_counted_toward_width(self):
48
+ colored = f"{_GREEN}hello{_RESET}"
49
+ result = _visible_truncate(colored, 5)
50
+ assert strip_ansi(result) == "hello"
51
+
52
+ def test_ansi_codes_preserved_before_cut_point(self):
53
+ colored = f"{_GREEN}abcdef{_RESET}"
54
+ result = _visible_truncate(colored, 4)
55
+ assert strip_ansi(result) == "abc…"
56
+ assert _GREEN in result
57
+
58
+ def test_ansi_colored_truncated_visible_length_matches_max_width(self):
59
+ colored = f"{_GREEN}abcdefghij{_RESET}"
60
+ result = _visible_truncate(colored, 6)
61
+ assert len(strip_ansi(result)) == 6
62
+
63
+
64
+ class TestBoxLine:
65
+ def test_short_content_pads_to_box_width(self):
66
+ assert _display_width(_box_line("hi", 20)) == 20
67
+
68
+ def test_empty_content_pads_to_box_width(self):
69
+ assert _display_width(_box_line("", 20)) == 20
70
+
71
+ def test_content_fills_inner_exactly(self):
72
+ inner = 20 - 4
73
+ assert _display_width(_box_line("x" * inner, 20)) == 20
74
+
75
+ def test_content_exceeding_inner_truncated_to_box_width(self):
76
+ assert _display_width(_box_line("x" * 200, 20)) == 20
77
+
78
+ def test_truncated_line_contains_ellipsis(self):
79
+ assert "…" in _box_line("x" * 200, 20)
80
+
81
+ def test_truncated_line_has_right_border(self):
82
+ assert _box_line("x" * 200, 20).endswith("│")
83
+
84
+ def test_all_lines_start_with_left_border(self):
85
+ for content in ("", "short", "x" * 200):
86
+ assert _box_line(content, 40).startswith("│")
87
+
88
+ def test_ansi_content_exceeding_inner_truncated_to_box_width(self):
89
+ content = f"{_GREEN}" + "x" * 100 + f"{_RESET}"
90
+ assert _display_width(_box_line(content, 20)) == 20
91
+
92
+
93
+ class TestResultBoxLinesOverflow:
94
+ def _badge_in(self, lines, badge="[PASS]"):
95
+ return any(badge in strip_ansi(line) for line in lines)
96
+
97
+ def _all_widths(self, lines):
98
+ return [_display_width(line) for line in lines]
99
+
100
+ def test_normal_result_all_lines_fit_exactly(self):
101
+ lines = result_box_lines(_result(), _WIDTH)
102
+ assert all(w == _WIDTH for w in self._all_widths(lines))
103
+
104
+ def test_pass_badge_present(self):
105
+ lines = result_box_lines(_result(outcome=ResultOutcome.PASSED), _WIDTH)
106
+ assert self._badge_in(lines, "[PASS]")
107
+
108
+ def test_fail_badge_present(self):
109
+ lines = result_box_lines(_result(outcome=ResultOutcome.FAILED), _WIDTH)
110
+ assert self._badge_in(lines, "[FAIL]")
111
+
112
+ def test_long_action_all_lines_fit(self):
113
+ lines = result_box_lines(_result(action="A" * 300), _WIDTH)
114
+ assert all(w == _WIDTH for w in self._all_widths(lines))
115
+
116
+ def test_long_expected_all_lines_fit(self):
117
+ lines = result_box_lines(_result(expected="E" * 300), _WIDTH)
118
+ assert all(w == _WIDTH for w in self._all_widths(lines))
119
+
120
+ def test_long_actual_all_lines_fit(self):
121
+ lines = result_box_lines(_result(actual="A" * 300), _WIDTH)
122
+ assert all(w == _WIDTH for w in self._all_widths(lines))
123
+
124
+ def test_long_requirements_all_lines_fit(self):
125
+ reqs = ["REQ-" + str(i) for i in range(50)]
126
+ lines = result_box_lines(_result(requirements=reqs), _WIDTH)
127
+ assert all(w == _WIDTH for w in self._all_widths(lines))
128
+
129
+ def test_badge_present_with_all_fields_long(self):
130
+ r = _result(
131
+ action="ACT" * 100,
132
+ expected="EXP" * 100,
133
+ actual="ACT" * 100,
134
+ requirements=["REQ-" + str(i) for i in range(30)],
135
+ )
136
+ lines = result_box_lines(r, _WIDTH)
137
+ assert self._badge_in(lines, "[PASS]")
138
+ assert all(w == _WIDTH for w in self._all_widths(lines))
139
+
140
+ def test_narrow_width_all_lines_fit(self):
141
+ lines = result_box_lines(_result(), 20)
142
+ assert all(w == 20 for w in self._all_widths(lines))
143
+
144
+ def test_narrow_width_badge_present(self):
145
+ lines = result_box_lines(_result(outcome=ResultOutcome.PASSED), 20)
146
+ assert self._badge_in(lines, "[PASS]")
147
+
148
+ def test_last_field_line_too_long_badge_still_present(self):
149
+ # Actual is the last field with no reqs; its last wrapped chunk will be
150
+ # artificially wide enough to trigger the badge-overflow guard.
151
+ r = _result(actual="X" * 300)
152
+ lines = result_box_lines(r, _WIDTH)
153
+ assert self._badge_in(lines, "[PASS]")
154
+ assert all(w == _WIDTH for w in self._all_widths(lines))
155
+
156
+ def test_long_reqs_last_line_badge_still_present(self):
157
+ r = _result(requirements=["REQ-" + "X" * 60])
158
+ lines = result_box_lines(r, _WIDTH)
159
+ assert self._badge_in(lines, "[PASS]")
160
+ assert all(w == _WIDTH for w in self._all_widths(lines))
@@ -0,0 +1,82 @@
1
+ """Tests for environment dataclasses and their from_dict constructors"""
2
+
3
+ from vault88.constructs.environment import Credential, Environment, Host, Service
4
+
5
+
6
+ class TestCredential:
7
+ def test_username_only(self):
8
+ c = Credential.from_dict({"username": "alice"})
9
+ assert c.username == "alice"
10
+ assert c.password is None
11
+ assert c.key_path is None
12
+
13
+ def test_with_password_and_key_path(self):
14
+ c = Credential.from_dict({"username": "bob", "password": "s3cr3t", "key_path": "/id_rsa"})
15
+ assert c.password == "s3cr3t"
16
+ assert c.key_path == "/id_rsa"
17
+
18
+
19
+ class TestService:
20
+ def test_minimal(self):
21
+ s = Service.from_dict({"name": "http"})
22
+ assert s.name == "http"
23
+ assert s.port is None
24
+ assert s.url is None
25
+ assert s.credential is None
26
+
27
+ def test_with_port_url_and_credential(self):
28
+ s = Service.from_dict(
29
+ {
30
+ "name": "api",
31
+ "port": 8080,
32
+ "url": "http://localhost:8080",
33
+ "credential": {"username": "admin"},
34
+ }
35
+ )
36
+ assert s.port == 8080
37
+ assert s.url == "http://localhost:8080"
38
+ assert s.credential is not None
39
+ assert s.credential.username == "admin"
40
+
41
+
42
+ class TestHost:
43
+ def test_minimal(self):
44
+ h = Host.from_dict({"name": "server", "address": "10.0.0.1"})
45
+ assert h.name == "server"
46
+ assert h.address == "10.0.0.1"
47
+ assert h.services == []
48
+ assert h.credential is None
49
+
50
+ def test_with_services_and_credential(self):
51
+ h = Host.from_dict(
52
+ {
53
+ "name": "server",
54
+ "address": "10.0.0.1",
55
+ "services": [{"name": "ssh", "port": 22}],
56
+ "credential": {"username": "root"},
57
+ }
58
+ )
59
+ assert len(h.services) == 1
60
+ assert h.services[0].name == "ssh"
61
+ assert h.credential.username == "root"
62
+
63
+
64
+ class TestEnvironment:
65
+ def test_minimal(self):
66
+ e = Environment.from_dict({"name": "staging"})
67
+ assert e.name == "staging"
68
+ assert e.hosts == []
69
+
70
+ def test_with_hosts(self):
71
+ e = Environment.from_dict(
72
+ {
73
+ "name": "prod",
74
+ "hosts": [
75
+ {"name": "web1", "address": "192.168.0.1"},
76
+ {"name": "db1", "address": "192.168.0.2"},
77
+ ],
78
+ }
79
+ )
80
+ assert len(e.hosts) == 2
81
+ assert e.hosts[0].name == "web1"
82
+ assert e.hosts[1].name == "db1"