codeplain 0.1.8__py3-none-any.whl → 0.2.1__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.
tests/test_imports.py ADDED
@@ -0,0 +1,42 @@
1
+ import pytest
2
+
3
+ import plain_file
4
+
5
+
6
+ def test_non_existent_import(load_test_data, get_test_data_path):
7
+ plain_source_text = load_test_data("data/imports/non_existent_import.plain")
8
+ with pytest.raises(Exception, match="Module does not exist"):
9
+ plain_file.parse_plain_source(plain_source_text, {}, [], [], [])
10
+
11
+
12
+ def test_circular_imports(load_test_data, get_test_data_path):
13
+ plain_source_text = load_test_data("data/imports/circular_imports_main.plain")
14
+ template_dirs = [get_test_data_path("data/imports")]
15
+ with pytest.raises(Exception, match="Circular import detected"):
16
+ plain_file.parse_plain_source(plain_source_text, {}, template_dirs, [], [])
17
+
18
+
19
+ def test_diamond_imports(load_test_data, get_test_data_path):
20
+ plain_source_text = load_test_data("data/imports/diamond_imports_main.plain")
21
+ template_dirs = [get_test_data_path("data/imports")]
22
+ plain_file_parse_result = plain_file.parse_plain_source(plain_source_text, {}, template_dirs, [], [])
23
+
24
+ assert plain_file.marshall_plain_source(plain_file_parse_result.plain_source) == {
25
+ "definitions": [
26
+ {"markdown": "- :CommonImportDef: is a definition in diamond_import_common."},
27
+ {"markdown": "- :Import1Def: is a definition in diamond_import_1."},
28
+ {"markdown": "- :Import2Def: is a definition in diamond_import_2."},
29
+ ],
30
+ "technical specs": [
31
+ {"markdown": "- :CommonImportDef: is used in diamond_import_common."},
32
+ {"markdown": "- :Import1Def: is used in diamond_import_1."},
33
+ {"markdown": "- :Import2Def: is used in diamond_import_2."},
34
+ {"markdown": '- :MainExecutableFile: of :App: should be called "hello_world.py".'},
35
+ ],
36
+ "test specs": [
37
+ {"markdown": "- :CommonImportDef: is tested in diamond_import_common."},
38
+ {"markdown": "- :Import1Def: is tested in diamond_import_1."},
39
+ {"markdown": "- :Import2Def: is tested in diamond_import_2."},
40
+ ],
41
+ "functional specs": [{"markdown": '- Display "hello, world"'}],
42
+ }
@@ -0,0 +1,271 @@
1
+ from unittest.mock import MagicMock, patch
2
+
3
+ import pytest
4
+ from mistletoe.block_token import List, ListItem, Paragraph
5
+ from mistletoe.span_token import Emphasis, RawText, Strong
6
+
7
+ import plain_file
8
+ import plain_spec
9
+ from plain2code_exceptions import PlainSyntaxError
10
+ from plain_file import process_acceptance_tests
11
+
12
+
13
+ class MockParseBuffer(list):
14
+ def __init__(self, *args, loose=False):
15
+ super().__init__(*args)
16
+ self.loose = loose
17
+
18
+
19
+ def _create_mock_fr_content(content="Implement the entry point for ...", line_number=32):
20
+ """Helper to create a mock functional requirement content paragraph."""
21
+ mock_fr_content_text = MagicMock(spec=RawText)
22
+ mock_fr_content_text.content = content
23
+
24
+ mock_fr_content = MagicMock(spec=Paragraph)
25
+ mock_fr_content.children = [mock_fr_content_text]
26
+ mock_fr_content.line_number = line_number
27
+
28
+ return mock_fr_content
29
+
30
+
31
+ def _create_mock_at_heading_paragraph(line_number=34):
32
+ """Helper to create a mock acceptance test heading paragraph with proper structure."""
33
+ mock_at_heading_raw_text = MagicMock(spec=RawText)
34
+ mock_at_heading_raw_text.content = plain_spec.ACCEPTANCE_TEST_HEADING
35
+
36
+ mock_at_heading_strong = MagicMock(spec=Strong)
37
+ mock_at_heading_strong.children = [mock_at_heading_raw_text]
38
+
39
+ mock_at_heading_emphasis = MagicMock(spec=Emphasis)
40
+ mock_at_heading_emphasis.children = [mock_at_heading_strong]
41
+
42
+ mock_at_heading_paragraph = MagicMock(spec=Paragraph)
43
+ mock_at_heading_paragraph.children = [mock_at_heading_emphasis]
44
+ mock_at_heading_paragraph.line_number = line_number
45
+
46
+ return mock_at_heading_paragraph
47
+
48
+
49
+ def _create_mock_at_list(content="The App shouldn't show logging...", line_number=36):
50
+ """Helper to create a mock acceptance test list with a single item."""
51
+ mock_at_item_raw_text = MagicMock(spec=RawText)
52
+ mock_at_item_raw_text.content = content
53
+
54
+ mock_at_item_paragraph = MagicMock(spec=Paragraph)
55
+ mock_at_item_paragraph.children = [mock_at_item_raw_text]
56
+ mock_at_item_paragraph.line_number = line_number
57
+
58
+ mock_at_item = MagicMock(spec=ListItem)
59
+ mock_at_item.children = [mock_at_item_paragraph]
60
+ mock_at_item.line_number = line_number
61
+
62
+ mock_at_list = MagicMock(spec=List)
63
+ mock_at_list.children = [mock_at_item]
64
+ mock_at_list.line_number = line_number
65
+
66
+ return mock_at_list
67
+
68
+
69
+ @patch("plain_file._process_single_acceptance_test_requirement")
70
+ def test_process_acceptance_tests_no_functional_requirements_key(mock_process_single):
71
+ """Tests early return when functional requirements key is missing."""
72
+ plain_source_tree = {}
73
+
74
+ process_acceptance_tests(plain_source_tree)
75
+
76
+ mock_process_single.assert_not_called()
77
+
78
+
79
+ @patch("plain_file._process_single_acceptance_test_requirement")
80
+ def test_process_acceptance_tests_frs_lacks_children_attr(mock_process_single):
81
+ """Tests early return when frs object lacks children attribute."""
82
+ plain_source_tree = {plain_spec.FUNCTIONAL_REQUIREMENTS: None}
83
+
84
+ process_acceptance_tests(plain_source_tree)
85
+
86
+ mock_process_single.assert_not_called()
87
+
88
+
89
+ @patch("plain_file._process_single_acceptance_test_requirement")
90
+ def test_process_acceptance_tests_no_sections_direct_frs(mock_process_single):
91
+ """Tests processing of top-level functional requirements when no sections exist."""
92
+ mock_fr_item1_with_children = MagicMock(name="FRItem1_WithChildren")
93
+ mock_fr_item1_with_children.children = MagicMock(name="ChildrenOfFRItem1")
94
+
95
+ mock_fr_item2_no_children = MagicMock(spec=[], name="FRItem2_NoChildren")
96
+
97
+ mock_fr_item3_with_children = MagicMock(name="FRItem3_WithChildren")
98
+ mock_fr_item3_with_children.children = MagicMock(name="ChildrenOfFRItem3")
99
+
100
+ mock_frs_top_level = MagicMock(name="TopLevelFRS")
101
+ mock_frs_top_level.children = [
102
+ mock_fr_item1_with_children,
103
+ mock_fr_item2_no_children,
104
+ mock_fr_item3_with_children,
105
+ ]
106
+
107
+ plain_source_tree = {
108
+ plain_spec.FUNCTIONAL_REQUIREMENTS: mock_frs_top_level,
109
+ "definitions": MagicMock(name="TopLevelDefs"),
110
+ "technical specs": MagicMock(name="TopLevelNFRs"),
111
+ }
112
+
113
+ process_acceptance_tests(plain_source_tree)
114
+
115
+ assert mock_process_single.call_count == 2
116
+ mock_process_single.assert_any_call(mock_fr_item1_with_children)
117
+ mock_process_single.assert_any_call(mock_fr_item3_with_children)
118
+
119
+
120
+ def test_psart_no_acceptance_tests_present():
121
+ """Tests behavior when no 'Acceptance test:' heading is present."""
122
+ functional_requirement_mock = MagicMock(spec=ListItem)
123
+ functional_requirement_mock.line_number = 1
124
+
125
+ mock_paragraph1 = MagicMock(spec=Paragraph)
126
+ mock_paragraph1.children = []
127
+ mock_paragraph2 = MagicMock(spec=Paragraph)
128
+ mock_paragraph2.children = []
129
+
130
+ original_children = [mock_paragraph1, mock_paragraph2]
131
+ functional_requirement_mock.children = list(original_children)
132
+
133
+ plain_file._process_single_acceptance_test_requirement(functional_requirement_mock)
134
+
135
+ assert not hasattr(
136
+ functional_requirement_mock, plain_spec.ACCEPTANCE_TESTS
137
+ ), "acceptance_tests attribute should not be added if no ATs are found."
138
+ assert list(functional_requirement_mock.children) == original_children, "FR children should remain unchanged."
139
+
140
+
141
+ def test_psart_empty_children_list_in_fr():
142
+ """Tests behavior when the functional requirement itself has no children."""
143
+ functional_requirement_mock = MagicMock(spec=ListItem)
144
+ functional_requirement_mock.line_number = 1
145
+ functional_requirement_mock.children = []
146
+
147
+ plain_file._process_single_acceptance_test_requirement(functional_requirement_mock)
148
+
149
+ assert not hasattr(functional_requirement_mock, plain_spec.ACCEPTANCE_TESTS)
150
+ assert list(functional_requirement_mock.children) == []
151
+
152
+
153
+ def test_psart_heading_not_followed_by_list():
154
+ """Tests behavior if AT heading is present but not followed by a list token."""
155
+ functional_requirement_mock = MagicMock(spec=ListItem)
156
+ functional_requirement_mock.line_number = 1
157
+
158
+ mock_heading_raw_text = MagicMock(spec=RawText)
159
+ mock_heading_raw_text.content = plain_spec.ACCEPTANCE_TEST_HEADING
160
+ mock_heading_paragraph = MagicMock(spec=Paragraph)
161
+ mock_heading_paragraph.children = [mock_heading_raw_text]
162
+ mock_heading_paragraph.line_number = 2
163
+
164
+ mock_not_a_list_token = MagicMock(spec=Paragraph)
165
+ mock_not_a_list_token.children = []
166
+
167
+ original_children = [mock_heading_paragraph, mock_not_a_list_token]
168
+ functional_requirement_mock.children = list(original_children)
169
+
170
+ plain_file._process_single_acceptance_test_requirement(functional_requirement_mock)
171
+
172
+ assert not hasattr(
173
+ functional_requirement_mock, plain_spec.ACCEPTANCE_TESTS
174
+ ), "acceptance_tests attribute should not be added."
175
+ assert len(functional_requirement_mock.children) == 2, "Children count should remain the same as original."
176
+ assert (
177
+ list(functional_requirement_mock.children) == original_children
178
+ ), "Children should be unchanged as AT block was malformed."
179
+
180
+
181
+ def test_psart_with_valid_acceptance_tests():
182
+ # Main item to test - OuterListItem
183
+ functional_requirement_mock = MagicMock(spec=ListItem)
184
+ functional_requirement_mock.line_number = 1
185
+
186
+ # Create the three children using helper methods
187
+ mock_fr_content = _create_mock_fr_content()
188
+ mock_at_heading_paragraph = _create_mock_at_heading_paragraph()
189
+ mock_at_list = _create_mock_at_list()
190
+
191
+ # Set up children of the functional requirement
192
+ functional_requirement_mock.children = [mock_fr_content, mock_at_heading_paragraph, mock_at_list]
193
+
194
+ is_acceptance_test_heading, acceptance_test_heading_problem = plain_file._is_acceptance_test_heading(
195
+ mock_at_heading_paragraph
196
+ )
197
+ assert is_acceptance_test_heading
198
+ assert acceptance_test_heading_problem is None
199
+
200
+ # Call the function under test
201
+ plain_file._process_single_acceptance_test_requirement(functional_requirement_mock)
202
+
203
+ # Verify acceptance_tests were extracted correctly
204
+ assert hasattr(functional_requirement_mock, plain_spec.ACCEPTANCE_TESTS)
205
+ assert len(functional_requirement_mock.acceptance_tests) == 1
206
+ assert mock_at_list.children[0] in functional_requirement_mock.acceptance_tests
207
+
208
+ # Verify only the FR content paragraph remains in children
209
+ assert len(functional_requirement_mock.children) == 1
210
+ assert mock_fr_content in functional_requirement_mock.children
211
+
212
+
213
+ def test_psart_with_invalid_acceptance_test_heading():
214
+ functional_requirement_mock = MagicMock(spec=ListItem)
215
+ functional_requirement_mock.line_number = 1
216
+
217
+ # Create a heading paragraph with wrong content
218
+ mock_at_heading_paragraph = _create_mock_at_heading_paragraph()
219
+
220
+ # Change the content of the raw text to be invalid
221
+ mock_at_heading_paragraph.children[0].children[0].children[0].content = "Acceptance test" # missing colon
222
+
223
+ # Set up children of the functional requirement with just the invalid heading
224
+ functional_requirement_mock.children = [mock_at_heading_paragraph]
225
+
226
+ # Verify that invalid heading is detected
227
+ is_acceptance_test_heading, acceptance_test_heading_problem = plain_file._is_acceptance_test_heading(
228
+ mock_at_heading_paragraph
229
+ )
230
+ assert not is_acceptance_test_heading
231
+ assert acceptance_test_heading_problem is not None
232
+
233
+ # Call the function under test and expect a PlainSyntaxError
234
+ with pytest.raises(PlainSyntaxError):
235
+ plain_file._process_single_acceptance_test_requirement(functional_requirement_mock)
236
+
237
+
238
+ def test_psart_with_duplicate_acceptance_test_heading():
239
+ functional_requirement_mock = MagicMock(spec=ListItem)
240
+ functional_requirement_mock.line_number = 1
241
+
242
+ # Create content and list mocks
243
+ mock_fr_content = _create_mock_fr_content()
244
+ mock_at_list1 = _create_mock_at_list("First acceptance test")
245
+ mock_at_list2 = _create_mock_at_list("Second acceptance test", line_number=40)
246
+
247
+ # Create two heading paragraphs with different line numbers
248
+ mock_at_heading_paragraph1 = _create_mock_at_heading_paragraph(line_number=34)
249
+ mock_at_heading_paragraph2 = _create_mock_at_heading_paragraph(line_number=38)
250
+
251
+ # Set up children of the functional requirement with duplicate AT headings
252
+ functional_requirement_mock.children = [
253
+ mock_fr_content,
254
+ mock_at_heading_paragraph1,
255
+ mock_at_list1,
256
+ mock_at_heading_paragraph2,
257
+ mock_at_list2,
258
+ ]
259
+
260
+ # Call the function under test and expect a PlainSyntaxError about duplicate headings
261
+ expected_error_message = f"Syntax error at line {mock_at_heading_paragraph2.line_number}: Duplicate 'acceptance tests' heading found within the same functional requirement. Only one block of acceptance tests is allowed per functional requirement."
262
+
263
+ with pytest.raises(PlainSyntaxError) as exc_info:
264
+ plain_file._process_single_acceptance_test_requirement(functional_requirement_mock)
265
+
266
+ assert str(exc_info.value) == expected_error_message
267
+
268
+
269
+ def test_invalid_plain_file_extension():
270
+ with pytest.raises(plain_file.InvalidPlainFileExtension):
271
+ plain_file.plain_file_parser("test.txt", [])