socx-cli 0.13.0__tar.gz → 0.13.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 (113) hide show
  1. {socx_cli-0.13.0 → socx_cli-0.13.2}/PKG-INFO +3 -2
  2. {socx_cli-0.13.0 → socx_cli-0.13.2}/pyproject.toml +6 -2
  3. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/__init__.py +8 -6
  4. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/_paths.py +1 -1
  5. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/patterns/__init__.py +2 -2
  6. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/patterns/visitor/__init__.py +2 -2
  7. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/patterns/visitor/protocol.py +13 -8
  8. socx_cli-0.13.2/socx/patterns/visitor/traversal.py +64 -0
  9. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/regression/__init__.py +2 -0
  10. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/regression/progress.py +2 -2
  11. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/regression/regression.py +85 -62
  12. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/regression/test.py +35 -32
  13. socx_cli-0.13.2/socx/regression/visitor.py +25 -0
  14. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/static/settings/regression.yaml +1 -1
  15. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/regression/_run.py +79 -68
  16. socx_cli-0.13.0/socx/patterns/visitor/traversal.py +0 -49
  17. {socx_cli-0.13.0 → socx_cli-0.13.2}/.gitignore +0 -0
  18. {socx_cli-0.13.0 → socx_cli-0.13.2}/LICENSE +0 -0
  19. {socx_cli-0.13.0 → socx_cli-0.13.2}/README.md +0 -0
  20. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/__main__.py +0 -0
  21. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/cli/__init__.py +0 -0
  22. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/cli/_cli.py +0 -0
  23. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/cli/_jinja.py +0 -0
  24. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/cli/callbacks.py +0 -0
  25. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/cli/cfg.py +0 -0
  26. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/cli/cli.py +0 -0
  27. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/cli/params.py +0 -0
  28. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/cli/types.py +0 -0
  29. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/config/__init__.py +0 -0
  30. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/config/_config.py +0 -0
  31. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/config/_settings.py +0 -0
  32. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/config/converters.py +0 -0
  33. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/config/encoders.py +0 -0
  34. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/config/formatters.py +0 -0
  35. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/config/serializers.py +0 -0
  36. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/config/validators.py +0 -0
  37. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/__init__.py +0 -0
  38. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/encoder.py +0 -0
  39. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/enums.py +0 -0
  40. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/funcs.py +0 -0
  41. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/metadata.py +0 -0
  42. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/paths.py +0 -0
  43. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/schema/__init__.py +0 -0
  44. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/schema/git/__init__.py +0 -0
  45. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/schema/git/git.py +0 -0
  46. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/schema/git/manifest.py +0 -0
  47. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/schema/plugin.py +0 -0
  48. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/schema/types.py +0 -0
  49. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/core/serializer.py +0 -0
  50. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/git/__init__.py +0 -0
  51. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/git/_git.py +0 -0
  52. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/git/_manifest.py +0 -0
  53. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/git/_ssh.py +0 -0
  54. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/io/__init__.py +0 -0
  55. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/io/console.py +0 -0
  56. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/io/decorators.py +0 -0
  57. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/io/log.py +0 -0
  58. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/patterns/mixins/__init__.py +0 -0
  59. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/patterns/mixins/proxy.py +0 -0
  60. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/patterns/mixins/uid.py +0 -0
  61. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/patterns/singleton/__init__.py +0 -0
  62. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/patterns/singleton/singleton.py +0 -0
  63. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/regression/status.py +0 -0
  64. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/regression/validator.py +0 -0
  65. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/static/settings/cli.yaml +0 -0
  66. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/static/settings/console.yaml +0 -0
  67. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/static/settings/git.yaml +0 -0
  68. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/static/settings/plugins.yaml +0 -0
  69. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/static/settings/rich_click.yaml +0 -0
  70. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/static/settings/settings.yaml +0 -0
  71. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/static/sql/socx.sql +0 -0
  72. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/utils/__init__.py +0 -0
  73. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx/utils/decorators.py +0 -0
  74. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/config/__init__.py +0 -0
  75. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/config/_config.py +0 -0
  76. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/config/edit.py +0 -0
  77. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/git/__init__.py +0 -0
  78. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/git/arguments.py +0 -0
  79. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/git/callbacks.py +0 -0
  80. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/git/cli.py +0 -0
  81. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/git/manifest.py +0 -0
  82. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/git/renderables.py +0 -0
  83. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/git/summary.py +0 -0
  84. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/git/utils.py +0 -0
  85. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/plugin/__init__.py +0 -0
  86. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/plugin/example.py +0 -0
  87. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/plugin/schema.py +0 -0
  88. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/regression/__init__.py +0 -0
  89. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/regression/callbacks.py +0 -0
  90. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/regression/cli.py +0 -0
  91. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/regression/run.py +0 -0
  92. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/regression/tui.py +0 -0
  93. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/version/__init__.py +0 -0
  94. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_plugins/version/__main__.py +0 -0
  95. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/__init__.py +0 -0
  96. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/__init__.py +0 -0
  97. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/__main__.py +0 -0
  98. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/app.py +0 -0
  99. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/bindings/__init__.py +0 -0
  100. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/bindings/vim/__init__.py +0 -0
  101. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/bindings/vim/mode.py +0 -0
  102. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/bindings/vim/vim.py +0 -0
  103. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/containers.py +0 -0
  104. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/details.py +0 -0
  105. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/dialog.py +0 -0
  106. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/mixins/__init__.py +0 -0
  107. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/mixins/composable.py +0 -0
  108. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/preview.py +0 -0
  109. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/table.py +0 -0
  110. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/tree.py +0 -0
  111. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/regression/widget.py +0 -0
  112. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/static/tcss/regression/app.tcss +0 -0
  113. {socx_cli-0.13.0 → socx_cli-0.13.2}/socx_tui/static/tcss/regression/preview.tcss +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socx-cli
3
- Version: 0.13.0
3
+ Version: 0.13.2
4
4
  Summary: System on chip verification and tooling infrastructure.
5
5
  Project-URL: Issues, https://github.com/sagikimhi/socx-cli/issues
6
6
  Project-URL: Homepage, https://sagikimhi.dev/socx-cli
@@ -28,9 +28,10 @@ Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA
28
28
  Classifier: Topic :: Software Development
29
29
  Classifier: Topic :: Utilities
30
30
  Requires-Python: >=3.12
31
+ Requires-Dist: anyio>=4.12.1
31
32
  Requires-Dist: click
32
33
  Requires-Dist: copier~=9.11
33
- Requires-Dist: dynaconf
34
+ Requires-Dist: dynaconf<3.2.12
34
35
  Requires-Dist: gitpython
35
36
  Requires-Dist: hoptex
36
37
  Requires-Dist: jinja2
@@ -29,7 +29,7 @@ socx = 'socx.__main__:main'
29
29
  [project]
30
30
  name = "socx-cli"
31
31
  readme = "README.md"
32
- version = "0.13.0"
32
+ version = "0.13.2"
33
33
  license = "Apache-2.0"
34
34
  authors = [{ name = "Sagi Kimhi", email = "sagi.kim5@gmail.com" }]
35
35
  maintainers = [{ name = "Sagi Kimhi", email = "sagi.kim5@gmail.com" }]
@@ -67,7 +67,7 @@ dependencies = [
67
67
  "textual",
68
68
  "plumbum",
69
69
  "pydantic",
70
- "dynaconf",
70
+ "dynaconf<3.2.12",
71
71
  "paramiko",
72
72
  "werkzeug>=3.1.4",
73
73
  "gitpython",
@@ -81,6 +81,7 @@ dependencies = [
81
81
  "pydantic-extra-types>=2.11.0",
82
82
  "pydantic-settings>=2.12.0",
83
83
  "sqlmodel>=0.0.31",
84
+ "anyio>=4.12.1",
84
85
  ]
85
86
 
86
87
  [project.urls]
@@ -164,6 +165,9 @@ packages = ["src/socx", "src/socx_tui", "src/socx_plugins"]
164
165
  [tool.hatch.build.targets.wheel]
165
166
  packages = ["src/socx", "src/socx_tui", "src/socx_plugins"]
166
167
 
168
+ [tool.hatch.build.targets.binary]
169
+ packages = ["src/socx", "src/socx_tui", "src/socx_plugins"]
170
+
167
171
  # -----------------------------------------------------------------------------
168
172
  # Tool Configurations - pytest
169
173
  # -----------------------------------------------------------------------------
@@ -123,9 +123,9 @@ __all__ = (
123
123
  # Patterns
124
124
  "Node",
125
125
  "Visitor",
126
- "Structure",
127
126
  "Traversal",
128
127
  "Singleton",
128
+ "StructureProxy",
129
129
  "ByLevelTraversal",
130
130
  "TopDownTraversal",
131
131
  "BottomUpTraversal",
@@ -135,6 +135,7 @@ __all__ = (
135
135
  "Regression",
136
136
  "TestStatus",
137
137
  "TestResult",
138
+ "RegressionTree",
138
139
  "RegressionProgress",
139
140
  )
140
141
 
@@ -248,21 +249,22 @@ from socx.config import validate_all as validate_all
248
249
 
249
250
  from socx.patterns import Node as Node
250
251
  from socx.patterns import Visitor as Visitor
251
- from socx.patterns import Structure as Structure
252
252
  from socx.patterns import Traversal as Traversal
253
- from socx.patterns import UIDMixin as UIDMixin
254
- from socx.patterns import PtrMixin as PtrMixin
255
- from socx.patterns import ProxyMixin as ProxyMixin
256
- from socx.patterns import Singleton as Singleton
253
+ from socx.patterns import StructureProxy as StructureProxy
257
254
  from socx.patterns import TopDownTraversal as TopDownTraversal
258
255
  from socx.patterns import BottomUpTraversal as BottomUpTraversal
259
256
  from socx.patterns import ByLevelTraversal as ByLevelTraversal
257
+ from socx.patterns import UIDMixin as UIDMixin
258
+ from socx.patterns import PtrMixin as PtrMixin
259
+ from socx.patterns import Singleton as Singleton
260
+ from socx.patterns import ProxyMixin as ProxyMixin
260
261
 
261
262
  from socx.regression import Test as Test
262
263
  from socx.regression import TestBase as TestBase
263
264
  from socx.regression import Regression as Regression
264
265
  from socx.regression import TestStatus as TestStatus
265
266
  from socx.regression import TestResult as TestResult
267
+ from socx.regression import RegressionTree as RegressionTree
266
268
  from socx.regression import RegressionProgress as RegressionProgress
267
269
 
268
270
  from socx.cli import FuncType as FuncType
@@ -55,7 +55,7 @@ def find_project_root(start_path: Path) -> Path:
55
55
  # Application Paths
56
56
  # -----------------------------------------------------------------------------
57
57
 
58
- APP_ROOT_DIR: Path = Path(getfile(lambda: None)).parents[3].resolve()
58
+ APP_ROOT_DIR: Path = Path(getfile(lambda: None)).parents[1].resolve()
59
59
  """Absolute path to directory where application is installed."""
60
60
 
61
61
  APP_STATIC_DIR: Path = __directory__ / "static"
@@ -6,7 +6,7 @@ __all__ = (
6
6
  "PtrMixin",
7
7
  "UIDMixin",
8
8
  "ProxyMixin",
9
- "Structure",
9
+ "StructureProxy",
10
10
  "Traversal",
11
11
  "Singleton",
12
12
  "ByLevelTraversal",
@@ -21,8 +21,8 @@ from socx.patterns.mixins import ProxyMixin as ProxyMixin
21
21
 
22
22
  from socx.patterns.visitor import Node as Node
23
23
  from socx.patterns.visitor import Visitor as Visitor
24
- from socx.patterns.visitor import Structure as Structure
25
24
  from socx.patterns.visitor import Traversal as Traversal
25
+ from socx.patterns.visitor import StructureProxy as StructureProxy
26
26
  from socx.patterns.visitor import ByLevelTraversal as ByLevelTraversal
27
27
  from socx.patterns.visitor import TopDownTraversal as TopDownTraversal
28
28
  from socx.patterns.visitor import BottomUpTraversal as BottomUpTraversal
@@ -3,7 +3,7 @@
3
3
  __all__ = (
4
4
  "Node",
5
5
  "Visitor",
6
- "Structure",
6
+ "StructureProxy",
7
7
  "Traversal",
8
8
  "ByLevelTraversal",
9
9
  "TopDownTraversal",
@@ -13,8 +13,8 @@ __all__ = (
13
13
 
14
14
  from socx.patterns.visitor.protocol import Node as Node
15
15
  from socx.patterns.visitor.protocol import Visitor as Visitor
16
- from socx.patterns.visitor.protocol import Structure as Structure
17
16
  from socx.patterns.visitor.protocol import Traversal as Traversal
17
+ from socx.patterns.visitor.protocol import StructureProxy as StructureProxy
18
18
 
19
19
  from socx.patterns.visitor.traversal import (
20
20
  ByLevelTraversal as ByLevelTraversal,
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from typing import Protocol
6
- from collections.abc import Iterable
6
+ from collections.abc import Generator
7
7
 
8
8
 
9
9
  class Visitor[NODE](Protocol):
@@ -11,7 +11,7 @@ class Visitor[NODE](Protocol):
11
11
 
12
12
  __slots__ = ()
13
13
 
14
- def visit(self, n: NODE) -> None:
14
+ def visit(self, node: NODE) -> None:
15
15
  """Visit a node of a structure."""
16
16
  ...
17
17
 
@@ -21,19 +21,19 @@ class Node[NODE](Protocol):
21
21
 
22
22
  __slots__ = ()
23
23
 
24
- def accept(self, v: Visitor[NODE]) -> None:
24
+ def accept(self, visitor: Visitor[NODE]) -> None:
25
25
  """Accept a visit from a `Visitor`."""
26
26
  ...
27
27
 
28
28
 
29
- class Structure[NODE](Protocol):
29
+ class StructureProxy[STRUCTURE](Protocol):
30
30
  """Protocol for structures exposing child relationships."""
31
31
 
32
32
  __slots__ = ()
33
33
 
34
34
  @classmethod
35
- def children(cls, s: NODE) -> Iterable[NODE]:
36
- """Return the immediate children of ``s``."""
35
+ def children(cls, structure: STRUCTURE) -> Generator[STRUCTURE]:
36
+ """Return the immediate children of ``structure``."""
37
37
  ...
38
38
 
39
39
 
@@ -43,6 +43,11 @@ class Traversal[NODE](Protocol):
43
43
  __slots__ = ()
44
44
 
45
45
  @classmethod
46
- def accept(cls, n: NODE, v: Visitor[NODE], p: Structure[NODE]) -> None:
47
- """Accept visits of a `NODE` n from a `Visitor` v."""
46
+ def accept(
47
+ cls,
48
+ structure: NODE,
49
+ visitor: Visitor[NODE],
50
+ proxy: StructureProxy[NODE],
51
+ ) -> None:
52
+ """Accept visits of a `NODE` node from a `Visitor` visitor."""
48
53
  ...
@@ -0,0 +1,64 @@
1
+ """Traversal strategies compatible with the visitor protocol."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from socx.patterns.visitor.protocol import Visitor
6
+ from socx.patterns.visitor.protocol import StructureProxy
7
+ from socx.patterns.visitor.protocol import Traversal
8
+
9
+
10
+ class TopDownTraversal[NODE](Traversal[NODE]):
11
+ """Pre-order traversal that visits parents before descendants."""
12
+
13
+ @classmethod
14
+ def accept(
15
+ cls,
16
+ structure: NODE,
17
+ visitor: Visitor[NODE],
18
+ proxy: StructureProxy[NODE],
19
+ ) -> None:
20
+ """Visit ``structure`` before recursively traversing its children."""
21
+ visitor.visit(structure)
22
+
23
+ for c in proxy.children(structure):
24
+ cls.accept(c, visitor, proxy)
25
+
26
+
27
+ class BottomUpTraversal[NODE](Traversal[NODE]):
28
+ """Post-order traversal that visits descendants before parents."""
29
+
30
+ @classmethod
31
+ def accept(
32
+ cls,
33
+ structure: NODE,
34
+ visitor: Visitor[NODE],
35
+ proxy: StructureProxy[NODE],
36
+ ) -> None:
37
+ """Traverse child subtrees prior to visiting ``structure``."""
38
+ for c in proxy.children(structure):
39
+ cls.accept(c, visitor, proxy)
40
+
41
+ visitor.visit(structure)
42
+
43
+
44
+ class ByLevelTraversal[NODE](Traversal[NODE]):
45
+ """Breadth-first traversal that visits nodes one level at a time."""
46
+
47
+ @classmethod
48
+ def accept(
49
+ cls,
50
+ structure: NODE,
51
+ visitor: Visitor[NODE],
52
+ proxy: StructureProxy[NODE],
53
+ ) -> None:
54
+ """Walk the structure level-by-level starting from ``structure``."""
55
+ q: list[NODE] = [structure]
56
+
57
+ while q:
58
+ t: list[NODE] = []
59
+
60
+ for n_ in q:
61
+ visitor.visit(n_)
62
+ t.extend(proxy.children(n_))
63
+
64
+ q = t
@@ -6,6 +6,7 @@ __all__ = (
6
6
  "TestStatus",
7
7
  "TestResult",
8
8
  "Regression",
9
+ "RegressionTree",
9
10
  "RegressionProgress",
10
11
  )
11
12
 
@@ -14,4 +15,5 @@ from socx.regression.test import TestBase as TestBase
14
15
  from socx.regression.test import TestStatus as TestStatus
15
16
  from socx.regression.test import TestResult as TestResult
16
17
  from socx.regression.regression import Regression as Regression
18
+ from socx.regression.regression import RegressionTree as RegressionTree
17
19
  from socx.regression.progress import RegressionProgress as RegressionProgress
@@ -126,7 +126,7 @@ class RegressionProgress:
126
126
  prev_status = status
127
127
  prev_finished = finished
128
128
  status = regression.status
129
- finished = await self._count_statuses(
129
+ finished = self._count_statuses(
130
130
  regression,
131
131
  TestStatus.Finished,
132
132
  TestStatus.Terminated,
@@ -167,7 +167,7 @@ class RegressionProgress:
167
167
  description=description,
168
168
  )
169
169
 
170
- async def _count_statuses(
170
+ def _count_statuses(
171
171
  self, regression: Regression, *statuses: TestStatus
172
172
  ) -> int:
173
173
  with self.regression.lock:
@@ -10,15 +10,20 @@ from collections import OrderedDict
10
10
  from collections.abc import AsyncGenerator, Iterable
11
11
  from pathlib import Path
12
12
  from threading import RLock
13
- from typing import Self, Any
13
+ from typing import Self, Any, Annotated
14
14
 
15
+ import box
15
16
  from pydantic import (
17
+ Field,
16
18
  ConfigDict,
19
+ BaseModel,
17
20
  PrivateAttr,
18
21
  TypeAdapter,
19
22
  UUID4,
20
23
  computed_field,
21
24
  validate_call,
25
+ SerializeAsAny,
26
+ PlainValidator,
22
27
  )
23
28
 
24
29
  from socx.config import settings
@@ -28,26 +33,28 @@ from socx.regression.test import Test, TestBase, TestResult, TestStatus
28
33
 
29
34
  logger = logging.getLogger(__name__)
30
35
 
36
+ semaphore = anyio.Semaphore(max(1, settings.regression.max_runs_in_parallel))
37
+
31
38
 
32
39
  class Regression(TestBase):
33
40
  """Manage and execute a collection of tests with concurrency control."""
34
41
 
42
+ test_map: OrderedDict[UUID4, SerializeAsAny[TestBase]] = Field(
43
+ default_factory=OrderedDict, repr=True, title="Test Map"
44
+ )
35
45
  model_config = ConfigDict(
46
+ title="Regression",
36
47
  from_attributes=True,
37
48
  arbitrary_types_allowed=True,
38
49
  )
39
-
40
50
  _lock: RLock = PrivateAttr(default_factory=RLock)
41
51
  _done: aio.Queue[TestBase] = PrivateAttr(default_factory=aio.Queue)
42
- _pending: aio.Queue[TestBase | Regression | None] = PrivateAttr(
52
+ _pending: aio.Queue[TestBase | None] = PrivateAttr(
43
53
  default_factory=aio.Queue
44
54
  )
45
55
  _running: set[UUID4] = PrivateAttr(default_factory=set)
46
- _test_map: OrderedDict[UUID4, TestBase] = PrivateAttr(
47
- default_factory=OrderedDict
48
- )
49
- _stop_requested: bool = PrivateAttr(default=False)
50
56
  _pause_event: aio.Event = PrivateAttr(default_factory=aio.Event)
57
+ _stop_requested: bool = PrivateAttr(default=False)
51
58
 
52
59
  def __init__(
53
60
  self,
@@ -58,10 +65,9 @@ class Regression(TestBase):
58
65
  **kwargs: Any,
59
66
  ) -> None:
60
67
  super().__init__(*args, name=name, **kwargs)
61
- tests = tests or []
62
68
  test_map = test_map or {}
63
- self._test_map = OrderedDict()
64
- self.tests = [*tests, *list(test_map.values())]
69
+ tests = [*list(test_map.values()), *(tests or [])]
70
+ self.test_map = OrderedDict({test.id: test for test in tests})
65
71
 
66
72
  @classmethod
67
73
  @validate_call()
@@ -74,7 +80,7 @@ class Regression(TestBase):
74
80
  ) -> Self:
75
81
  return cls._from_file(path, test_cls=test_cls, **kwargs)
76
82
 
77
- @computed_field(repr=True)
83
+ @computed_field
78
84
  @property
79
85
  def result(self) -> TestResult:
80
86
  with self.lock:
@@ -94,7 +100,7 @@ class Regression(TestBase):
94
100
  else:
95
101
  return super().result
96
102
 
97
- @computed_field(repr=True)
103
+ @computed_field
98
104
  @property
99
105
  def status(self) -> TestStatus:
100
106
  terminated_statuses = [
@@ -102,50 +108,36 @@ class Regression(TestBase):
102
108
  TestStatus.Terminated,
103
109
  TestStatus.Finished,
104
110
  ]
105
- running_statuses = [TestStatus.Pending, TestStatus.Running]
111
+ running_statuses = [TestStatus.Running]
106
112
  tests = self.tests
107
113
  if all(test.status is TestStatus.Idle for test in tests):
108
114
  return TestStatus.Idle
109
115
  if all(test.status is TestStatus.Finished for test in tests):
110
116
  return TestStatus.Finished
111
- if all(test.status in terminated_statuses for test in tests):
112
- return TestStatus.Terminated
113
117
  if any(test.status in running_statuses for test in tests):
114
118
  return TestStatus.Running
115
- return TestStatus.Paused
119
+ if any(test.status is TestStatus.Paused for test in tests):
120
+ return TestStatus.Paused
121
+ if all(test.status in terminated_statuses for test in tests):
122
+ return TestStatus.Terminated
123
+ return TestStatus.Pending
116
124
 
117
125
  @computed_field
118
126
  @property
119
127
  def tests(self) -> list[TestBase]:
120
128
  with self.lock:
121
- return list(self._test_map.values())
129
+ return list(self.test_map.values())
122
130
 
123
131
  @tests.setter
124
132
  def tests(self, other: list[TestBase]) -> None:
125
133
  with self.lock:
126
- self._test_map = OrderedDict({test.id: test for test in other})
127
-
128
- @computed_field(repr=True)
129
- @property
130
- def test_map(self) -> dict[UUID4, TestBase]:
131
- with self.lock:
132
- return self._test_map
133
-
134
- @test_map.setter
135
- def test_map(self, other: dict[UUID4, TestBase]) -> None:
136
- with self.lock:
137
- if isinstance(other, OrderedDict):
138
- self._test_map = other
139
- else:
140
- self._test_map = OrderedDict(other)
134
+ self.test_map = OrderedDict({test.id: test for test in other})
141
135
 
142
- @computed_field(repr=True)
136
+ @computed_field
143
137
  @property
144
138
  def run_limit(self) -> int:
145
139
  """Return the maximum number of tests that may run concurrently."""
146
- with self.lock:
147
- run_limit = settings.regression.max_runs_in_parallel
148
- return max(1, min(run_limit, len(self) or 1))
140
+ return int(max(1, int(settings.regression.max_runs_in_parallel)))
149
141
 
150
142
  @property
151
143
  def lock(self) -> RLock:
@@ -275,13 +267,13 @@ class Regression(TestBase):
275
267
  if test.status in (TestStatus.Finished, TestStatus.Terminated):
276
268
  test.reset()
277
269
  await self.pending.put(test)
278
-
279
270
  for _ in range(self.run_limit):
280
271
  await self.pending.put(None)
281
272
 
282
273
  async def _runner(self) -> None:
283
274
  """Consume queued tests and execute them sequentially."""
284
275
  while True:
276
+ await semaphore.acquire()
285
277
  test = await self.pending.get()
286
278
  try:
287
279
  if test is None:
@@ -294,13 +286,28 @@ class Regression(TestBase):
294
286
  return
295
287
 
296
288
  self._running.add(test.id)
297
- if test is not None:
298
- await test.start()
289
+ await test.start()
299
290
  await self.done.put(test)
300
291
  finally:
301
292
  if test is not None:
302
293
  self._running.discard(test.id)
303
294
  self.pending.task_done()
295
+ semaphore.release()
296
+
297
+ def dump_state(self, output_dir: Path) -> None:
298
+ """Write the regression command results to their respective files."""
299
+ logger.info("saving regression state and results to disk...")
300
+ file = output_dir / self.name / "state.yaml"
301
+ state = self.model_dump(
302
+ mode="json",
303
+ round_trip=True,
304
+ serialize_as_any=True,
305
+ include={"result", "status"},
306
+ )
307
+ file.parent.mkdir(parents=True, exist_ok=True)
308
+ file.touch(exist_ok=False)
309
+ box.DDBox(state).to_yaml(str(file))
310
+ logger.info(f"state and results saved to: '{output_dir}'.")
304
311
 
305
312
  def _active_tests(self) -> list[TestBase]:
306
313
  return [test for test in self.tests if test.id in self._running]
@@ -359,26 +366,42 @@ class Regression(TestBase):
359
366
  regressions.append(regression)
360
367
  return cls(name=name, tests=regressions)
361
368
 
362
- # @field_validator("test_map", mode="before")
363
- # @classmethod
364
- # def _test_map_validator(
365
- # cls,
366
- # tests: set[TestBase]
367
- # | list[TestBase]
368
- # | tuple[TestBase, ...]
369
- # | dict[str, TestBase],
370
- # ) -> OrderedDict[UUID4, TestBase]:
371
- # if tests is None:
372
- # err = "must not be none"
373
- # raise ValueError(err)
374
-
375
- # rv = OrderedDict()
376
- # it: Iterator[TestBase] = (
377
- # iter(list(tests.values()))
378
- # if isinstance(tests, dict)
379
- # else iter(tests)
380
- # )
381
-
382
- # for test in it:
383
- # rv[test.id] = test
384
- # return rv
369
+
370
+ TreeNode = Annotated[
371
+ TestBase,
372
+ PlainValidator(lambda x: x, TestBase),
373
+ SerializeAsAny[Test | Regression],
374
+ ]
375
+
376
+
377
+ class RegressionTree(BaseModel):
378
+ root_node: TreeNode = Field(..., title="Root Node", repr=True)
379
+ model_config = ConfigDict(
380
+ from_attributes=True,
381
+ arbitrary_types_allowed=True,
382
+ )
383
+
384
+
385
+ # @field_validator("test_map", mode="before")
386
+ # @classmethod
387
+ # def _test_map_validator(
388
+ # cls,
389
+ # tests: set[TestBase]
390
+ # | list[TestBase]
391
+ # | tuple[TestBase, ...]
392
+ # | dict[str, TestBase],
393
+ # ) -> OrderedDict[UUID4, TestBase]:
394
+ # if tests is None:
395
+ # err = "must not be none"
396
+ # raise ValueError(err)
397
+
398
+ # rv = OrderedDict()
399
+ # it: Iterator[TestBase] = (
400
+ # iter(list(tests.values()))
401
+ # if isinstance(tests, dict)
402
+ # else iter(tests)
403
+ # )
404
+
405
+ # for test in it:
406
+ # rv[test.id] = test
407
+ # return rv