julee 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl
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.
- julee/__init__.py +1 -1
- julee/api/tests/routers/test_assembly_specifications.py +2 -0
- julee/api/tests/routers/test_documents.py +2 -0
- julee/api/tests/routers/test_knowledge_service_configs.py +2 -0
- julee/api/tests/routers/test_knowledge_service_queries.py +2 -0
- julee/api/tests/routers/test_system.py +2 -0
- julee/api/tests/routers/test_workflows.py +2 -0
- julee/api/tests/test_app.py +2 -0
- julee/api/tests/test_dependencies.py +2 -0
- julee/api/tests/test_requests.py +2 -0
- julee/contrib/polling/__init__.py +22 -19
- julee/contrib/polling/apps/__init__.py +17 -0
- julee/contrib/polling/apps/worker/__init__.py +17 -0
- julee/contrib/polling/apps/worker/pipelines.py +288 -0
- julee/contrib/polling/domain/__init__.py +7 -9
- julee/contrib/polling/domain/models/__init__.py +6 -7
- julee/contrib/polling/domain/models/polling_config.py +18 -1
- julee/contrib/polling/domain/services/__init__.py +6 -5
- julee/contrib/polling/domain/services/poller.py +1 -1
- julee/contrib/polling/infrastructure/__init__.py +9 -8
- julee/contrib/polling/infrastructure/services/__init__.py +6 -5
- julee/contrib/polling/infrastructure/services/polling/__init__.py +6 -5
- julee/contrib/polling/infrastructure/services/polling/http/__init__.py +6 -5
- julee/contrib/polling/infrastructure/services/polling/http/http_poller_service.py +5 -2
- julee/contrib/polling/infrastructure/temporal/__init__.py +12 -12
- julee/contrib/polling/infrastructure/temporal/activities.py +1 -1
- julee/contrib/polling/infrastructure/temporal/manager.py +291 -0
- julee/contrib/polling/infrastructure/temporal/proxies.py +1 -1
- julee/contrib/polling/tests/unit/apps/worker/test_pipelines.py +580 -0
- julee/contrib/polling/tests/unit/infrastructure/services/polling/http/test_http_poller_service.py +40 -2
- julee/contrib/polling/tests/unit/infrastructure/temporal/__init__.py +7 -0
- julee/contrib/polling/tests/unit/infrastructure/temporal/test_manager.py +475 -0
- julee/docs/sphinx_hcd/__init__.py +146 -13
- julee/docs/sphinx_hcd/domain/__init__.py +5 -0
- julee/docs/sphinx_hcd/domain/models/__init__.py +32 -0
- julee/docs/sphinx_hcd/domain/models/accelerator.py +152 -0
- julee/docs/sphinx_hcd/domain/models/app.py +151 -0
- julee/docs/sphinx_hcd/domain/models/code_info.py +121 -0
- julee/docs/sphinx_hcd/domain/models/epic.py +79 -0
- julee/docs/sphinx_hcd/domain/models/integration.py +230 -0
- julee/docs/sphinx_hcd/domain/models/journey.py +222 -0
- julee/docs/sphinx_hcd/domain/models/persona.py +106 -0
- julee/docs/sphinx_hcd/domain/models/story.py +128 -0
- julee/docs/sphinx_hcd/domain/repositories/__init__.py +25 -0
- julee/docs/sphinx_hcd/domain/repositories/accelerator.py +98 -0
- julee/docs/sphinx_hcd/domain/repositories/app.py +57 -0
- julee/docs/sphinx_hcd/domain/repositories/base.py +89 -0
- julee/docs/sphinx_hcd/domain/repositories/code_info.py +69 -0
- julee/docs/sphinx_hcd/domain/repositories/epic.py +62 -0
- julee/docs/sphinx_hcd/domain/repositories/integration.py +79 -0
- julee/docs/sphinx_hcd/domain/repositories/journey.py +106 -0
- julee/docs/sphinx_hcd/domain/repositories/story.py +68 -0
- julee/docs/sphinx_hcd/domain/use_cases/__init__.py +64 -0
- julee/docs/sphinx_hcd/domain/use_cases/derive_personas.py +166 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_accelerator_references.py +236 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_app_references.py +144 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_story_references.py +121 -0
- julee/docs/sphinx_hcd/parsers/__init__.py +48 -0
- julee/docs/sphinx_hcd/parsers/ast.py +150 -0
- julee/docs/sphinx_hcd/parsers/gherkin.py +155 -0
- julee/docs/sphinx_hcd/parsers/yaml.py +184 -0
- julee/docs/sphinx_hcd/repositories/__init__.py +4 -0
- julee/docs/sphinx_hcd/repositories/memory/__init__.py +25 -0
- julee/docs/sphinx_hcd/repositories/memory/accelerator.py +86 -0
- julee/docs/sphinx_hcd/repositories/memory/app.py +45 -0
- julee/docs/sphinx_hcd/repositories/memory/base.py +106 -0
- julee/docs/sphinx_hcd/repositories/memory/code_info.py +59 -0
- julee/docs/sphinx_hcd/repositories/memory/epic.py +54 -0
- julee/docs/sphinx_hcd/repositories/memory/integration.py +70 -0
- julee/docs/sphinx_hcd/repositories/memory/journey.py +96 -0
- julee/docs/sphinx_hcd/repositories/memory/story.py +63 -0
- julee/docs/sphinx_hcd/sphinx/__init__.py +28 -0
- julee/docs/sphinx_hcd/sphinx/adapters.py +116 -0
- julee/docs/sphinx_hcd/sphinx/context.py +163 -0
- julee/docs/sphinx_hcd/sphinx/directives/__init__.py +160 -0
- julee/docs/sphinx_hcd/sphinx/directives/accelerator.py +576 -0
- julee/docs/sphinx_hcd/sphinx/directives/app.py +349 -0
- julee/docs/sphinx_hcd/sphinx/directives/base.py +211 -0
- julee/docs/sphinx_hcd/sphinx/directives/epic.py +434 -0
- julee/docs/sphinx_hcd/sphinx/directives/integration.py +220 -0
- julee/docs/sphinx_hcd/sphinx/directives/journey.py +642 -0
- julee/docs/sphinx_hcd/sphinx/directives/persona.py +345 -0
- julee/docs/sphinx_hcd/sphinx/directives/story.py +575 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/__init__.py +16 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/builder_inited.py +31 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_read.py +27 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_resolved.py +43 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/env_purge_doc.py +42 -0
- julee/docs/sphinx_hcd/sphinx/initialization.py +139 -0
- julee/docs/sphinx_hcd/tests/__init__.py +9 -0
- julee/docs/sphinx_hcd/tests/conftest.py +6 -0
- julee/docs/sphinx_hcd/tests/domain/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/models/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_accelerator.py +266 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_app.py +258 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_code_info.py +231 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_epic.py +163 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_integration.py +327 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_journey.py +249 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_persona.py +172 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_story.py +216 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_derive_personas.py +314 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_accelerator_references.py +476 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_app_references.py +265 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_story_references.py +229 -0
- julee/docs/sphinx_hcd/tests/integration/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/parsers/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/parsers/test_ast.py +298 -0
- julee/docs/sphinx_hcd/tests/parsers/test_gherkin.py +282 -0
- julee/docs/sphinx_hcd/tests/parsers/test_yaml.py +496 -0
- julee/docs/sphinx_hcd/tests/repositories/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/repositories/test_accelerator.py +298 -0
- julee/docs/sphinx_hcd/tests/repositories/test_app.py +218 -0
- julee/docs/sphinx_hcd/tests/repositories/test_base.py +151 -0
- julee/docs/sphinx_hcd/tests/repositories/test_code_info.py +253 -0
- julee/docs/sphinx_hcd/tests/repositories/test_epic.py +237 -0
- julee/docs/sphinx_hcd/tests/repositories/test_integration.py +268 -0
- julee/docs/sphinx_hcd/tests/repositories/test_journey.py +294 -0
- julee/docs/sphinx_hcd/tests/repositories/test_story.py +236 -0
- julee/docs/sphinx_hcd/tests/sphinx/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/sphinx/directives/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/sphinx/directives/test_base.py +160 -0
- julee/docs/sphinx_hcd/tests/sphinx/test_adapters.py +176 -0
- julee/docs/sphinx_hcd/tests/sphinx/test_context.py +257 -0
- julee/domain/models/assembly/tests/test_assembly.py +2 -0
- julee/domain/models/assembly_specification/tests/test_assembly_specification.py +2 -0
- julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +2 -0
- julee/domain/models/custom_fields/tests/test_custom_fields.py +2 -0
- julee/domain/models/document/tests/test_document.py +2 -0
- julee/domain/models/policy/tests/test_document_policy_validation.py +2 -0
- julee/domain/models/policy/tests/test_policy.py +2 -0
- julee/domain/use_cases/tests/test_extract_assemble_data.py +2 -0
- julee/domain/use_cases/tests/test_initialize_system_data.py +2 -0
- julee/domain/use_cases/tests/test_validate_document.py +2 -0
- julee/maintenance/release.py +10 -5
- julee/repositories/memory/tests/test_document.py +2 -0
- julee/repositories/memory/tests/test_document_policy_validation.py +2 -0
- julee/repositories/memory/tests/test_policy.py +2 -0
- julee/repositories/minio/tests/test_assembly.py +2 -0
- julee/repositories/minio/tests/test_assembly_specification.py +2 -0
- julee/repositories/minio/tests/test_client_protocol.py +3 -0
- julee/repositories/minio/tests/test_document.py +2 -0
- julee/repositories/minio/tests/test_document_policy_validation.py +2 -0
- julee/repositories/minio/tests/test_knowledge_service_config.py +2 -0
- julee/repositories/minio/tests/test_knowledge_service_query.py +2 -0
- julee/repositories/minio/tests/test_policy.py +2 -0
- julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +2 -0
- julee/services/knowledge_service/memory/test_knowledge_service.py +2 -0
- julee/services/knowledge_service/test_factory.py +2 -0
- julee/util/tests/test_decorators.py +2 -0
- julee-0.1.6.dist-info/METADATA +104 -0
- julee-0.1.6.dist-info/RECORD +288 -0
- julee/docs/sphinx_hcd/accelerators.py +0 -1175
- julee/docs/sphinx_hcd/apps.py +0 -518
- julee/docs/sphinx_hcd/epics.py +0 -453
- julee/docs/sphinx_hcd/integrations.py +0 -310
- julee/docs/sphinx_hcd/journeys.py +0 -797
- julee/docs/sphinx_hcd/personas.py +0 -457
- julee/docs/sphinx_hcd/stories.py +0 -960
- julee-0.1.4.dist-info/METADATA +0 -197
- julee-0.1.4.dist-info/RECORD +0 -196
- {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/WHEEL +0 -0
- {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Tests for AST parser."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from julee.docs.sphinx_hcd.parsers.ast import (
|
|
6
|
+
parse_bounded_context,
|
|
7
|
+
parse_module_docstring,
|
|
8
|
+
parse_python_classes,
|
|
9
|
+
scan_bounded_contexts,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestParsePythonClasses:
|
|
14
|
+
"""Test parse_python_classes function."""
|
|
15
|
+
|
|
16
|
+
def test_parse_single_class(self, tmp_path: Path) -> None:
|
|
17
|
+
"""Test parsing a file with a single class."""
|
|
18
|
+
py_file = tmp_path / "document.py"
|
|
19
|
+
py_file.write_text(
|
|
20
|
+
'''
|
|
21
|
+
class Document:
|
|
22
|
+
"""A document entity."""
|
|
23
|
+
pass
|
|
24
|
+
'''
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
classes = parse_python_classes(tmp_path)
|
|
28
|
+
assert len(classes) == 1
|
|
29
|
+
assert classes[0].name == "Document"
|
|
30
|
+
assert classes[0].docstring == "A document entity."
|
|
31
|
+
assert classes[0].file == "document.py"
|
|
32
|
+
|
|
33
|
+
def test_parse_multiple_classes(self, tmp_path: Path) -> None:
|
|
34
|
+
"""Test parsing a file with multiple classes."""
|
|
35
|
+
py_file = tmp_path / "models.py"
|
|
36
|
+
py_file.write_text(
|
|
37
|
+
'''
|
|
38
|
+
class Document:
|
|
39
|
+
"""A document entity."""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
class Term:
|
|
43
|
+
"""A term in a vocabulary."""
|
|
44
|
+
pass
|
|
45
|
+
'''
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
classes = parse_python_classes(tmp_path)
|
|
49
|
+
assert len(classes) == 2
|
|
50
|
+
names = {c.name for c in classes}
|
|
51
|
+
assert names == {"Document", "Term"}
|
|
52
|
+
|
|
53
|
+
def test_parse_class_no_docstring(self, tmp_path: Path) -> None:
|
|
54
|
+
"""Test parsing a class without a docstring."""
|
|
55
|
+
py_file = tmp_path / "simple.py"
|
|
56
|
+
py_file.write_text(
|
|
57
|
+
"""
|
|
58
|
+
class SimpleClass:
|
|
59
|
+
pass
|
|
60
|
+
"""
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
classes = parse_python_classes(tmp_path)
|
|
64
|
+
assert len(classes) == 1
|
|
65
|
+
assert classes[0].name == "SimpleClass"
|
|
66
|
+
assert classes[0].docstring == ""
|
|
67
|
+
|
|
68
|
+
def test_parse_multiline_docstring_extracts_first_line(
|
|
69
|
+
self, tmp_path: Path
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Test that only the first line of docstring is extracted."""
|
|
72
|
+
py_file = tmp_path / "complex.py"
|
|
73
|
+
py_file.write_text(
|
|
74
|
+
'''
|
|
75
|
+
class ComplexClass:
|
|
76
|
+
"""First line of docstring.
|
|
77
|
+
|
|
78
|
+
More detailed description here.
|
|
79
|
+
With multiple lines.
|
|
80
|
+
"""
|
|
81
|
+
pass
|
|
82
|
+
'''
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
classes = parse_python_classes(tmp_path)
|
|
86
|
+
assert len(classes) == 1
|
|
87
|
+
assert classes[0].docstring == "First line of docstring."
|
|
88
|
+
|
|
89
|
+
def test_skip_private_files(self, tmp_path: Path) -> None:
|
|
90
|
+
"""Test that files starting with underscore are skipped."""
|
|
91
|
+
(tmp_path / "_private.py").write_text("class Private: pass")
|
|
92
|
+
(tmp_path / "__init__.py").write_text("class Init: pass")
|
|
93
|
+
(tmp_path / "public.py").write_text("class Public: pass")
|
|
94
|
+
|
|
95
|
+
classes = parse_python_classes(tmp_path)
|
|
96
|
+
assert len(classes) == 1
|
|
97
|
+
assert classes[0].name == "Public"
|
|
98
|
+
|
|
99
|
+
def test_nonexistent_directory(self) -> None:
|
|
100
|
+
"""Test parsing nonexistent directory returns empty list."""
|
|
101
|
+
classes = parse_python_classes(Path("/nonexistent/path"))
|
|
102
|
+
assert classes == []
|
|
103
|
+
|
|
104
|
+
def test_sorted_by_name(self, tmp_path: Path) -> None:
|
|
105
|
+
"""Test classes are sorted by name."""
|
|
106
|
+
py_file = tmp_path / "classes.py"
|
|
107
|
+
py_file.write_text(
|
|
108
|
+
"""
|
|
109
|
+
class Zebra: pass
|
|
110
|
+
class Apple: pass
|
|
111
|
+
class Mango: pass
|
|
112
|
+
"""
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
classes = parse_python_classes(tmp_path)
|
|
116
|
+
names = [c.name for c in classes]
|
|
117
|
+
assert names == ["Apple", "Mango", "Zebra"]
|
|
118
|
+
|
|
119
|
+
def test_syntax_error_handled(self, tmp_path: Path) -> None:
|
|
120
|
+
"""Test that syntax errors are handled gracefully."""
|
|
121
|
+
py_file = tmp_path / "broken.py"
|
|
122
|
+
py_file.write_text("class Broken def invalid") # Invalid syntax
|
|
123
|
+
(tmp_path / "valid.py").write_text("class Valid: pass")
|
|
124
|
+
|
|
125
|
+
classes = parse_python_classes(tmp_path)
|
|
126
|
+
assert len(classes) == 1
|
|
127
|
+
assert classes[0].name == "Valid"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestParseModuleDocstring:
|
|
131
|
+
"""Test parse_module_docstring function."""
|
|
132
|
+
|
|
133
|
+
def test_parse_module_with_docstring(self, tmp_path: Path) -> None:
|
|
134
|
+
"""Test parsing a module with a docstring."""
|
|
135
|
+
py_file = tmp_path / "module.py"
|
|
136
|
+
py_file.write_text(
|
|
137
|
+
'''"""Module docstring.
|
|
138
|
+
|
|
139
|
+
More details about the module.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
class SomeClass:
|
|
143
|
+
pass
|
|
144
|
+
'''
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
first_line, full = parse_module_docstring(py_file)
|
|
148
|
+
assert first_line == "Module docstring."
|
|
149
|
+
assert "More details" in full
|
|
150
|
+
|
|
151
|
+
def test_parse_module_no_docstring(self, tmp_path: Path) -> None:
|
|
152
|
+
"""Test parsing a module without a docstring."""
|
|
153
|
+
py_file = tmp_path / "no_doc.py"
|
|
154
|
+
py_file.write_text(
|
|
155
|
+
"""
|
|
156
|
+
class SomeClass:
|
|
157
|
+
pass
|
|
158
|
+
"""
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
first_line, full = parse_module_docstring(py_file)
|
|
162
|
+
assert first_line is None
|
|
163
|
+
assert full is None
|
|
164
|
+
|
|
165
|
+
def test_parse_nonexistent_file(self) -> None:
|
|
166
|
+
"""Test parsing nonexistent file."""
|
|
167
|
+
first_line, full = parse_module_docstring(Path("/nonexistent/file.py"))
|
|
168
|
+
assert first_line is None
|
|
169
|
+
assert full is None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestParseBoundedContext:
|
|
173
|
+
"""Test parse_bounded_context function."""
|
|
174
|
+
|
|
175
|
+
def test_parse_full_context(self, tmp_path: Path) -> None:
|
|
176
|
+
"""Test parsing a complete bounded context structure."""
|
|
177
|
+
# Create ADR 001-compliant structure
|
|
178
|
+
context_dir = tmp_path / "vocabulary"
|
|
179
|
+
(context_dir / "domain" / "models").mkdir(parents=True)
|
|
180
|
+
(context_dir / "domain" / "repositories").mkdir(parents=True)
|
|
181
|
+
(context_dir / "domain" / "services").mkdir(parents=True)
|
|
182
|
+
(context_dir / "use_cases").mkdir(parents=True)
|
|
183
|
+
(context_dir / "infrastructure").mkdir(parents=True)
|
|
184
|
+
|
|
185
|
+
# Module docstring
|
|
186
|
+
(context_dir / "__init__.py").write_text('"""Vocabulary management."""')
|
|
187
|
+
|
|
188
|
+
# Entity
|
|
189
|
+
(context_dir / "domain" / "models" / "vocabulary.py").write_text(
|
|
190
|
+
'''
|
|
191
|
+
class Vocabulary:
|
|
192
|
+
"""A vocabulary catalog."""
|
|
193
|
+
pass
|
|
194
|
+
'''
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Use case
|
|
198
|
+
(context_dir / "use_cases" / "create.py").write_text(
|
|
199
|
+
'''
|
|
200
|
+
class CreateVocabulary:
|
|
201
|
+
"""Create a new vocabulary."""
|
|
202
|
+
pass
|
|
203
|
+
'''
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Repository protocol
|
|
207
|
+
(context_dir / "domain" / "repositories" / "vocabulary.py").write_text(
|
|
208
|
+
'''
|
|
209
|
+
class VocabularyRepository:
|
|
210
|
+
"""Repository for vocabularies."""
|
|
211
|
+
pass
|
|
212
|
+
'''
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
info = parse_bounded_context(context_dir)
|
|
216
|
+
assert info is not None
|
|
217
|
+
assert info.slug == "vocabulary"
|
|
218
|
+
assert info.objective == "Vocabulary management."
|
|
219
|
+
assert len(info.entities) == 1
|
|
220
|
+
assert info.entities[0].name == "Vocabulary"
|
|
221
|
+
assert len(info.use_cases) == 1
|
|
222
|
+
assert info.use_cases[0].name == "CreateVocabulary"
|
|
223
|
+
assert len(info.repository_protocols) == 1
|
|
224
|
+
assert info.has_infrastructure is True
|
|
225
|
+
assert info.code_dir == "vocabulary"
|
|
226
|
+
|
|
227
|
+
def test_parse_minimal_context(self, tmp_path: Path) -> None:
|
|
228
|
+
"""Test parsing a minimal bounded context."""
|
|
229
|
+
context_dir = tmp_path / "simple"
|
|
230
|
+
context_dir.mkdir()
|
|
231
|
+
(context_dir / "__init__.py").write_text("")
|
|
232
|
+
|
|
233
|
+
info = parse_bounded_context(context_dir)
|
|
234
|
+
assert info is not None
|
|
235
|
+
assert info.slug == "simple"
|
|
236
|
+
assert info.entities == []
|
|
237
|
+
assert info.use_cases == []
|
|
238
|
+
assert info.has_infrastructure is False
|
|
239
|
+
|
|
240
|
+
def test_parse_nonexistent_context(self) -> None:
|
|
241
|
+
"""Test parsing nonexistent context returns None."""
|
|
242
|
+
info = parse_bounded_context(Path("/nonexistent/context"))
|
|
243
|
+
assert info is None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class TestScanBoundedContexts:
|
|
247
|
+
"""Test scan_bounded_contexts function."""
|
|
248
|
+
|
|
249
|
+
def test_scan_multiple_contexts(self, tmp_path: Path) -> None:
|
|
250
|
+
"""Test scanning a directory with multiple contexts."""
|
|
251
|
+
# Create two contexts
|
|
252
|
+
for name in ["vocabulary", "traceability"]:
|
|
253
|
+
context_dir = tmp_path / name
|
|
254
|
+
context_dir.mkdir()
|
|
255
|
+
(context_dir / "__init__.py").write_text(f'"""{name.title()} module."""')
|
|
256
|
+
(context_dir / "domain" / "models").mkdir(parents=True)
|
|
257
|
+
(context_dir / "domain" / "models" / "entity.py").write_text(
|
|
258
|
+
f"class {name.title()}Entity: pass"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
contexts = scan_bounded_contexts(tmp_path)
|
|
262
|
+
assert len(contexts) == 2
|
|
263
|
+
slugs = {c.slug for c in contexts}
|
|
264
|
+
assert slugs == {"vocabulary", "traceability"}
|
|
265
|
+
|
|
266
|
+
def test_scan_skips_hidden_directories(self, tmp_path: Path) -> None:
|
|
267
|
+
"""Test that hidden directories are skipped."""
|
|
268
|
+
(tmp_path / ".hidden").mkdir()
|
|
269
|
+
(tmp_path / "__pycache__").mkdir()
|
|
270
|
+
(tmp_path / "_private").mkdir()
|
|
271
|
+
visible = tmp_path / "visible"
|
|
272
|
+
visible.mkdir()
|
|
273
|
+
(visible / "__init__.py").write_text("")
|
|
274
|
+
|
|
275
|
+
contexts = scan_bounded_contexts(tmp_path)
|
|
276
|
+
assert len(contexts) == 1
|
|
277
|
+
assert contexts[0].slug == "visible"
|
|
278
|
+
|
|
279
|
+
def test_scan_skips_files(self, tmp_path: Path) -> None:
|
|
280
|
+
"""Test that files (not directories) are skipped."""
|
|
281
|
+
(tmp_path / "file.py").write_text("x = 1")
|
|
282
|
+
context_dir = tmp_path / "context"
|
|
283
|
+
context_dir.mkdir()
|
|
284
|
+
(context_dir / "__init__.py").write_text("")
|
|
285
|
+
|
|
286
|
+
contexts = scan_bounded_contexts(tmp_path)
|
|
287
|
+
assert len(contexts) == 1
|
|
288
|
+
assert contexts[0].slug == "context"
|
|
289
|
+
|
|
290
|
+
def test_scan_nonexistent_directory(self) -> None:
|
|
291
|
+
"""Test scanning nonexistent directory returns empty list."""
|
|
292
|
+
contexts = scan_bounded_contexts(Path("/nonexistent/src"))
|
|
293
|
+
assert contexts == []
|
|
294
|
+
|
|
295
|
+
def test_scan_empty_directory(self, tmp_path: Path) -> None:
|
|
296
|
+
"""Test scanning empty directory returns empty list."""
|
|
297
|
+
contexts = scan_bounded_contexts(tmp_path)
|
|
298
|
+
assert contexts == []
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Tests for Gherkin feature file parser."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from julee.docs.sphinx_hcd.parsers.gherkin import (
|
|
8
|
+
parse_feature_content,
|
|
9
|
+
parse_feature_file,
|
|
10
|
+
scan_feature_directory,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestParseFeatureContent:
|
|
15
|
+
"""Test parse_feature_content function."""
|
|
16
|
+
|
|
17
|
+
def test_parse_complete_feature(self) -> None:
|
|
18
|
+
"""Test parsing a complete feature file."""
|
|
19
|
+
content = """Feature: Submit Order
|
|
20
|
+
|
|
21
|
+
As a Customer
|
|
22
|
+
I want to submit my order
|
|
23
|
+
So that I can purchase products
|
|
24
|
+
|
|
25
|
+
Scenario: Successful submission
|
|
26
|
+
Given I have items in my cart
|
|
27
|
+
When I submit my order
|
|
28
|
+
Then the order is confirmed
|
|
29
|
+
"""
|
|
30
|
+
result = parse_feature_content(content)
|
|
31
|
+
|
|
32
|
+
assert result.feature_title == "Submit Order"
|
|
33
|
+
assert result.persona == "Customer"
|
|
34
|
+
assert result.i_want == "submit my order"
|
|
35
|
+
assert result.so_that == "I can purchase products"
|
|
36
|
+
assert "Feature: Submit Order" in result.gherkin_snippet
|
|
37
|
+
assert "As a Customer" in result.gherkin_snippet
|
|
38
|
+
# Scenario should not be in snippet
|
|
39
|
+
assert "Scenario" not in result.gherkin_snippet
|
|
40
|
+
|
|
41
|
+
def test_parse_feature_with_as_an(self) -> None:
|
|
42
|
+
"""Test parsing 'As an' variant."""
|
|
43
|
+
content = """Feature: Admin Dashboard
|
|
44
|
+
|
|
45
|
+
As an Administrator
|
|
46
|
+
I want to view the dashboard
|
|
47
|
+
So that I can monitor the system
|
|
48
|
+
"""
|
|
49
|
+
result = parse_feature_content(content)
|
|
50
|
+
assert result.persona == "Administrator"
|
|
51
|
+
|
|
52
|
+
def test_parse_feature_missing_persona(self) -> None:
|
|
53
|
+
"""Test parsing feature without persona defaults to 'unknown'."""
|
|
54
|
+
content = """Feature: Some Feature
|
|
55
|
+
|
|
56
|
+
I want to do something
|
|
57
|
+
So that I achieve a goal
|
|
58
|
+
"""
|
|
59
|
+
result = parse_feature_content(content)
|
|
60
|
+
assert result.persona == "unknown"
|
|
61
|
+
|
|
62
|
+
def test_parse_feature_missing_i_want(self) -> None:
|
|
63
|
+
"""Test parsing feature without I want defaults."""
|
|
64
|
+
content = """Feature: Some Feature
|
|
65
|
+
|
|
66
|
+
As a User
|
|
67
|
+
So that I achieve a goal
|
|
68
|
+
"""
|
|
69
|
+
result = parse_feature_content(content)
|
|
70
|
+
assert result.i_want == "do something"
|
|
71
|
+
|
|
72
|
+
def test_parse_feature_missing_so_that(self) -> None:
|
|
73
|
+
"""Test parsing feature without So that defaults."""
|
|
74
|
+
content = """Feature: Some Feature
|
|
75
|
+
|
|
76
|
+
As a User
|
|
77
|
+
I want to do something
|
|
78
|
+
"""
|
|
79
|
+
result = parse_feature_content(content)
|
|
80
|
+
assert result.so_that == "achieve a goal"
|
|
81
|
+
|
|
82
|
+
def test_parse_feature_missing_title(self) -> None:
|
|
83
|
+
"""Test parsing content without Feature line."""
|
|
84
|
+
content = """
|
|
85
|
+
As a User
|
|
86
|
+
I want to do something
|
|
87
|
+
"""
|
|
88
|
+
result = parse_feature_content(content)
|
|
89
|
+
assert result.feature_title == "Unknown"
|
|
90
|
+
|
|
91
|
+
def test_snippet_stops_at_background(self) -> None:
|
|
92
|
+
"""Test that snippet extraction stops at Background."""
|
|
93
|
+
content = """Feature: Test
|
|
94
|
+
|
|
95
|
+
As a User
|
|
96
|
+
I want to test
|
|
97
|
+
|
|
98
|
+
Background:
|
|
99
|
+
Given some setup
|
|
100
|
+
"""
|
|
101
|
+
result = parse_feature_content(content)
|
|
102
|
+
assert "Background" not in result.gherkin_snippet
|
|
103
|
+
|
|
104
|
+
def test_snippet_stops_at_tags(self) -> None:
|
|
105
|
+
"""Test that snippet extraction stops at tags."""
|
|
106
|
+
content = """Feature: Test
|
|
107
|
+
|
|
108
|
+
As a User
|
|
109
|
+
I want to test
|
|
110
|
+
|
|
111
|
+
@slow @integration
|
|
112
|
+
Scenario: Tagged scenario
|
|
113
|
+
"""
|
|
114
|
+
result = parse_feature_content(content)
|
|
115
|
+
assert "@slow" not in result.gherkin_snippet
|
|
116
|
+
|
|
117
|
+
def test_parse_indented_content(self) -> None:
|
|
118
|
+
"""Test parsing with various indentation."""
|
|
119
|
+
content = """Feature: Upload Document
|
|
120
|
+
|
|
121
|
+
As a Staff Member
|
|
122
|
+
I want to upload a document
|
|
123
|
+
So that it can be analyzed
|
|
124
|
+
"""
|
|
125
|
+
result = parse_feature_content(content)
|
|
126
|
+
assert result.persona == "Staff Member"
|
|
127
|
+
assert result.i_want == "upload a document"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestParseFeatureFile:
|
|
131
|
+
"""Test parse_feature_file function."""
|
|
132
|
+
|
|
133
|
+
@pytest.fixture
|
|
134
|
+
def temp_project(self, tmp_path: Path) -> Path:
|
|
135
|
+
"""Create a temporary project structure."""
|
|
136
|
+
# Create feature directory structure
|
|
137
|
+
feature_dir = tmp_path / "tests" / "e2e" / "my-app" / "features"
|
|
138
|
+
feature_dir.mkdir(parents=True)
|
|
139
|
+
return tmp_path
|
|
140
|
+
|
|
141
|
+
def test_parse_feature_file_success(self, temp_project: Path) -> None:
|
|
142
|
+
"""Test parsing a feature file."""
|
|
143
|
+
feature_dir = temp_project / "tests" / "e2e" / "my-app" / "features"
|
|
144
|
+
feature_file = feature_dir / "submit.feature"
|
|
145
|
+
feature_file.write_text(
|
|
146
|
+
"""Feature: Submit Form
|
|
147
|
+
|
|
148
|
+
As a User
|
|
149
|
+
I want to submit a form
|
|
150
|
+
So that my data is saved
|
|
151
|
+
|
|
152
|
+
Scenario: Valid submission
|
|
153
|
+
Given I fill the form
|
|
154
|
+
When I submit
|
|
155
|
+
Then it succeeds
|
|
156
|
+
"""
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
story = parse_feature_file(feature_file, temp_project)
|
|
160
|
+
|
|
161
|
+
assert story is not None
|
|
162
|
+
assert story.feature_title == "Submit Form"
|
|
163
|
+
assert story.persona == "User"
|
|
164
|
+
assert story.app_slug == "my-app"
|
|
165
|
+
assert "tests/e2e/my-app/features/submit.feature" in story.file_path
|
|
166
|
+
|
|
167
|
+
def test_parse_feature_file_with_explicit_app(self, temp_project: Path) -> None:
|
|
168
|
+
"""Test parsing with explicit app slug override."""
|
|
169
|
+
feature_dir = temp_project / "tests" / "e2e" / "my-app" / "features"
|
|
170
|
+
feature_file = feature_dir / "test.feature"
|
|
171
|
+
feature_file.write_text("Feature: Test\n\n As a User\n")
|
|
172
|
+
|
|
173
|
+
story = parse_feature_file(feature_file, temp_project, app_slug="override-app")
|
|
174
|
+
|
|
175
|
+
assert story is not None
|
|
176
|
+
assert story.app_slug == "override-app"
|
|
177
|
+
|
|
178
|
+
def test_parse_feature_file_nonexistent(self, temp_project: Path) -> None:
|
|
179
|
+
"""Test parsing a nonexistent file returns None."""
|
|
180
|
+
nonexistent = temp_project / "nonexistent.feature"
|
|
181
|
+
story = parse_feature_file(nonexistent, temp_project)
|
|
182
|
+
assert story is None
|
|
183
|
+
|
|
184
|
+
def test_parse_feature_file_outside_project_root(self, tmp_path: Path) -> None:
|
|
185
|
+
"""Test parsing a feature file outside the project root logs warning but works."""
|
|
186
|
+
# Create feature file in tmp_path
|
|
187
|
+
feature_file = tmp_path / "test.feature"
|
|
188
|
+
feature_file.write_text("Feature: Test\n\n As a User\n I want to test\n")
|
|
189
|
+
|
|
190
|
+
# Use a different directory as project root (feature is outside)
|
|
191
|
+
project_root = tmp_path / "project"
|
|
192
|
+
project_root.mkdir()
|
|
193
|
+
|
|
194
|
+
story = parse_feature_file(feature_file, project_root)
|
|
195
|
+
|
|
196
|
+
assert story is not None
|
|
197
|
+
assert story.feature_title == "Test"
|
|
198
|
+
# File path should be the full path when outside project root
|
|
199
|
+
assert str(feature_file) in story.file_path or "test.feature" in story.file_path
|
|
200
|
+
|
|
201
|
+
def test_parse_feature_file_unknown_app_slug_structure(
|
|
202
|
+
self, tmp_path: Path
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Test parsing feature file with non-standard path defaults to 'unknown' app."""
|
|
205
|
+
# Create a feature file not in tests/e2e/{app}/features/ structure
|
|
206
|
+
feature_dir = tmp_path / "features"
|
|
207
|
+
feature_dir.mkdir()
|
|
208
|
+
feature_file = feature_dir / "test.feature"
|
|
209
|
+
feature_file.write_text("Feature: Test\n\n As a User\n I want to test\n")
|
|
210
|
+
|
|
211
|
+
story = parse_feature_file(feature_file, tmp_path)
|
|
212
|
+
|
|
213
|
+
assert story is not None
|
|
214
|
+
assert story.app_slug == "unknown"
|
|
215
|
+
|
|
216
|
+
def test_parse_feature_file_short_path_defaults_to_unknown(
|
|
217
|
+
self, tmp_path: Path
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Test parsing feature with path too short for app extraction defaults to 'unknown'."""
|
|
220
|
+
# Create feature file directly in tmp_path (path has < 4 parts)
|
|
221
|
+
feature_file = tmp_path / "test.feature"
|
|
222
|
+
feature_file.write_text("Feature: Test\n\n As a User\n I want to test\n")
|
|
223
|
+
|
|
224
|
+
story = parse_feature_file(feature_file, tmp_path)
|
|
225
|
+
|
|
226
|
+
assert story is not None
|
|
227
|
+
assert story.app_slug == "unknown"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class TestScanFeatureDirectory:
|
|
231
|
+
"""Test scan_feature_directory function."""
|
|
232
|
+
|
|
233
|
+
@pytest.fixture
|
|
234
|
+
def temp_project(self, tmp_path: Path) -> Path:
|
|
235
|
+
"""Create a temporary project with multiple apps."""
|
|
236
|
+
# Create app1 features
|
|
237
|
+
app1_dir = tmp_path / "tests" / "e2e" / "app-one" / "features"
|
|
238
|
+
app1_dir.mkdir(parents=True)
|
|
239
|
+
(app1_dir / "feature1.feature").write_text(
|
|
240
|
+
"Feature: Feature One\n\n As a User\n I want to do one\n"
|
|
241
|
+
)
|
|
242
|
+
(app1_dir / "feature2.feature").write_text(
|
|
243
|
+
"Feature: Feature Two\n\n As an Admin\n I want to do two\n"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Create app2 features
|
|
247
|
+
app2_dir = tmp_path / "tests" / "e2e" / "app-two" / "features"
|
|
248
|
+
app2_dir.mkdir(parents=True)
|
|
249
|
+
(app2_dir / "feature3.feature").write_text(
|
|
250
|
+
"Feature: Feature Three\n\n As a Customer\n I want to do three\n"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return tmp_path
|
|
254
|
+
|
|
255
|
+
def test_scan_finds_all_features(self, temp_project: Path) -> None:
|
|
256
|
+
"""Test scanning finds all feature files."""
|
|
257
|
+
feature_dir = temp_project / "tests" / "e2e"
|
|
258
|
+
stories = scan_feature_directory(feature_dir, temp_project)
|
|
259
|
+
|
|
260
|
+
assert len(stories) == 3
|
|
261
|
+
titles = {s.feature_title for s in stories}
|
|
262
|
+
assert titles == {"Feature One", "Feature Two", "Feature Three"}
|
|
263
|
+
|
|
264
|
+
def test_scan_extracts_apps(self, temp_project: Path) -> None:
|
|
265
|
+
"""Test scanning correctly extracts app slugs."""
|
|
266
|
+
feature_dir = temp_project / "tests" / "e2e"
|
|
267
|
+
stories = scan_feature_directory(feature_dir, temp_project)
|
|
268
|
+
|
|
269
|
+
apps = {s.app_slug for s in stories}
|
|
270
|
+
assert apps == {"app-one", "app-two"}
|
|
271
|
+
|
|
272
|
+
def test_scan_nonexistent_directory(self, tmp_path: Path) -> None:
|
|
273
|
+
"""Test scanning nonexistent directory returns empty list."""
|
|
274
|
+
stories = scan_feature_directory(tmp_path / "nonexistent", tmp_path)
|
|
275
|
+
assert stories == []
|
|
276
|
+
|
|
277
|
+
def test_scan_empty_directory(self, tmp_path: Path) -> None:
|
|
278
|
+
"""Test scanning empty directory returns empty list."""
|
|
279
|
+
empty_dir = tmp_path / "empty"
|
|
280
|
+
empty_dir.mkdir()
|
|
281
|
+
stories = scan_feature_directory(empty_dir, tmp_path)
|
|
282
|
+
assert stories == []
|