fishertools 0.2.1__py3-none-any.whl → 0.4.0__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.
- fishertools/__init__.py +16 -5
- fishertools/errors/__init__.py +11 -3
- fishertools/errors/exception_types.py +282 -0
- fishertools/errors/explainer.py +87 -1
- fishertools/errors/models.py +73 -1
- fishertools/errors/patterns.py +40 -0
- fishertools/examples/cli_example.py +156 -0
- fishertools/examples/learn_example.py +65 -0
- fishertools/examples/logger_example.py +176 -0
- fishertools/examples/menu_example.py +101 -0
- fishertools/examples/storage_example.py +175 -0
- fishertools/input_utils.py +185 -0
- fishertools/learn/__init__.py +19 -2
- fishertools/learn/examples.py +88 -1
- fishertools/learn/knowledge_engine.py +321 -0
- fishertools/learn/repl/__init__.py +19 -0
- fishertools/learn/repl/cli.py +31 -0
- fishertools/learn/repl/code_sandbox.py +229 -0
- fishertools/learn/repl/command_handler.py +544 -0
- fishertools/learn/repl/command_parser.py +165 -0
- fishertools/learn/repl/engine.py +479 -0
- fishertools/learn/repl/models.py +121 -0
- fishertools/learn/repl/session_manager.py +284 -0
- fishertools/learn/repl/test_code_sandbox.py +261 -0
- fishertools/learn/repl/test_code_sandbox_pbt.py +148 -0
- fishertools/learn/repl/test_command_handler.py +224 -0
- fishertools/learn/repl/test_command_handler_pbt.py +189 -0
- fishertools/learn/repl/test_command_parser.py +160 -0
- fishertools/learn/repl/test_command_parser_pbt.py +100 -0
- fishertools/learn/repl/test_engine.py +190 -0
- fishertools/learn/repl/test_session_manager.py +310 -0
- fishertools/learn/repl/test_session_manager_pbt.py +182 -0
- fishertools/learn/test_knowledge_engine.py +241 -0
- fishertools/learn/test_knowledge_engine_pbt.py +180 -0
- fishertools/patterns/__init__.py +46 -0
- fishertools/patterns/cli.py +175 -0
- fishertools/patterns/logger.py +140 -0
- fishertools/patterns/menu.py +99 -0
- fishertools/patterns/storage.py +127 -0
- fishertools/readme_transformer.py +631 -0
- fishertools/safe/__init__.py +6 -1
- fishertools/safe/files.py +329 -1
- fishertools/transform_readme.py +105 -0
- fishertools-0.4.0.dist-info/METADATA +104 -0
- fishertools-0.4.0.dist-info/RECORD +131 -0
- {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/WHEEL +1 -1
- tests/test_documentation_properties.py +329 -0
- tests/test_documentation_structure.py +349 -0
- tests/test_errors/test_exception_types.py +446 -0
- tests/test_errors/test_exception_types_pbt.py +333 -0
- tests/test_errors/test_patterns.py +52 -0
- tests/test_input_utils/__init__.py +1 -0
- tests/test_input_utils/test_input_utils.py +65 -0
- tests/test_learn/test_examples.py +179 -1
- tests/test_learn/test_explain_properties.py +307 -0
- tests/test_patterns_cli.py +611 -0
- tests/test_patterns_docstrings.py +473 -0
- tests/test_patterns_logger.py +465 -0
- tests/test_patterns_menu.py +440 -0
- tests/test_patterns_storage.py +447 -0
- tests/test_readme_enhancements_v0_3_1.py +2036 -0
- tests/test_readme_transformer/__init__.py +1 -0
- tests/test_readme_transformer/test_readme_infrastructure.py +1023 -0
- tests/test_readme_transformer/test_transform_readme_integration.py +431 -0
- tests/test_safe/test_files.py +726 -1
- fishertools-0.2.1.dist-info/METADATA +0 -256
- fishertools-0.2.1.dist-info/RECORD +0 -81
- {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,2036 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for README Enhancements v0.3.1
|
|
3
|
+
|
|
4
|
+
Tests verify that the README contains all required sections and content
|
|
5
|
+
for the explain() data structure documentation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from hypothesis import given, strategies as st
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestExplainDataStructureSection:
|
|
15
|
+
"""Tests for the explain() data structure section in README."""
|
|
16
|
+
|
|
17
|
+
def test_explain_data_structure_section_exists(self) -> None:
|
|
18
|
+
"""Test that the explain() data structure section exists in README."""
|
|
19
|
+
readme_path = Path("README.md")
|
|
20
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
21
|
+
|
|
22
|
+
# Check for the section header
|
|
23
|
+
assert "Структура данных explain()" in content, \
|
|
24
|
+
"Section 'Структура данных explain()' not found in README"
|
|
25
|
+
|
|
26
|
+
def test_explain_data_structure_shows_all_three_keys(self) -> None:
|
|
27
|
+
"""Test that the example shows all three dictionary keys."""
|
|
28
|
+
readme_path = Path("README.md")
|
|
29
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
30
|
+
|
|
31
|
+
# Find the section
|
|
32
|
+
section_start = content.find("Структура данных explain()")
|
|
33
|
+
assert section_start != -1, "Section not found"
|
|
34
|
+
|
|
35
|
+
# Get the section content (up to the next heading)
|
|
36
|
+
section_end = content.find("###", section_start + 1)
|
|
37
|
+
if section_end == -1:
|
|
38
|
+
section_end = content.find("##", section_start + 1)
|
|
39
|
+
if section_end == -1:
|
|
40
|
+
section_end = len(content)
|
|
41
|
+
|
|
42
|
+
section_content = content[section_start:section_end]
|
|
43
|
+
|
|
44
|
+
# Verify all three keys are shown in the example
|
|
45
|
+
assert '"description"' in section_content or "'description'" in section_content, \
|
|
46
|
+
"Key 'description' not found in example"
|
|
47
|
+
assert '"when_to_use"' in section_content or "'when_to_use'" in section_content, \
|
|
48
|
+
"Key 'when_to_use' not found in example"
|
|
49
|
+
assert '"example"' in section_content or "'example'" in section_content, \
|
|
50
|
+
"Key 'example' not found in example"
|
|
51
|
+
|
|
52
|
+
def test_explain_data_structure_shows_realistic_data(self) -> None:
|
|
53
|
+
"""Test that the example uses realistic data from an actual topic."""
|
|
54
|
+
readme_path = Path("README.md")
|
|
55
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
56
|
+
|
|
57
|
+
# Find the section
|
|
58
|
+
section_start = content.find("Структура данных explain()")
|
|
59
|
+
assert section_start != -1, "Section not found"
|
|
60
|
+
|
|
61
|
+
# Get the section content
|
|
62
|
+
section_end = content.find("###", section_start + 1)
|
|
63
|
+
if section_end == -1:
|
|
64
|
+
section_end = content.find("##", section_start + 1)
|
|
65
|
+
if section_end == -1:
|
|
66
|
+
section_end = len(content)
|
|
67
|
+
|
|
68
|
+
section_content = content[section_start:section_end]
|
|
69
|
+
|
|
70
|
+
# Verify realistic data is present (from the "list" topic)
|
|
71
|
+
# Should contain information about lists
|
|
72
|
+
assert "list" in section_content.lower(), \
|
|
73
|
+
"Example should reference 'list' topic"
|
|
74
|
+
|
|
75
|
+
# Should contain description about ordered collection
|
|
76
|
+
assert "Ordered collection" in section_content or "ordered collection" in section_content, \
|
|
77
|
+
"Example should contain description about ordered collection"
|
|
78
|
+
|
|
79
|
+
def test_explain_data_structure_shows_field_access_code(self) -> None:
|
|
80
|
+
"""Test that code snippet showing field access is present."""
|
|
81
|
+
readme_path = Path("README.md")
|
|
82
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
83
|
+
|
|
84
|
+
# Find the section
|
|
85
|
+
section_start = content.find("Структура данных explain()")
|
|
86
|
+
assert section_start != -1, "Section not found"
|
|
87
|
+
|
|
88
|
+
# Get the section content
|
|
89
|
+
section_end = content.find("###", section_start + 1)
|
|
90
|
+
if section_end == -1:
|
|
91
|
+
section_end = content.find("##", section_start + 1)
|
|
92
|
+
if section_end == -1:
|
|
93
|
+
section_end = len(content)
|
|
94
|
+
|
|
95
|
+
section_content = content[section_start:section_end]
|
|
96
|
+
|
|
97
|
+
# Verify field access code is shown
|
|
98
|
+
assert 'explanation["description"]' in section_content, \
|
|
99
|
+
"Code to access 'description' field not found"
|
|
100
|
+
assert 'explanation["when_to_use"]' in section_content, \
|
|
101
|
+
"Code to access 'when_to_use' field not found"
|
|
102
|
+
assert 'explanation["example"]' in section_content, \
|
|
103
|
+
"Code to access 'example' field not found"
|
|
104
|
+
|
|
105
|
+
def test_explain_data_structure_section_placement(self) -> None:
|
|
106
|
+
"""Test that the section is placed in the correct location."""
|
|
107
|
+
readme_path = Path("README.md")
|
|
108
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
109
|
+
|
|
110
|
+
# Find the main section
|
|
111
|
+
learning_tools_section = content.find("Обучающие инструменты v0.3.1")
|
|
112
|
+
assert learning_tools_section != -1, \
|
|
113
|
+
"Main section 'Обучающие инструменты v0.3.1' not found"
|
|
114
|
+
|
|
115
|
+
# Find the data structure section
|
|
116
|
+
data_structure_section = content.find("Структура данных explain()")
|
|
117
|
+
assert data_structure_section != -1, \
|
|
118
|
+
"Section 'Структура данных explain()' not found"
|
|
119
|
+
|
|
120
|
+
# Verify it's after the main section
|
|
121
|
+
assert data_structure_section > learning_tools_section, \
|
|
122
|
+
"Data structure section should be in 'Обучающие инструменты v0.3.1' section"
|
|
123
|
+
|
|
124
|
+
def test_explain_data_structure_section_has_python_code_block(self) -> None:
|
|
125
|
+
"""Test that the section contains Python code blocks."""
|
|
126
|
+
readme_path = Path("README.md")
|
|
127
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
128
|
+
|
|
129
|
+
# Find the section
|
|
130
|
+
section_start = content.find("Структура данных explain()")
|
|
131
|
+
assert section_start != -1, "Section not found"
|
|
132
|
+
|
|
133
|
+
# Get the section content
|
|
134
|
+
section_end = content.find("###", section_start + 1)
|
|
135
|
+
if section_end == -1:
|
|
136
|
+
section_end = content.find("##", section_start + 1)
|
|
137
|
+
if section_end == -1:
|
|
138
|
+
section_end = len(content)
|
|
139
|
+
|
|
140
|
+
section_content = content[section_start:section_end]
|
|
141
|
+
|
|
142
|
+
# Verify Python code blocks are present
|
|
143
|
+
assert "```python" in section_content, \
|
|
144
|
+
"Python code block not found in section"
|
|
145
|
+
assert "from fishertools.learn import explain" in section_content, \
|
|
146
|
+
"Import statement not found in code example"
|
|
147
|
+
|
|
148
|
+
def test_explain_data_structure_section_has_comments(self) -> None:
|
|
149
|
+
"""Test that code examples include helpful comments."""
|
|
150
|
+
readme_path = Path("README.md")
|
|
151
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
152
|
+
|
|
153
|
+
# Find the section
|
|
154
|
+
section_start = content.find("Структура данных explain()")
|
|
155
|
+
assert section_start != -1, "Section not found"
|
|
156
|
+
|
|
157
|
+
# Get the section content
|
|
158
|
+
section_end = content.find("###", section_start + 1)
|
|
159
|
+
if section_end == -1:
|
|
160
|
+
section_end = content.find("##", section_start + 1)
|
|
161
|
+
if section_end == -1:
|
|
162
|
+
section_end = len(content)
|
|
163
|
+
|
|
164
|
+
section_content = content[section_start:section_end]
|
|
165
|
+
|
|
166
|
+
# Verify comments are present
|
|
167
|
+
assert "#" in section_content, \
|
|
168
|
+
"Comments not found in code examples"
|
|
169
|
+
|
|
170
|
+
def test_explain_data_structure_section_formatting(self) -> None:
|
|
171
|
+
"""Test that the section uses proper markdown formatting."""
|
|
172
|
+
readme_path = Path("README.md")
|
|
173
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
174
|
+
|
|
175
|
+
# Find the section
|
|
176
|
+
section_start = content.find("Структура данных explain()")
|
|
177
|
+
assert section_start != -1, "Section not found"
|
|
178
|
+
|
|
179
|
+
# Verify it's a subsection (###)
|
|
180
|
+
# Find the line with the section header
|
|
181
|
+
lines = content[:section_start + 100].split('\n')
|
|
182
|
+
header_line = lines[-1] if lines else ""
|
|
183
|
+
|
|
184
|
+
# The section should be a subsection (###)
|
|
185
|
+
assert "###" in content[max(0, section_start - 50):section_start + 50], \
|
|
186
|
+
"Section should be formatted as a subsection (###)"
|
|
187
|
+
|
|
188
|
+
def test_explain_data_structure_section_has_description_text(self) -> None:
|
|
189
|
+
"""Test that the section has descriptive text explaining the structure."""
|
|
190
|
+
readme_path = Path("README.md")
|
|
191
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
192
|
+
|
|
193
|
+
# Find the section
|
|
194
|
+
section_start = content.find("Структура данных explain()")
|
|
195
|
+
assert section_start != -1, "Section not found"
|
|
196
|
+
|
|
197
|
+
# Get the section content
|
|
198
|
+
section_end = content.find("###", section_start + 1)
|
|
199
|
+
if section_end == -1:
|
|
200
|
+
section_end = content.find("##", section_start + 1)
|
|
201
|
+
if section_end == -1:
|
|
202
|
+
section_end = len(content)
|
|
203
|
+
|
|
204
|
+
section_content = content[section_start:section_end]
|
|
205
|
+
|
|
206
|
+
# Verify descriptive text is present
|
|
207
|
+
assert "словарь" in section_content.lower() or "dictionary" in section_content.lower(), \
|
|
208
|
+
"Section should explain that explain() returns a dictionary"
|
|
209
|
+
assert "ключ" in section_content.lower() or "key" in section_content.lower(), \
|
|
210
|
+
"Section should mention the keys"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class TestTopicsTableCompleteness:
|
|
215
|
+
"""Tests for the topics table completeness in README.
|
|
216
|
+
|
|
217
|
+
Feature: readme-enhancements-v0.3.1, Property 1: Topics Table Completeness
|
|
218
|
+
Validates: Requirements 3.3
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def test_topics_table_exists(self) -> None:
|
|
222
|
+
"""Test that the topics table exists in README."""
|
|
223
|
+
readme_path = Path("README.md")
|
|
224
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
225
|
+
|
|
226
|
+
# Check for the section header
|
|
227
|
+
assert "Поддерживаемые темы" in content, \
|
|
228
|
+
"Section 'Поддерживаемые темы' not found in README"
|
|
229
|
+
|
|
230
|
+
def test_topics_table_has_markdown_table_format(self) -> None:
|
|
231
|
+
"""Test that the topics are presented in a markdown table format."""
|
|
232
|
+
readme_path = Path("README.md")
|
|
233
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
234
|
+
|
|
235
|
+
# Find the section
|
|
236
|
+
section_start = content.find("Поддерживаемые темы")
|
|
237
|
+
assert section_start != -1, "Section not found"
|
|
238
|
+
|
|
239
|
+
# Get the section content (up to the next heading)
|
|
240
|
+
section_end = content.find("###", section_start + 1)
|
|
241
|
+
if section_end == -1:
|
|
242
|
+
section_end = content.find("##", section_start + 1)
|
|
243
|
+
if section_end == -1:
|
|
244
|
+
section_end = len(content)
|
|
245
|
+
|
|
246
|
+
section_content = content[section_start:section_end]
|
|
247
|
+
|
|
248
|
+
# Verify markdown table format
|
|
249
|
+
assert "|" in section_content, \
|
|
250
|
+
"Table should use markdown format with pipes"
|
|
251
|
+
assert "Категория" in section_content, \
|
|
252
|
+
"Table should have 'Категория' column"
|
|
253
|
+
assert "Тема" in section_content, \
|
|
254
|
+
"Table should have 'Тема' column"
|
|
255
|
+
assert "Описание" in section_content, \
|
|
256
|
+
"Table should have 'Описание' column"
|
|
257
|
+
|
|
258
|
+
def test_topics_table_contains_at_least_30_topics(self) -> None:
|
|
259
|
+
"""Test that the topics table contains at least 30 topics.
|
|
260
|
+
|
|
261
|
+
**Validates: Requirements 3.3**
|
|
262
|
+
"""
|
|
263
|
+
import json
|
|
264
|
+
|
|
265
|
+
readme_path = Path("README.md")
|
|
266
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
267
|
+
|
|
268
|
+
# Find the section
|
|
269
|
+
section_start = content.find("Поддерживаемые темы")
|
|
270
|
+
assert section_start != -1, "Section not found"
|
|
271
|
+
|
|
272
|
+
# Get the section content (up to the next heading)
|
|
273
|
+
section_end = content.find("###", section_start + 1)
|
|
274
|
+
if section_end == -1:
|
|
275
|
+
section_end = content.find("##", section_start + 1)
|
|
276
|
+
if section_end == -1:
|
|
277
|
+
section_end = len(content)
|
|
278
|
+
|
|
279
|
+
section_content = content[section_start:section_end]
|
|
280
|
+
|
|
281
|
+
# Count table rows (each row starts with |)
|
|
282
|
+
# Skip the header and separator rows
|
|
283
|
+
lines = section_content.split('\n')
|
|
284
|
+
table_rows = [line for line in lines if line.strip().startswith('|') and '---' not in line]
|
|
285
|
+
|
|
286
|
+
# Subtract header row to get data rows
|
|
287
|
+
data_rows = len(table_rows) - 1 if len(table_rows) > 0 else 0
|
|
288
|
+
|
|
289
|
+
# Load explanations.json to get the expected number of topics
|
|
290
|
+
explanations_path = Path("fishertools/learn/explanations.json")
|
|
291
|
+
with open(explanations_path, 'r', encoding='utf-8') as f:
|
|
292
|
+
explanations = json.load(f)
|
|
293
|
+
|
|
294
|
+
expected_topics = len(explanations)
|
|
295
|
+
|
|
296
|
+
assert data_rows >= expected_topics, \
|
|
297
|
+
f"Table should contain at least {expected_topics} topics, but found {data_rows}"
|
|
298
|
+
|
|
299
|
+
def test_topics_table_has_all_required_categories(self) -> None:
|
|
300
|
+
"""Test that the topics table includes all 5 required categories."""
|
|
301
|
+
readme_path = Path("README.md")
|
|
302
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
303
|
+
|
|
304
|
+
# Find the section
|
|
305
|
+
section_start = content.find("Поддерживаемые темы")
|
|
306
|
+
assert section_start != -1, "Section not found"
|
|
307
|
+
|
|
308
|
+
# Get the section content
|
|
309
|
+
section_end = content.find("###", section_start + 1)
|
|
310
|
+
if section_end == -1:
|
|
311
|
+
section_end = content.find("##", section_start + 1)
|
|
312
|
+
if section_end == -1:
|
|
313
|
+
section_end = len(content)
|
|
314
|
+
|
|
315
|
+
section_content = content[section_start:section_end]
|
|
316
|
+
|
|
317
|
+
# Verify all 5 categories are present
|
|
318
|
+
required_categories = [
|
|
319
|
+
"Типы данных",
|
|
320
|
+
"Управляющие конструкции",
|
|
321
|
+
"Функции",
|
|
322
|
+
"Обработка ошибок",
|
|
323
|
+
"Работа с файлами"
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
for category in required_categories:
|
|
327
|
+
assert category in section_content, \
|
|
328
|
+
f"Category '{category}' not found in topics table"
|
|
329
|
+
|
|
330
|
+
def test_topics_table_has_data_types_category(self) -> None:
|
|
331
|
+
"""Test that Data Types category is present with expected topics."""
|
|
332
|
+
readme_path = Path("README.md")
|
|
333
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
334
|
+
|
|
335
|
+
# Find the section
|
|
336
|
+
section_start = content.find("Поддерживаемые темы")
|
|
337
|
+
assert section_start != -1, "Section not found"
|
|
338
|
+
|
|
339
|
+
# Get the section content
|
|
340
|
+
section_end = content.find("###", section_start + 1)
|
|
341
|
+
if section_end == -1:
|
|
342
|
+
section_end = content.find("##", section_start + 1)
|
|
343
|
+
if section_end == -1:
|
|
344
|
+
section_end = len(content)
|
|
345
|
+
|
|
346
|
+
section_content = content[section_start:section_end]
|
|
347
|
+
|
|
348
|
+
# Verify Data Types category and topics
|
|
349
|
+
assert "Типы данных" in section_content, \
|
|
350
|
+
"Data Types category not found"
|
|
351
|
+
|
|
352
|
+
data_types = ["int", "float", "str", "bool", "list", "tuple", "set", "dict"]
|
|
353
|
+
for dtype in data_types:
|
|
354
|
+
assert dtype in section_content, \
|
|
355
|
+
f"Data type '{dtype}' not found in topics table"
|
|
356
|
+
|
|
357
|
+
def test_topics_table_has_control_structures_category(self) -> None:
|
|
358
|
+
"""Test that Control Structures category is present with expected topics."""
|
|
359
|
+
readme_path = Path("README.md")
|
|
360
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
361
|
+
|
|
362
|
+
# Find the section
|
|
363
|
+
section_start = content.find("Поддерживаемые темы")
|
|
364
|
+
assert section_start != -1, "Section not found"
|
|
365
|
+
|
|
366
|
+
# Get the section content
|
|
367
|
+
section_end = content.find("###", section_start + 1)
|
|
368
|
+
if section_end == -1:
|
|
369
|
+
section_end = content.find("##", section_start + 1)
|
|
370
|
+
if section_end == -1:
|
|
371
|
+
section_end = len(content)
|
|
372
|
+
|
|
373
|
+
section_content = content[section_start:section_end]
|
|
374
|
+
|
|
375
|
+
# Verify Control Structures category and topics
|
|
376
|
+
assert "Управляющие конструкции" in section_content, \
|
|
377
|
+
"Control Structures category not found"
|
|
378
|
+
|
|
379
|
+
control_structures = ["if", "for", "while", "break", "continue"]
|
|
380
|
+
for cs in control_structures:
|
|
381
|
+
assert cs in section_content, \
|
|
382
|
+
f"Control structure '{cs}' not found in topics table"
|
|
383
|
+
|
|
384
|
+
def test_topics_table_has_functions_category(self) -> None:
|
|
385
|
+
"""Test that Functions category is present with expected topics."""
|
|
386
|
+
readme_path = Path("README.md")
|
|
387
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
388
|
+
|
|
389
|
+
# Find the section
|
|
390
|
+
section_start = content.find("Поддерживаемые темы")
|
|
391
|
+
assert section_start != -1, "Section not found"
|
|
392
|
+
|
|
393
|
+
# Get the section content
|
|
394
|
+
section_end = content.find("###", section_start + 1)
|
|
395
|
+
if section_end == -1:
|
|
396
|
+
section_end = content.find("##", section_start + 1)
|
|
397
|
+
if section_end == -1:
|
|
398
|
+
section_end = len(content)
|
|
399
|
+
|
|
400
|
+
section_content = content[section_start:section_end]
|
|
401
|
+
|
|
402
|
+
# Verify Functions category and topics
|
|
403
|
+
assert "Функции" in section_content, \
|
|
404
|
+
"Functions category not found"
|
|
405
|
+
|
|
406
|
+
functions = ["function", "return", "lambda", "*args", "**kwargs"]
|
|
407
|
+
for func in functions:
|
|
408
|
+
assert func in section_content, \
|
|
409
|
+
f"Function '{func}' not found in topics table"
|
|
410
|
+
|
|
411
|
+
def test_topics_table_has_error_handling_category(self) -> None:
|
|
412
|
+
"""Test that Error Handling category is present with expected topics."""
|
|
413
|
+
readme_path = Path("README.md")
|
|
414
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
415
|
+
|
|
416
|
+
# Find the section
|
|
417
|
+
section_start = content.find("Поддерживаемые темы")
|
|
418
|
+
assert section_start != -1, "Section not found"
|
|
419
|
+
|
|
420
|
+
# Get the section content
|
|
421
|
+
section_end = content.find("###", section_start + 1)
|
|
422
|
+
if section_end == -1:
|
|
423
|
+
section_end = content.find("##", section_start + 1)
|
|
424
|
+
if section_end == -1:
|
|
425
|
+
section_end = len(content)
|
|
426
|
+
|
|
427
|
+
section_content = content[section_start:section_end]
|
|
428
|
+
|
|
429
|
+
# Verify Error Handling category and topics
|
|
430
|
+
assert "Обработка ошибок" in section_content, \
|
|
431
|
+
"Error Handling category not found"
|
|
432
|
+
|
|
433
|
+
error_handling = ["try", "except", "finally", "raise"]
|
|
434
|
+
for eh in error_handling:
|
|
435
|
+
assert eh in section_content, \
|
|
436
|
+
f"Error handling '{eh}' not found in topics table"
|
|
437
|
+
|
|
438
|
+
def test_topics_table_has_file_operations_category(self) -> None:
|
|
439
|
+
"""Test that File Operations category is present with expected topics."""
|
|
440
|
+
readme_path = Path("README.md")
|
|
441
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
442
|
+
|
|
443
|
+
# Find the section
|
|
444
|
+
section_start = content.find("Поддерживаемые темы")
|
|
445
|
+
assert section_start != -1, "Section not found"
|
|
446
|
+
|
|
447
|
+
# Get the section content
|
|
448
|
+
section_end = content.find("###", section_start + 1)
|
|
449
|
+
if section_end == -1:
|
|
450
|
+
section_end = content.find("##", section_start + 1)
|
|
451
|
+
if section_end == -1:
|
|
452
|
+
section_end = len(content)
|
|
453
|
+
|
|
454
|
+
section_content = content[section_start:section_end]
|
|
455
|
+
|
|
456
|
+
# Verify File Operations category and topics
|
|
457
|
+
assert "Работа с файлами" in section_content, \
|
|
458
|
+
"File Operations category not found"
|
|
459
|
+
|
|
460
|
+
file_operations = ["open", "read", "write", "with"]
|
|
461
|
+
for fo in file_operations:
|
|
462
|
+
assert fo in section_content, \
|
|
463
|
+
f"File operation '{fo}' not found in topics table"
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class TestTopicDescriptions:
|
|
467
|
+
"""Tests for topic descriptions in the topics table.
|
|
468
|
+
|
|
469
|
+
Feature: readme-enhancements-v0.3.1, Property 2: Topic Descriptions Present
|
|
470
|
+
Validates: Requirements 3.4
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
def test_each_topic_has_description(self) -> None:
|
|
474
|
+
"""Test that each topic in the table has a description.
|
|
475
|
+
|
|
476
|
+
**Validates: Requirements 3.4**
|
|
477
|
+
"""
|
|
478
|
+
readme_path = Path("README.md")
|
|
479
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
480
|
+
|
|
481
|
+
# Find the section
|
|
482
|
+
section_start = content.find("Поддерживаемые темы")
|
|
483
|
+
assert section_start != -1, "Section not found"
|
|
484
|
+
|
|
485
|
+
# Get the section content
|
|
486
|
+
section_end = content.find("###", section_start + 1)
|
|
487
|
+
if section_end == -1:
|
|
488
|
+
section_end = content.find("##", section_start + 1)
|
|
489
|
+
if section_end == -1:
|
|
490
|
+
section_end = len(content)
|
|
491
|
+
|
|
492
|
+
section_content = content[section_start:section_end]
|
|
493
|
+
|
|
494
|
+
# Parse table rows
|
|
495
|
+
lines = section_content.split('\n')
|
|
496
|
+
table_rows = [line for line in lines if line.strip().startswith('|') and '---' not in line]
|
|
497
|
+
|
|
498
|
+
# Skip header row
|
|
499
|
+
data_rows = table_rows[1:] if len(table_rows) > 1 else []
|
|
500
|
+
|
|
501
|
+
# Verify each row has a description (4th column)
|
|
502
|
+
for row in data_rows:
|
|
503
|
+
cells = [cell.strip() for cell in row.split('|')]
|
|
504
|
+
# Table format: | Category | Topic | Description |
|
|
505
|
+
# cells[0] is empty, cells[1] is category, cells[2] is topic, cells[3] is description, cells[4] is empty
|
|
506
|
+
if len(cells) >= 4:
|
|
507
|
+
description = cells[3]
|
|
508
|
+
assert description and len(description) > 0, \
|
|
509
|
+
f"Row '{row}' has empty description"
|
|
510
|
+
# Description should be meaningful (not just whitespace)
|
|
511
|
+
assert len(description.strip()) > 5, \
|
|
512
|
+
f"Description in row '{row}' is too short"
|
|
513
|
+
|
|
514
|
+
def test_descriptions_are_meaningful(self) -> None:
|
|
515
|
+
"""Test that descriptions are meaningful and not just placeholders."""
|
|
516
|
+
readme_path = Path("README.md")
|
|
517
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
518
|
+
|
|
519
|
+
# Find the section
|
|
520
|
+
section_start = content.find("Поддерживаемые темы")
|
|
521
|
+
assert section_start != -1, "Section not found"
|
|
522
|
+
|
|
523
|
+
# Get the section content
|
|
524
|
+
section_end = content.find("###", section_start + 1)
|
|
525
|
+
if section_end == -1:
|
|
526
|
+
section_end = content.find("##", section_start + 1)
|
|
527
|
+
if section_end == -1:
|
|
528
|
+
section_end = len(content)
|
|
529
|
+
|
|
530
|
+
section_content = content[section_start:section_end]
|
|
531
|
+
|
|
532
|
+
# Parse table rows
|
|
533
|
+
lines = section_content.split('\n')
|
|
534
|
+
table_rows = [line for line in lines if line.strip().startswith('|') and '---' not in line]
|
|
535
|
+
|
|
536
|
+
# Skip header row
|
|
537
|
+
data_rows = table_rows[1:] if len(table_rows) > 1 else []
|
|
538
|
+
|
|
539
|
+
# Verify descriptions contain meaningful content
|
|
540
|
+
for row in data_rows:
|
|
541
|
+
cells = [cell.strip() for cell in row.split('|')]
|
|
542
|
+
if len(cells) >= 4:
|
|
543
|
+
description = cells[3]
|
|
544
|
+
# Description should not be just "..." or similar placeholders
|
|
545
|
+
assert description != "..." and description != "...", \
|
|
546
|
+
f"Description is a placeholder in row '{row}'"
|
|
547
|
+
# Description should contain actual words
|
|
548
|
+
assert len(description.split()) > 2, \
|
|
549
|
+
f"Description is too short in row '{row}'"
|
|
550
|
+
|
|
551
|
+
def test_descriptions_match_explanations_json(self) -> None:
|
|
552
|
+
"""Test that descriptions in table match the explanations.json file."""
|
|
553
|
+
import json
|
|
554
|
+
|
|
555
|
+
readme_path = Path("README.md")
|
|
556
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
557
|
+
|
|
558
|
+
# Load explanations.json
|
|
559
|
+
explanations_path = Path("fishertools/learn/explanations.json")
|
|
560
|
+
with open(explanations_path, 'r', encoding='utf-8') as f:
|
|
561
|
+
explanations = json.load(f)
|
|
562
|
+
|
|
563
|
+
# Find the section
|
|
564
|
+
section_start = content.find("Поддерживаемые темы")
|
|
565
|
+
assert section_start != -1, "Section not found"
|
|
566
|
+
|
|
567
|
+
# Get the section content
|
|
568
|
+
section_end = content.find("###", section_start + 1)
|
|
569
|
+
if section_end == -1:
|
|
570
|
+
section_end = content.find("##", section_start + 1)
|
|
571
|
+
if section_end == -1:
|
|
572
|
+
section_end = len(content)
|
|
573
|
+
|
|
574
|
+
section_content = content[section_start:section_end]
|
|
575
|
+
|
|
576
|
+
# Parse table rows
|
|
577
|
+
lines = section_content.split('\n')
|
|
578
|
+
table_rows = [line for line in lines if line.strip().startswith('|') and '---' not in line]
|
|
579
|
+
|
|
580
|
+
# Skip header row
|
|
581
|
+
data_rows = table_rows[1:] if len(table_rows) > 1 else []
|
|
582
|
+
|
|
583
|
+
# Verify descriptions match explanations.json
|
|
584
|
+
for row in data_rows:
|
|
585
|
+
cells = [cell.strip() for cell in row.split('|')]
|
|
586
|
+
if len(cells) >= 3:
|
|
587
|
+
topic = cells[2]
|
|
588
|
+
description = cells[3] if len(cells) >= 4 else ""
|
|
589
|
+
|
|
590
|
+
# Check if topic exists in explanations.json
|
|
591
|
+
if topic in explanations:
|
|
592
|
+
expected_description = explanations[topic]["description"]
|
|
593
|
+
# Description should match or be similar to the one in explanations.json
|
|
594
|
+
assert description in expected_description or expected_description in description, \
|
|
595
|
+
f"Description for '{topic}' doesn't match explanations.json"
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class TestTopicsTableCompletenessProperty:
|
|
600
|
+
"""Property-based tests for topics table completeness.
|
|
601
|
+
|
|
602
|
+
Feature: readme-enhancements-v0.3.1, Property 1: Topics Table Completeness
|
|
603
|
+
**Validates: Requirements 3.3**
|
|
604
|
+
"""
|
|
605
|
+
|
|
606
|
+
@given(st.just(None))
|
|
607
|
+
def test_topics_table_contains_at_least_30_topics_property(self, _) -> None:
|
|
608
|
+
"""Property test: Topics table contains at least 30 topics.
|
|
609
|
+
|
|
610
|
+
**Validates: Requirements 3.3**
|
|
611
|
+
|
|
612
|
+
This property verifies that for any valid README.md file with a topics table,
|
|
613
|
+
the table contains at least 30 topics across all categories as specified in
|
|
614
|
+
requirement 3.3: "THE System SHALL list all 30+ topics with their names"
|
|
615
|
+
|
|
616
|
+
The property checks that:
|
|
617
|
+
1. The topics table exists in the README
|
|
618
|
+
2. The table contains at least 30 topics (as per requirement 3.3)
|
|
619
|
+
3. All topics from explanations.json are represented in the table
|
|
620
|
+
"""
|
|
621
|
+
readme_path = Path("README.md")
|
|
622
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
623
|
+
|
|
624
|
+
# Find the section
|
|
625
|
+
section_start = content.find("Поддерживаемые темы")
|
|
626
|
+
assert section_start != -1, "Section 'Поддерживаемые темы' not found"
|
|
627
|
+
|
|
628
|
+
# Get the section content (up to the next heading)
|
|
629
|
+
section_end = content.find("###", section_start + 1)
|
|
630
|
+
if section_end == -1:
|
|
631
|
+
section_end = content.find("##", section_start + 1)
|
|
632
|
+
if section_end == -1:
|
|
633
|
+
section_end = len(content)
|
|
634
|
+
|
|
635
|
+
section_content = content[section_start:section_end]
|
|
636
|
+
|
|
637
|
+
# Count table rows (each row starts with |)
|
|
638
|
+
# Skip the header and separator rows
|
|
639
|
+
lines = section_content.split('\n')
|
|
640
|
+
table_rows = [line for line in lines if line.strip().startswith('|') and '---' not in line]
|
|
641
|
+
|
|
642
|
+
# Subtract header row to get data rows
|
|
643
|
+
data_rows = len(table_rows) - 1 if len(table_rows) > 0 else 0
|
|
644
|
+
|
|
645
|
+
# Load explanations.json to get the expected number of topics
|
|
646
|
+
explanations_path = Path("fishertools/learn/explanations.json")
|
|
647
|
+
with open(explanations_path, 'r', encoding='utf-8') as f:
|
|
648
|
+
explanations = json.load(f)
|
|
649
|
+
|
|
650
|
+
expected_topics = len(explanations)
|
|
651
|
+
|
|
652
|
+
# Property 1: The table must contain at least as many topics as in explanations.json
|
|
653
|
+
assert data_rows >= expected_topics, \
|
|
654
|
+
f"Property violated: Table should contain at least {expected_topics} topics (from explanations.json), but found {data_rows}"
|
|
655
|
+
|
|
656
|
+
# Property 2: Verify the minimum is at least 30 (as per requirement 3.3)
|
|
657
|
+
# Requirement 3.3 states: "THE System SHALL list all 30+ topics with their names"
|
|
658
|
+
assert data_rows >= 30, \
|
|
659
|
+
f"Property violated: Requirement 3.3 specifies at least 30 topics, but found {data_rows}"
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class TestTopicDescriptionsProperty:
|
|
663
|
+
"""Property-based tests for topic descriptions in the topics table.
|
|
664
|
+
|
|
665
|
+
Feature: readme-enhancements-v0.3.1, Property 2: Topic Descriptions Present
|
|
666
|
+
**Validates: Requirements 3.4**
|
|
667
|
+
"""
|
|
668
|
+
|
|
669
|
+
@given(st.just(None))
|
|
670
|
+
def test_each_topic_has_description_property(self, _) -> None:
|
|
671
|
+
"""Property test: Each topic in table has a description.
|
|
672
|
+
|
|
673
|
+
**Validates: Requirements 3.4**
|
|
674
|
+
|
|
675
|
+
This property verifies that for any valid README.md file with a topics table,
|
|
676
|
+
each topic listed in the table has a brief description as specified in
|
|
677
|
+
requirement 3.4: "THE System SHALL include a brief description of each topic"
|
|
678
|
+
|
|
679
|
+
The property checks that:
|
|
680
|
+
1. The topics table exists in the README
|
|
681
|
+
2. Each topic row in the table has a non-empty description
|
|
682
|
+
3. Each description is meaningful (not just whitespace or placeholders)
|
|
683
|
+
4. Each description is of reasonable length (more than just a few words)
|
|
684
|
+
"""
|
|
685
|
+
readme_path = Path("README.md")
|
|
686
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
687
|
+
|
|
688
|
+
# Find the section
|
|
689
|
+
section_start = content.find("Поддерживаемые темы")
|
|
690
|
+
assert section_start != -1, "Section 'Поддерживаемые темы' not found"
|
|
691
|
+
|
|
692
|
+
# Get the section content (up to the next heading)
|
|
693
|
+
section_end = content.find("###", section_start + 1)
|
|
694
|
+
if section_end == -1:
|
|
695
|
+
section_end = content.find("##", section_start + 1)
|
|
696
|
+
if section_end == -1:
|
|
697
|
+
section_end = len(content)
|
|
698
|
+
|
|
699
|
+
section_content = content[section_start:section_end]
|
|
700
|
+
|
|
701
|
+
# Parse table rows
|
|
702
|
+
lines = section_content.split('\n')
|
|
703
|
+
table_rows = [line for line in lines if line.strip().startswith('|') and '---' not in line]
|
|
704
|
+
|
|
705
|
+
# Skip header row
|
|
706
|
+
data_rows = table_rows[1:] if len(table_rows) > 1 else []
|
|
707
|
+
|
|
708
|
+
# Property: Each row must have a non-empty, meaningful description
|
|
709
|
+
for row_index, row in enumerate(data_rows):
|
|
710
|
+
cells = [cell.strip() for cell in row.split('|')]
|
|
711
|
+
# Table format: | Category | Topic | Description |
|
|
712
|
+
# cells[0] is empty, cells[1] is category, cells[2] is topic, cells[3] is description, cells[4] is empty
|
|
713
|
+
assert len(cells) >= 4, \
|
|
714
|
+
f"Property violated: Row {row_index} doesn't have enough columns"
|
|
715
|
+
|
|
716
|
+
description = cells[3]
|
|
717
|
+
|
|
718
|
+
# Property 1: Description must not be empty
|
|
719
|
+
assert description and len(description) > 0, \
|
|
720
|
+
f"Property violated: Row {row_index} has empty description"
|
|
721
|
+
|
|
722
|
+
# Property 2: Description must not be just whitespace
|
|
723
|
+
assert len(description.strip()) > 0, \
|
|
724
|
+
f"Property violated: Row {row_index} has whitespace-only description"
|
|
725
|
+
|
|
726
|
+
# Property 3: Description must be meaningful (not just placeholders)
|
|
727
|
+
assert description.strip() != "..." and description.strip() != "...", \
|
|
728
|
+
f"Property violated: Row {row_index} has placeholder description"
|
|
729
|
+
|
|
730
|
+
# Property 4: Description should be of reasonable length
|
|
731
|
+
# Requirement 3.4 specifies "brief description", which should be at least a few words
|
|
732
|
+
assert len(description.split()) >= 2, \
|
|
733
|
+
f"Property violated: Row {row_index} description is too short: '{description}'"
|
|
734
|
+
|
|
735
|
+
# Property 5: All rows must have descriptions (universal property)
|
|
736
|
+
# This ensures that the property holds for ALL topics in the table
|
|
737
|
+
assert len(data_rows) > 0, \
|
|
738
|
+
"Property violated: No data rows found in topics table"
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class TestJSONStorageDocumentation:
|
|
742
|
+
"""Tests for the JSON storage documentation section in README.
|
|
743
|
+
|
|
744
|
+
Feature: readme-enhancements-v0.3.1, Task 3: Add JSON storage documentation
|
|
745
|
+
Validates: Requirements 2.1, 2.2
|
|
746
|
+
"""
|
|
747
|
+
|
|
748
|
+
def test_json_storage_section_exists(self) -> None:
|
|
749
|
+
"""Test that the JSON storage documentation section exists in README."""
|
|
750
|
+
readme_path = Path("README.md")
|
|
751
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
752
|
+
|
|
753
|
+
# Check for the section header
|
|
754
|
+
assert "Расширение и локализация" in content, \
|
|
755
|
+
"Section 'Расширение и локализация' not found in README"
|
|
756
|
+
|
|
757
|
+
def test_json_storage_section_mentions_explanations_json(self) -> None:
|
|
758
|
+
"""Test that the section explicitly mentions fishertools/learn/explanations.json.
|
|
759
|
+
|
|
760
|
+
**Validates: Requirements 2.1**
|
|
761
|
+
"""
|
|
762
|
+
readme_path = Path("README.md")
|
|
763
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
764
|
+
|
|
765
|
+
# Find the section
|
|
766
|
+
section_start = content.find("Расширение и локализация")
|
|
767
|
+
assert section_start != -1, "Section not found"
|
|
768
|
+
|
|
769
|
+
# Get the section content (up to the next heading)
|
|
770
|
+
section_end = content.find("###", section_start + 1)
|
|
771
|
+
if section_end == -1:
|
|
772
|
+
section_end = content.find("##", section_start + 1)
|
|
773
|
+
if section_end == -1:
|
|
774
|
+
section_end = len(content)
|
|
775
|
+
|
|
776
|
+
section_content = content[section_start:section_end]
|
|
777
|
+
|
|
778
|
+
# Verify the JSON file path is mentioned
|
|
779
|
+
assert "fishertools/learn/explanations.json" in section_content, \
|
|
780
|
+
"Path 'fishertools/learn/explanations.json' not mentioned in section"
|
|
781
|
+
|
|
782
|
+
def test_json_storage_section_mentions_extensibility(self) -> None:
|
|
783
|
+
"""Test that the section mentions extensibility for contributors.
|
|
784
|
+
|
|
785
|
+
**Validates: Requirements 2.1, 2.2**
|
|
786
|
+
"""
|
|
787
|
+
readme_path = Path("README.md")
|
|
788
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
789
|
+
|
|
790
|
+
# Find the section
|
|
791
|
+
section_start = content.find("Расширение и локализация")
|
|
792
|
+
assert section_start != -1, "Section not found"
|
|
793
|
+
|
|
794
|
+
# Get the section content
|
|
795
|
+
section_end = content.find("###", section_start + 1)
|
|
796
|
+
if section_end == -1:
|
|
797
|
+
section_end = content.find("##", section_start + 1)
|
|
798
|
+
if section_end == -1:
|
|
799
|
+
section_end = len(content)
|
|
800
|
+
|
|
801
|
+
section_content = content[section_start:section_end]
|
|
802
|
+
|
|
803
|
+
# Verify extensibility is mentioned
|
|
804
|
+
assert "контрибьютор" in section_content.lower() or "contributor" in section_content.lower() or \
|
|
805
|
+
"расширен" in section_content.lower() or "extend" in section_content.lower(), \
|
|
806
|
+
"Extensibility for contributors not mentioned in section"
|
|
807
|
+
|
|
808
|
+
def test_json_storage_section_mentions_localization(self) -> None:
|
|
809
|
+
"""Test that the section mentions localization possibilities.
|
|
810
|
+
|
|
811
|
+
**Validates: Requirements 2.2**
|
|
812
|
+
"""
|
|
813
|
+
readme_path = Path("README.md")
|
|
814
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
815
|
+
|
|
816
|
+
# Find the section
|
|
817
|
+
section_start = content.find("Расширение и локализация")
|
|
818
|
+
assert section_start != -1, "Section not found"
|
|
819
|
+
|
|
820
|
+
# Get the section content
|
|
821
|
+
section_end = content.find("###", section_start + 1)
|
|
822
|
+
if section_end == -1:
|
|
823
|
+
section_end = content.find("##", section_start + 1)
|
|
824
|
+
if section_end == -1:
|
|
825
|
+
section_end = len(content)
|
|
826
|
+
|
|
827
|
+
section_content = content[section_start:section_end]
|
|
828
|
+
|
|
829
|
+
# Verify localization is mentioned
|
|
830
|
+
assert "локализ" in section_content.lower() or "locali" in section_content.lower() or \
|
|
831
|
+
"перевод" in section_content.lower() or "translation" in section_content.lower(), \
|
|
832
|
+
"Localization possibilities not mentioned in section"
|
|
833
|
+
|
|
834
|
+
def test_json_storage_section_explains_easy_extension(self) -> None:
|
|
835
|
+
"""Test that the section explains JSON storage makes extension easy.
|
|
836
|
+
|
|
837
|
+
**Validates: Requirements 2.1, 2.2**
|
|
838
|
+
"""
|
|
839
|
+
readme_path = Path("README.md")
|
|
840
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
841
|
+
|
|
842
|
+
# Find the section
|
|
843
|
+
section_start = content.find("Расширение и локализация")
|
|
844
|
+
assert section_start != -1, "Section not found"
|
|
845
|
+
|
|
846
|
+
# Get the section content
|
|
847
|
+
section_end = content.find("###", section_start + 1)
|
|
848
|
+
if section_end == -1:
|
|
849
|
+
section_end = content.find("##", section_start + 1)
|
|
850
|
+
if section_end == -1:
|
|
851
|
+
section_end = len(content)
|
|
852
|
+
|
|
853
|
+
section_content = content[section_start:section_end]
|
|
854
|
+
|
|
855
|
+
# Verify explanation about easy extension
|
|
856
|
+
assert "легко" in section_content.lower() or "easy" in section_content.lower() or \
|
|
857
|
+
"просто" in section_content.lower() or "simple" in section_content.lower(), \
|
|
858
|
+
"Explanation about easy extension not found in section"
|
|
859
|
+
|
|
860
|
+
def test_json_storage_section_placement(self) -> None:
|
|
861
|
+
"""Test that the section is placed after the topics table."""
|
|
862
|
+
readme_path = Path("README.md")
|
|
863
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
864
|
+
|
|
865
|
+
# Find the topics table section
|
|
866
|
+
topics_section = content.find("Поддерживаемые темы")
|
|
867
|
+
assert topics_section != -1, "Topics section not found"
|
|
868
|
+
|
|
869
|
+
# Find the JSON storage section
|
|
870
|
+
json_section = content.find("Расширение и локализация")
|
|
871
|
+
assert json_section != -1, "JSON storage section not found"
|
|
872
|
+
|
|
873
|
+
# Verify it's after the topics table
|
|
874
|
+
assert json_section > topics_section, \
|
|
875
|
+
"JSON storage section should be placed after the topics table"
|
|
876
|
+
|
|
877
|
+
def test_json_storage_section_has_subsection_format(self) -> None:
|
|
878
|
+
"""Test that the section uses proper markdown subsection format."""
|
|
879
|
+
readme_path = Path("README.md")
|
|
880
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
881
|
+
|
|
882
|
+
# Find the section
|
|
883
|
+
section_start = content.find("Расширение и локализация")
|
|
884
|
+
assert section_start != -1, "Section not found"
|
|
885
|
+
|
|
886
|
+
# Verify it's a subsection (###)
|
|
887
|
+
assert "###" in content[max(0, section_start - 50):section_start + 50], \
|
|
888
|
+
"Section should be formatted as a subsection (###)"
|
|
889
|
+
|
|
890
|
+
def test_json_storage_section_has_contributor_guidance(self) -> None:
|
|
891
|
+
"""Test that the section provides guidance for contributors."""
|
|
892
|
+
readme_path = Path("README.md")
|
|
893
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
894
|
+
|
|
895
|
+
# Find the section
|
|
896
|
+
section_start = content.find("Расширение и локализация")
|
|
897
|
+
assert section_start != -1, "Section not found"
|
|
898
|
+
|
|
899
|
+
# Get the section content
|
|
900
|
+
section_end = content.find("###", section_start + 1)
|
|
901
|
+
if section_end == -1:
|
|
902
|
+
section_end = content.find("##", section_start + 1)
|
|
903
|
+
if section_end == -1:
|
|
904
|
+
section_end = len(content)
|
|
905
|
+
|
|
906
|
+
section_content = content[section_start:section_end]
|
|
907
|
+
|
|
908
|
+
# Verify contributor guidance is present
|
|
909
|
+
assert "контрибьютор" in section_content.lower() or "contributor" in section_content.lower(), \
|
|
910
|
+
"Contributor guidance not found"
|
|
911
|
+
|
|
912
|
+
# Should mention adding new topics
|
|
913
|
+
assert "добавь" in section_content.lower() or "add" in section_content.lower(), \
|
|
914
|
+
"Guidance about adding new topics not found"
|
|
915
|
+
|
|
916
|
+
def test_json_storage_section_has_localization_guidance(self) -> None:
|
|
917
|
+
"""Test that the section provides guidance for localization."""
|
|
918
|
+
readme_path = Path("README.md")
|
|
919
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
920
|
+
|
|
921
|
+
# Find the section
|
|
922
|
+
section_start = content.find("Расширение и локализация")
|
|
923
|
+
assert section_start != -1, "Section not found"
|
|
924
|
+
|
|
925
|
+
# Get the section content
|
|
926
|
+
section_end = content.find("###", section_start + 1)
|
|
927
|
+
if section_end == -1:
|
|
928
|
+
section_end = content.find("##", section_start + 1)
|
|
929
|
+
if section_end == -1:
|
|
930
|
+
section_end = len(content)
|
|
931
|
+
|
|
932
|
+
section_content = content[section_start:section_end]
|
|
933
|
+
|
|
934
|
+
# Verify localization guidance is present
|
|
935
|
+
assert "локализ" in section_content.lower() or "locali" in section_content.lower() or \
|
|
936
|
+
"перевод" in section_content.lower() or "translation" in section_content.lower(), \
|
|
937
|
+
"Localization guidance not found"
|
|
938
|
+
|
|
939
|
+
def test_json_storage_section_mentions_json_structure(self) -> None:
|
|
940
|
+
"""Test that the section mentions the JSON structure (description, when_to_use, example)."""
|
|
941
|
+
readme_path = Path("README.md")
|
|
942
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
943
|
+
|
|
944
|
+
# Find the section
|
|
945
|
+
section_start = content.find("Расширение и локализация")
|
|
946
|
+
assert section_start != -1, "Section not found"
|
|
947
|
+
|
|
948
|
+
# Get the section content
|
|
949
|
+
section_end = content.find("###", section_start + 1)
|
|
950
|
+
if section_end == -1:
|
|
951
|
+
section_end = content.find("##", section_start + 1)
|
|
952
|
+
if section_end == -1:
|
|
953
|
+
section_end = len(content)
|
|
954
|
+
|
|
955
|
+
section_content = content[section_start:section_end]
|
|
956
|
+
|
|
957
|
+
# Verify JSON structure is mentioned
|
|
958
|
+
assert "description" in section_content or "описание" in section_content.lower(), \
|
|
959
|
+
"JSON structure (description field) not mentioned"
|
|
960
|
+
assert "when_to_use" in section_content or "когда" in section_content.lower(), \
|
|
961
|
+
"JSON structure (when_to_use field) not mentioned"
|
|
962
|
+
assert "example" in section_content or "пример" in section_content.lower(), \
|
|
963
|
+
"JSON structure (example field) not mentioned"
|
|
964
|
+
|
|
965
|
+
def test_json_storage_section_has_meaningful_content(self) -> None:
|
|
966
|
+
"""Test that the section has meaningful content and is not just a placeholder."""
|
|
967
|
+
readme_path = Path("README.md")
|
|
968
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
969
|
+
|
|
970
|
+
# Find the section
|
|
971
|
+
section_start = content.find("Расширение и локализация")
|
|
972
|
+
assert section_start != -1, "Section not found"
|
|
973
|
+
|
|
974
|
+
# Get the section content
|
|
975
|
+
section_end = content.find("###", section_start + 1)
|
|
976
|
+
if section_end == -1:
|
|
977
|
+
section_end = content.find("##", section_start + 1)
|
|
978
|
+
if section_end == -1:
|
|
979
|
+
section_end = len(content)
|
|
980
|
+
|
|
981
|
+
section_content = content[section_start:section_end]
|
|
982
|
+
|
|
983
|
+
# Verify the section has meaningful content (not just a few words)
|
|
984
|
+
# Should have at least 100 characters of content
|
|
985
|
+
assert len(section_content.strip()) > 100, \
|
|
986
|
+
"Section content is too short to be meaningful"
|
|
987
|
+
|
|
988
|
+
# Should have multiple lines of content
|
|
989
|
+
lines = [line.strip() for line in section_content.split('\n') if line.strip()]
|
|
990
|
+
assert len(lines) > 3, \
|
|
991
|
+
"Section should have multiple lines of content"
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
class TestErrorTypesCompleteness:
|
|
995
|
+
"""Tests for the error types list in README.
|
|
996
|
+
|
|
997
|
+
Feature: readme-enhancements-v0.3.1, Task 4: Expand error types list
|
|
998
|
+
Validates: Requirements 4.1, 4.2, 4.3
|
|
999
|
+
"""
|
|
1000
|
+
|
|
1001
|
+
def test_error_types_section_exists(self) -> None:
|
|
1002
|
+
"""Test that the error types section exists in README."""
|
|
1003
|
+
readme_path = Path("README.md")
|
|
1004
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1005
|
+
|
|
1006
|
+
# Check for the section header
|
|
1007
|
+
assert "Поддерживаемые типы ошибок" in content, \
|
|
1008
|
+
"Section 'Поддерживаемые типы ошибок' not found in README"
|
|
1009
|
+
|
|
1010
|
+
def test_error_types_section_in_documentation(self) -> None:
|
|
1011
|
+
"""Test that the error types section is in the Documentation section."""
|
|
1012
|
+
readme_path = Path("README.md")
|
|
1013
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1014
|
+
|
|
1015
|
+
# Find the Documentation section
|
|
1016
|
+
doc_section = content.find("📖 Документация")
|
|
1017
|
+
assert doc_section != -1, "Documentation section not found"
|
|
1018
|
+
|
|
1019
|
+
# Find the error types section
|
|
1020
|
+
error_section = content.find("Поддерживаемые типы ошибок")
|
|
1021
|
+
assert error_section != -1, "Error types section not found"
|
|
1022
|
+
|
|
1023
|
+
# Verify it's in the Documentation section
|
|
1024
|
+
assert error_section > doc_section, \
|
|
1025
|
+
"Error types section should be in the Documentation section"
|
|
1026
|
+
|
|
1027
|
+
def test_error_types_section_lists_all_required_types(self) -> None:
|
|
1028
|
+
"""Test that all required error types are listed.
|
|
1029
|
+
|
|
1030
|
+
**Validates: Requirements 4.2**
|
|
1031
|
+
"""
|
|
1032
|
+
readme_path = Path("README.md")
|
|
1033
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1034
|
+
|
|
1035
|
+
# Required error types from requirement 4.2
|
|
1036
|
+
required_error_types = [
|
|
1037
|
+
"TypeError",
|
|
1038
|
+
"ValueError",
|
|
1039
|
+
"AttributeError",
|
|
1040
|
+
"IndexError",
|
|
1041
|
+
"KeyError",
|
|
1042
|
+
"ImportError",
|
|
1043
|
+
"SyntaxError",
|
|
1044
|
+
"NameError",
|
|
1045
|
+
"ZeroDivisionError",
|
|
1046
|
+
"FileNotFoundError"
|
|
1047
|
+
]
|
|
1048
|
+
|
|
1049
|
+
# Verify all required error types are present in the README
|
|
1050
|
+
for error_type in required_error_types:
|
|
1051
|
+
assert error_type in content, \
|
|
1052
|
+
f"Error type '{error_type}' not found in README"
|
|
1053
|
+
|
|
1054
|
+
def test_error_types_section_is_organized(self) -> None:
|
|
1055
|
+
"""Test that error types are organized by category or have descriptions.
|
|
1056
|
+
|
|
1057
|
+
**Validates: Requirements 4.3**
|
|
1058
|
+
"""
|
|
1059
|
+
readme_path = Path("README.md")
|
|
1060
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1061
|
+
|
|
1062
|
+
# Find the section
|
|
1063
|
+
section_start = content.find("Поддерживаемые типы ошибок")
|
|
1064
|
+
assert section_start != -1, "Section not found"
|
|
1065
|
+
|
|
1066
|
+
# Get the section content (up to the next main section "###")
|
|
1067
|
+
# Find the next "### " (with space) after the current section
|
|
1068
|
+
next_section = content.find("\n### ", section_start + 1)
|
|
1069
|
+
if next_section == -1:
|
|
1070
|
+
next_section = content.find("\n## ", section_start + 1)
|
|
1071
|
+
if next_section == -1:
|
|
1072
|
+
next_section = len(content)
|
|
1073
|
+
|
|
1074
|
+
section_content = content[section_start:next_section]
|
|
1075
|
+
|
|
1076
|
+
# Verify organization by categories (should have category headers)
|
|
1077
|
+
# Look for category headers like "#### Ошибки типов и значений"
|
|
1078
|
+
assert "####" in section_content, \
|
|
1079
|
+
"Error types should be organized by categories (using #### headers)"
|
|
1080
|
+
|
|
1081
|
+
def test_error_types_section_has_descriptions(self) -> None:
|
|
1082
|
+
"""Test that error types have brief descriptions.
|
|
1083
|
+
|
|
1084
|
+
**Validates: Requirements 4.3**
|
|
1085
|
+
"""
|
|
1086
|
+
readme_path = Path("README.md")
|
|
1087
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1088
|
+
|
|
1089
|
+
# Find the section
|
|
1090
|
+
section_start = content.find("Поддерживаемые типы ошибок")
|
|
1091
|
+
assert section_start != -1, "Section not found"
|
|
1092
|
+
|
|
1093
|
+
# Get the section content (up to the next main section "###")
|
|
1094
|
+
next_section = content.find("\n### ", section_start + 1)
|
|
1095
|
+
if next_section == -1:
|
|
1096
|
+
next_section = content.find("\n## ", section_start + 1)
|
|
1097
|
+
if next_section == -1:
|
|
1098
|
+
next_section = len(content)
|
|
1099
|
+
|
|
1100
|
+
section_content = content[section_start:next_section]
|
|
1101
|
+
|
|
1102
|
+
# Verify descriptions are present (should have dashes and descriptions)
|
|
1103
|
+
assert "-" in section_content, \
|
|
1104
|
+
"Error types should have descriptions (using - bullet points)"
|
|
1105
|
+
|
|
1106
|
+
# Count the number of error type entries with descriptions
|
|
1107
|
+
# Each should be formatted like: - **ErrorType** - description
|
|
1108
|
+
error_entries = section_content.count("**")
|
|
1109
|
+
assert error_entries >= 20, \
|
|
1110
|
+
"Should have descriptions for all error types (at least 10 error types with bold formatting)"
|
|
1111
|
+
|
|
1112
|
+
def test_error_types_section_indicates_complete_list(self) -> None:
|
|
1113
|
+
"""Test that the section indicates this is the complete list.
|
|
1114
|
+
|
|
1115
|
+
**Validates: Requirements 4.1**
|
|
1116
|
+
"""
|
|
1117
|
+
readme_path = Path("README.md")
|
|
1118
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1119
|
+
|
|
1120
|
+
# Find the section
|
|
1121
|
+
section_start = content.find("Поддерживаемые типы ошибок")
|
|
1122
|
+
assert section_start != -1, "Section not found"
|
|
1123
|
+
|
|
1124
|
+
# Get the section content
|
|
1125
|
+
section_end = content.find("###", section_start + 1)
|
|
1126
|
+
if section_end == -1:
|
|
1127
|
+
section_end = content.find("##", section_start + 1)
|
|
1128
|
+
if section_end == -1:
|
|
1129
|
+
section_end = len(content)
|
|
1130
|
+
|
|
1131
|
+
section_content = content[section_start:section_end]
|
|
1132
|
+
|
|
1133
|
+
# Verify indication of complete list
|
|
1134
|
+
# Should mention "полный список" or "complete list" or similar
|
|
1135
|
+
assert "полный" in section_content.lower() or "complete" in section_content.lower() or \
|
|
1136
|
+
"все" in section_content.lower() or "all" in section_content.lower(), \
|
|
1137
|
+
"Section should indicate this is the complete list of supported error types"
|
|
1138
|
+
|
|
1139
|
+
def test_error_types_section_has_examples(self) -> None:
|
|
1140
|
+
"""Test that the section includes usage examples."""
|
|
1141
|
+
readme_path = Path("README.md")
|
|
1142
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1143
|
+
|
|
1144
|
+
# Find the section
|
|
1145
|
+
section_start = content.find("Поддерживаемые типы ошибок")
|
|
1146
|
+
assert section_start != -1, "Section not found"
|
|
1147
|
+
|
|
1148
|
+
# Get the section content (up to the next main section "###")
|
|
1149
|
+
next_section = content.find("\n### ", section_start + 1)
|
|
1150
|
+
if next_section == -1:
|
|
1151
|
+
next_section = content.find("\n## ", section_start + 1)
|
|
1152
|
+
if next_section == -1:
|
|
1153
|
+
next_section = len(content)
|
|
1154
|
+
|
|
1155
|
+
section_content = content[section_start:next_section]
|
|
1156
|
+
|
|
1157
|
+
# Verify examples are present
|
|
1158
|
+
assert "```python" in section_content, \
|
|
1159
|
+
"Section should include Python code examples"
|
|
1160
|
+
assert "explain_error" in section_content, \
|
|
1161
|
+
"Examples should show how to use explain_error()"
|
|
1162
|
+
|
|
1163
|
+
def test_error_types_section_placement(self) -> None:
|
|
1164
|
+
"""Test that the section is properly placed in the README."""
|
|
1165
|
+
readme_path = Path("README.md")
|
|
1166
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1167
|
+
|
|
1168
|
+
# Find the Documentation section
|
|
1169
|
+
doc_section = content.find("📖 Документация")
|
|
1170
|
+
assert doc_section != -1, "Documentation section not found"
|
|
1171
|
+
|
|
1172
|
+
# Find the error types section
|
|
1173
|
+
error_section = content.find("Поддерживаемые типы ошибок")
|
|
1174
|
+
assert error_section != -1, "Error types section not found"
|
|
1175
|
+
|
|
1176
|
+
# Verify it's after the Documentation section header
|
|
1177
|
+
assert error_section > doc_section, \
|
|
1178
|
+
"Error types section should be in the Documentation section"
|
|
1179
|
+
|
|
1180
|
+
def test_error_types_section_has_meaningful_content(self) -> None:
|
|
1181
|
+
"""Test that the section has meaningful content."""
|
|
1182
|
+
readme_path = Path("README.md")
|
|
1183
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1184
|
+
|
|
1185
|
+
# Find the section
|
|
1186
|
+
section_start = content.find("Поддерживаемые типы ошибок")
|
|
1187
|
+
assert section_start != -1, "Section not found"
|
|
1188
|
+
|
|
1189
|
+
# Get the section content (up to the next main section "###")
|
|
1190
|
+
next_section = content.find("\n### ", section_start + 1)
|
|
1191
|
+
if next_section == -1:
|
|
1192
|
+
next_section = content.find("\n## ", section_start + 1)
|
|
1193
|
+
if next_section == -1:
|
|
1194
|
+
next_section = len(content)
|
|
1195
|
+
|
|
1196
|
+
section_content = content[section_start:next_section]
|
|
1197
|
+
|
|
1198
|
+
# Verify the section has meaningful content
|
|
1199
|
+
assert len(section_content.strip()) > 200, \
|
|
1200
|
+
"Section content should be substantial (more than 200 characters)"
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
class TestErrorTypesCompletenessProperty:
|
|
1204
|
+
"""Property-based tests for error types completeness.
|
|
1205
|
+
|
|
1206
|
+
Feature: readme-enhancements-v0.3.1, Property 3: Error Types Completeness
|
|
1207
|
+
**Validates: Requirements 4.2**
|
|
1208
|
+
"""
|
|
1209
|
+
|
|
1210
|
+
@given(st.just(None))
|
|
1211
|
+
def test_all_required_error_types_are_listed_property(self, _) -> None:
|
|
1212
|
+
"""Property test: All required error types are listed in README.
|
|
1213
|
+
|
|
1214
|
+
**Validates: Requirements 4.2**
|
|
1215
|
+
|
|
1216
|
+
This property verifies that for any valid README.md file with an error types section,
|
|
1217
|
+
all required error types are listed as specified in requirement 4.2:
|
|
1218
|
+
"THE System SHALL include: TypeError, ValueError, AttributeError, IndexError, KeyError,
|
|
1219
|
+
ImportError, SyntaxError, NameError, ZeroDivisionError, FileNotFoundError"
|
|
1220
|
+
|
|
1221
|
+
The property checks that:
|
|
1222
|
+
1. The error types section exists in the README
|
|
1223
|
+
2. All 10 required error types are present in the section
|
|
1224
|
+
3. Each error type is properly formatted and documented
|
|
1225
|
+
"""
|
|
1226
|
+
readme_path = Path("README.md")
|
|
1227
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1228
|
+
|
|
1229
|
+
# Find the section
|
|
1230
|
+
section_start = content.find("Поддерживаемые типы ошибок")
|
|
1231
|
+
assert section_start != -1, "Section 'Поддерживаемые типы ошибок' not found"
|
|
1232
|
+
|
|
1233
|
+
# Required error types from requirement 4.2
|
|
1234
|
+
required_error_types = [
|
|
1235
|
+
"TypeError",
|
|
1236
|
+
"ValueError",
|
|
1237
|
+
"AttributeError",
|
|
1238
|
+
"IndexError",
|
|
1239
|
+
"KeyError",
|
|
1240
|
+
"ImportError",
|
|
1241
|
+
"SyntaxError",
|
|
1242
|
+
"NameError",
|
|
1243
|
+
"ZeroDivisionError",
|
|
1244
|
+
"FileNotFoundError"
|
|
1245
|
+
]
|
|
1246
|
+
|
|
1247
|
+
# Property: All required error types must be present in the README
|
|
1248
|
+
for error_type in required_error_types:
|
|
1249
|
+
assert error_type in content, \
|
|
1250
|
+
f"Property violated: Required error type '{error_type}' not found in README"
|
|
1251
|
+
|
|
1252
|
+
# Property: The section must contain all 10 required error types
|
|
1253
|
+
# This is a universal property that must hold for all valid README files
|
|
1254
|
+
assert len(required_error_types) == 10, \
|
|
1255
|
+
"Property violated: Expected exactly 10 required error types"
|
|
1256
|
+
|
|
1257
|
+
# Property: Each error type should be formatted with bold (**ErrorType**)
|
|
1258
|
+
# Count the number of error types that are properly formatted
|
|
1259
|
+
formatted_count = 0
|
|
1260
|
+
for error_type in required_error_types:
|
|
1261
|
+
if f"**{error_type}**" in content:
|
|
1262
|
+
formatted_count += 1
|
|
1263
|
+
|
|
1264
|
+
# At least 8 out of 10 should be properly formatted
|
|
1265
|
+
assert formatted_count >= 8, \
|
|
1266
|
+
f"Property violated: Only {formatted_count} out of 10 error types are properly formatted with bold"
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
class TestLimitationsSection:
|
|
1271
|
+
"""Tests for the limitations section in README.
|
|
1272
|
+
|
|
1273
|
+
Feature: readme-enhancements-v0.3.1, Task 5: Add limitations section
|
|
1274
|
+
Validates: Requirements 5.1, 5.2, 5.3
|
|
1275
|
+
"""
|
|
1276
|
+
|
|
1277
|
+
def _get_section_content(self, content: str, section_name: str) -> str:
|
|
1278
|
+
"""Helper method to extract section content properly."""
|
|
1279
|
+
section_start = content.find(f"## ⚠️ {section_name}")
|
|
1280
|
+
if section_start == -1:
|
|
1281
|
+
section_start = content.find(section_name)
|
|
1282
|
+
if section_start == -1:
|
|
1283
|
+
return ""
|
|
1284
|
+
|
|
1285
|
+
# Find the next main section (## at start of line, not ###)
|
|
1286
|
+
# We need to find \n## that is NOT followed by another #
|
|
1287
|
+
import re
|
|
1288
|
+
next_section_match = re.search(r'\n##(?!#)', content[section_start + 1:])
|
|
1289
|
+
if next_section_match:
|
|
1290
|
+
next_section = section_start + 1 + next_section_match.start()
|
|
1291
|
+
else:
|
|
1292
|
+
next_section = len(content)
|
|
1293
|
+
|
|
1294
|
+
return content[section_start:next_section]
|
|
1295
|
+
|
|
1296
|
+
def test_limitations_section_exists(self) -> None:
|
|
1297
|
+
"""Test that the limitations section exists in README.
|
|
1298
|
+
|
|
1299
|
+
**Validates: Requirements 5.1**
|
|
1300
|
+
"""
|
|
1301
|
+
readme_path = Path("README.md")
|
|
1302
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1303
|
+
|
|
1304
|
+
# Check for the section header with warning emoji
|
|
1305
|
+
assert "⚠️ Ограничения" in content or "Ограничения" in content, \
|
|
1306
|
+
"Section 'Ограничения' (Limitations) not found in README"
|
|
1307
|
+
|
|
1308
|
+
def test_limitations_section_mentions_syntax_error_limitation(self) -> None:
|
|
1309
|
+
"""Test that the section clearly states SyntaxError limitation.
|
|
1310
|
+
|
|
1311
|
+
**Validates: Requirements 5.2**
|
|
1312
|
+
"""
|
|
1313
|
+
readme_path = Path("README.md")
|
|
1314
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1315
|
+
section_content = self._get_section_content(content, "Ограничения")
|
|
1316
|
+
assert section_content, "Limitations section not found"
|
|
1317
|
+
|
|
1318
|
+
# Verify SyntaxError limitation is mentioned
|
|
1319
|
+
assert "SyntaxError" in section_content, \
|
|
1320
|
+
"SyntaxError limitation not mentioned in section"
|
|
1321
|
+
assert "explain_error()" in section_content, \
|
|
1322
|
+
"explain_error() function not mentioned in SyntaxError limitation"
|
|
1323
|
+
|
|
1324
|
+
def test_limitations_section_explains_syntax_error_why(self) -> None:
|
|
1325
|
+
"""Test that the section explains why SyntaxError cannot be explained.
|
|
1326
|
+
|
|
1327
|
+
**Validates: Requirements 5.2**
|
|
1328
|
+
"""
|
|
1329
|
+
readme_path = Path("README.md")
|
|
1330
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1331
|
+
|
|
1332
|
+
# Find the section
|
|
1333
|
+
section_start = content.find("## ⚠️ Ограничения")
|
|
1334
|
+
if section_start == -1:
|
|
1335
|
+
section_start = content.find("Ограничения")
|
|
1336
|
+
assert section_start != -1, "Limitations section not found"
|
|
1337
|
+
|
|
1338
|
+
# Get the section content using the helper method
|
|
1339
|
+
section_content = self._get_section_content(content, "Ограничения")
|
|
1340
|
+
|
|
1341
|
+
# Verify explanation of why SyntaxError cannot be explained
|
|
1342
|
+
# Should mention parsing, parse time, or before execution
|
|
1343
|
+
assert "парс" in section_content.lower() or "parse" in section_content.lower() or \
|
|
1344
|
+
"до выполнения" in section_content.lower() or "before execution" in section_content.lower() or \
|
|
1345
|
+
"до исполнения" in section_content.lower(), \
|
|
1346
|
+
"Explanation of why SyntaxError cannot be explained not found"
|
|
1347
|
+
|
|
1348
|
+
def test_limitations_section_mentions_oop_limitation(self) -> None:
|
|
1349
|
+
"""Test that the section clearly states OOP limitation.
|
|
1350
|
+
|
|
1351
|
+
**Validates: Requirements 5.3**
|
|
1352
|
+
"""
|
|
1353
|
+
readme_path = Path("README.md")
|
|
1354
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1355
|
+
|
|
1356
|
+
# Find the section
|
|
1357
|
+
section_start = content.find("## ⚠️ Ограничения")
|
|
1358
|
+
if section_start == -1:
|
|
1359
|
+
section_start = content.find("Ограничения")
|
|
1360
|
+
assert section_start != -1, "Limitations section not found"
|
|
1361
|
+
|
|
1362
|
+
# Get the section content using the helper method
|
|
1363
|
+
section_content = self._get_section_content(content, "Ограничения")
|
|
1364
|
+
|
|
1365
|
+
# Verify OOP limitation is mentioned
|
|
1366
|
+
assert "OOP" in section_content or "объектно-ориентированное" in section_content.lower() or \
|
|
1367
|
+
"класс" in section_content.lower() or "class" in section_content.lower(), \
|
|
1368
|
+
"OOP limitation not mentioned in section"
|
|
1369
|
+
assert "explain()" in section_content, \
|
|
1370
|
+
"explain() function not mentioned in OOP limitation"
|
|
1371
|
+
|
|
1372
|
+
def test_limitations_section_explains_oop_why(self) -> None:
|
|
1373
|
+
"""Test that the section explains why OOP is not yet supported.
|
|
1374
|
+
|
|
1375
|
+
**Validates: Requirements 5.3**
|
|
1376
|
+
"""
|
|
1377
|
+
readme_path = Path("README.md")
|
|
1378
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1379
|
+
|
|
1380
|
+
# Find the section
|
|
1381
|
+
section_start = content.find("## ⚠️ Ограничения")
|
|
1382
|
+
if section_start == -1:
|
|
1383
|
+
section_start = content.find("Ограничения")
|
|
1384
|
+
assert section_start != -1, "Limitations section not found"
|
|
1385
|
+
|
|
1386
|
+
# Get the section content using the helper method
|
|
1387
|
+
section_content = self._get_section_content(content, "Ограничения")
|
|
1388
|
+
|
|
1389
|
+
# Verify explanation of why OOP is not yet supported
|
|
1390
|
+
# Should mention future versions or planned
|
|
1391
|
+
assert "будущ" in section_content.lower() or "future" in section_content.lower() or \
|
|
1392
|
+
"планиру" in section_content.lower() or "planned" in section_content.lower(), \
|
|
1393
|
+
"Explanation of why OOP is not yet supported not found"
|
|
1394
|
+
|
|
1395
|
+
def test_limitations_section_has_subsection_format(self) -> None:
|
|
1396
|
+
"""Test that the section uses proper markdown subsection format."""
|
|
1397
|
+
readme_path = Path("README.md")
|
|
1398
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1399
|
+
|
|
1400
|
+
# Find the section
|
|
1401
|
+
section_start = content.find("Ограничения")
|
|
1402
|
+
assert section_start != -1, "Section not found"
|
|
1403
|
+
|
|
1404
|
+
# Verify it's a main section (##)
|
|
1405
|
+
assert "##" in content[max(0, section_start - 50):section_start + 50], \
|
|
1406
|
+
"Section should be formatted as a main section (##)"
|
|
1407
|
+
|
|
1408
|
+
def test_limitations_section_has_subsections(self) -> None:
|
|
1409
|
+
"""Test that the section has subsections for each limitation."""
|
|
1410
|
+
readme_path = Path("README.md")
|
|
1411
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1412
|
+
|
|
1413
|
+
# Find the section
|
|
1414
|
+
section_start = content.find("## ⚠️ Ограничения")
|
|
1415
|
+
if section_start == -1:
|
|
1416
|
+
section_start = content.find("Ограничения")
|
|
1417
|
+
assert section_start != -1, "Section not found"
|
|
1418
|
+
|
|
1419
|
+
# Get the section content using the helper method
|
|
1420
|
+
section_content = self._get_section_content(content, "Ограничения")
|
|
1421
|
+
|
|
1422
|
+
# Verify subsections exist (###)
|
|
1423
|
+
assert "###" in section_content, \
|
|
1424
|
+
"Section should have subsections (###) for each limitation"
|
|
1425
|
+
|
|
1426
|
+
def test_limitations_section_has_syntax_error_subsection(self) -> None:
|
|
1427
|
+
"""Test that there's a subsection specifically for SyntaxError limitation."""
|
|
1428
|
+
readme_path = Path("README.md")
|
|
1429
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1430
|
+
|
|
1431
|
+
# Find the section
|
|
1432
|
+
section_start = content.find("## ⚠️ Ограничения")
|
|
1433
|
+
if section_start == -1:
|
|
1434
|
+
section_start = content.find("Ограничения")
|
|
1435
|
+
assert section_start != -1, "Section not found"
|
|
1436
|
+
|
|
1437
|
+
# Get the section content using the helper method
|
|
1438
|
+
section_content = self._get_section_content(content, "Ограничения")
|
|
1439
|
+
|
|
1440
|
+
# Verify SyntaxError subsection exists
|
|
1441
|
+
assert "SyntaxError" in section_content, \
|
|
1442
|
+
"SyntaxError subsection not found"
|
|
1443
|
+
|
|
1444
|
+
def test_limitations_section_has_oop_subsection(self) -> None:
|
|
1445
|
+
"""Test that there's a subsection specifically for OOP limitation."""
|
|
1446
|
+
readme_path = Path("README.md")
|
|
1447
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1448
|
+
|
|
1449
|
+
# Find the section
|
|
1450
|
+
section_start = content.find("## ⚠️ Ограничения")
|
|
1451
|
+
if section_start == -1:
|
|
1452
|
+
section_start = content.find("Ограничения")
|
|
1453
|
+
assert section_start != -1, "Section not found"
|
|
1454
|
+
|
|
1455
|
+
# Get the section content using the helper method
|
|
1456
|
+
section_content = self._get_section_content(content, "Ограничения")
|
|
1457
|
+
|
|
1458
|
+
# Verify OOP subsection exists
|
|
1459
|
+
assert "объектно-ориентированное" in section_content.lower() or \
|
|
1460
|
+
"OOP" in section_content or "класс" in section_content.lower(), \
|
|
1461
|
+
"OOP subsection not found"
|
|
1462
|
+
|
|
1463
|
+
def test_limitations_section_has_code_examples(self) -> None:
|
|
1464
|
+
"""Test that the section includes code examples."""
|
|
1465
|
+
readme_path = Path("README.md")
|
|
1466
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1467
|
+
|
|
1468
|
+
# Find the section
|
|
1469
|
+
section_start = content.find("## ⚠️ Ограничения")
|
|
1470
|
+
if section_start == -1:
|
|
1471
|
+
section_start = content.find("Ограничения")
|
|
1472
|
+
assert section_start != -1, "Section not found"
|
|
1473
|
+
|
|
1474
|
+
# Get the section content using the helper method
|
|
1475
|
+
section_content = self._get_section_content(content, "Ограничения")
|
|
1476
|
+
|
|
1477
|
+
# Verify code examples are present
|
|
1478
|
+
assert "```python" in section_content, \
|
|
1479
|
+
"Section should include Python code examples"
|
|
1480
|
+
|
|
1481
|
+
def test_limitations_section_placement(self) -> None:
|
|
1482
|
+
"""Test that the section is placed in a prominent location."""
|
|
1483
|
+
readme_path = Path("README.md")
|
|
1484
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1485
|
+
|
|
1486
|
+
# Find the Documentation section
|
|
1487
|
+
doc_section = content.find("📖 Документация")
|
|
1488
|
+
|
|
1489
|
+
# Find the Testing section
|
|
1490
|
+
testing_section = content.find("🧪 Тестирование")
|
|
1491
|
+
|
|
1492
|
+
# Find the limitations section
|
|
1493
|
+
limitations_section = content.find("Ограничения")
|
|
1494
|
+
assert limitations_section != -1, "Limitations section not found"
|
|
1495
|
+
|
|
1496
|
+
# Verify it's placed after Documentation and before Testing
|
|
1497
|
+
# (or in a prominent location)
|
|
1498
|
+
if doc_section != -1 and testing_section != -1:
|
|
1499
|
+
assert doc_section < limitations_section < testing_section, \
|
|
1500
|
+
"Limitations section should be placed between Documentation and Testing sections"
|
|
1501
|
+
|
|
1502
|
+
def test_limitations_section_has_meaningful_content(self) -> None:
|
|
1503
|
+
"""Test that the section has meaningful content."""
|
|
1504
|
+
readme_path = Path("README.md")
|
|
1505
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1506
|
+
|
|
1507
|
+
# Find the section
|
|
1508
|
+
section_start = content.find("## ⚠️ Ограничения")
|
|
1509
|
+
if section_start == -1:
|
|
1510
|
+
section_start = content.find("Ограничения")
|
|
1511
|
+
assert section_start != -1, "Section not found"
|
|
1512
|
+
|
|
1513
|
+
# Get the section content using the helper method
|
|
1514
|
+
section_content = self._get_section_content(content, "Ограничения")
|
|
1515
|
+
|
|
1516
|
+
# Verify the section has meaningful content
|
|
1517
|
+
assert len(section_content.strip()) > 200, \
|
|
1518
|
+
"Section content should be substantial (more than 200 characters)"
|
|
1519
|
+
|
|
1520
|
+
# Should have multiple lines of content
|
|
1521
|
+
lines = [line.strip() for line in section_content.split('\n') if line.strip()]
|
|
1522
|
+
assert len(lines) > 5, \
|
|
1523
|
+
"Section should have multiple lines of content"
|
|
1524
|
+
|
|
1525
|
+
def test_limitations_section_syntax_error_mentions_try_except(self) -> None:
|
|
1526
|
+
"""Test that SyntaxError limitation mentions try-except."""
|
|
1527
|
+
readme_path = Path("README.md")
|
|
1528
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1529
|
+
|
|
1530
|
+
# Find the section
|
|
1531
|
+
section_start = content.find("## ⚠️ Ограничения")
|
|
1532
|
+
if section_start == -1:
|
|
1533
|
+
section_start = content.find("Ограничения")
|
|
1534
|
+
assert section_start != -1, "Section not found"
|
|
1535
|
+
|
|
1536
|
+
# Get the section content using the helper method
|
|
1537
|
+
section_content = self._get_section_content(content, "Ограничения")
|
|
1538
|
+
|
|
1539
|
+
# Verify try-except is mentioned in context of SyntaxError
|
|
1540
|
+
assert "try" in section_content.lower() or "try-except" in section_content.lower(), \
|
|
1541
|
+
"SyntaxError limitation should mention try-except"
|
|
1542
|
+
|
|
1543
|
+
def test_limitations_section_oop_lists_unsupported_concepts(self) -> None:
|
|
1544
|
+
"""Test that OOP limitation lists specific unsupported concepts."""
|
|
1545
|
+
readme_path = Path("README.md")
|
|
1546
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1547
|
+
|
|
1548
|
+
# Find the section
|
|
1549
|
+
section_start = content.find("## ⚠️ Ограничения")
|
|
1550
|
+
if section_start == -1:
|
|
1551
|
+
section_start = content.find("Ограничения")
|
|
1552
|
+
assert section_start != -1, "Section not found"
|
|
1553
|
+
|
|
1554
|
+
# Get the section content using the helper method
|
|
1555
|
+
section_content = self._get_section_content(content, "Ограничения")
|
|
1556
|
+
|
|
1557
|
+
# Verify specific OOP concepts are mentioned
|
|
1558
|
+
oop_concepts = ["класс", "наследование", "полиморфизм", "инкапсуляция"]
|
|
1559
|
+
found_concepts = 0
|
|
1560
|
+
for concept in oop_concepts:
|
|
1561
|
+
if concept in section_content.lower():
|
|
1562
|
+
found_concepts += 1
|
|
1563
|
+
|
|
1564
|
+
assert found_concepts >= 2, \
|
|
1565
|
+
"OOP limitation should mention specific unsupported concepts"
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
class TestPyPIStatus:
|
|
1569
|
+
"""Tests for the PyPI publication status in README.
|
|
1570
|
+
|
|
1571
|
+
Feature: readme-enhancements-v0.3.1, Task 6: Clarify PyPI publication status
|
|
1572
|
+
Validates: Requirements 6.1, 6.2, 6.3, 6.4
|
|
1573
|
+
"""
|
|
1574
|
+
|
|
1575
|
+
def _get_installation_section_content(self, content: str) -> str:
|
|
1576
|
+
"""Helper method to extract installation section content."""
|
|
1577
|
+
# Find the installation section
|
|
1578
|
+
section_start = content.find("## 📦 Установка")
|
|
1579
|
+
if section_start == -1:
|
|
1580
|
+
section_start = content.find("📦 Установка")
|
|
1581
|
+
|
|
1582
|
+
if section_start == -1:
|
|
1583
|
+
return ""
|
|
1584
|
+
|
|
1585
|
+
# Find the next main section (##)
|
|
1586
|
+
# Look for the next ## after the current position
|
|
1587
|
+
next_section = content.find("\n## ", section_start + 1)
|
|
1588
|
+
if next_section == -1:
|
|
1589
|
+
section_end = len(content)
|
|
1590
|
+
else:
|
|
1591
|
+
section_end = next_section
|
|
1592
|
+
|
|
1593
|
+
return content[section_start:section_end]
|
|
1594
|
+
|
|
1595
|
+
def test_installation_section_exists(self) -> None:
|
|
1596
|
+
"""Test that the installation section exists in README."""
|
|
1597
|
+
readme_path = Path("README.md")
|
|
1598
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1599
|
+
|
|
1600
|
+
# Check for the section header
|
|
1601
|
+
assert "📦 Установка" in content, \
|
|
1602
|
+
"Section '📦 Установка' not found in README"
|
|
1603
|
+
|
|
1604
|
+
def test_pypi_status_section_exists(self) -> None:
|
|
1605
|
+
"""Test that the PyPI status subsection exists.
|
|
1606
|
+
|
|
1607
|
+
**Validates: Requirements 6.1**
|
|
1608
|
+
"""
|
|
1609
|
+
readme_path = Path("README.md")
|
|
1610
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1611
|
+
|
|
1612
|
+
# Get the installation section content
|
|
1613
|
+
section_content = self._get_installation_section_content(content)
|
|
1614
|
+
assert section_content, "Installation section not found"
|
|
1615
|
+
|
|
1616
|
+
# Check for the PyPI status subsection
|
|
1617
|
+
assert "Текущий статус версии" in section_content, \
|
|
1618
|
+
"PyPI status subsection 'Текущий статус версии' not found"
|
|
1619
|
+
|
|
1620
|
+
def test_v0_3_1_not_published_statement(self) -> None:
|
|
1621
|
+
"""Test that README clearly states v0.3.1 is not yet published on PyPI.
|
|
1622
|
+
|
|
1623
|
+
**Validates: Requirements 6.1**
|
|
1624
|
+
"""
|
|
1625
|
+
readme_path = Path("README.md")
|
|
1626
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1627
|
+
|
|
1628
|
+
# Get the installation section content
|
|
1629
|
+
section_content = self._get_installation_section_content(content)
|
|
1630
|
+
assert section_content, "Installation section not found"
|
|
1631
|
+
|
|
1632
|
+
# Verify clear statement about v0.3.1 not being published
|
|
1633
|
+
assert "0.3.1" in section_content, \
|
|
1634
|
+
"Version 0.3.1 not mentioned in installation section"
|
|
1635
|
+
assert "ещё не опубликована" in section_content or "not yet published" in section_content, \
|
|
1636
|
+
"Clear statement that v0.3.1 is not yet published not found"
|
|
1637
|
+
assert "PyPI" in section_content, \
|
|
1638
|
+
"PyPI not mentioned in the statement"
|
|
1639
|
+
|
|
1640
|
+
def test_v0_2_1_latest_on_pypi_statement(self) -> None:
|
|
1641
|
+
"""Test that README notes v0.2.1 is the latest version on PyPI.
|
|
1642
|
+
|
|
1643
|
+
**Validates: Requirements 6.2**
|
|
1644
|
+
"""
|
|
1645
|
+
readme_path = Path("README.md")
|
|
1646
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1647
|
+
|
|
1648
|
+
# Get the installation section content
|
|
1649
|
+
section_content = self._get_installation_section_content(content)
|
|
1650
|
+
assert section_content, "Installation section not found"
|
|
1651
|
+
|
|
1652
|
+
# Verify statement about v0.2.1 being latest on PyPI
|
|
1653
|
+
assert "0.2.1" in section_content, \
|
|
1654
|
+
"Version 0.2.1 not mentioned in installation section"
|
|
1655
|
+
assert "PyPI" in section_content, \
|
|
1656
|
+
"PyPI not mentioned"
|
|
1657
|
+
# Should mention it's the latest version
|
|
1658
|
+
assert "последняя" in section_content.lower() or "latest" in section_content.lower(), \
|
|
1659
|
+
"Statement about v0.2.1 being latest on PyPI not found"
|
|
1660
|
+
|
|
1661
|
+
def test_git_clone_instructions_present(self) -> None:
|
|
1662
|
+
"""Test that git clone instructions are provided for installing from source.
|
|
1663
|
+
|
|
1664
|
+
**Validates: Requirements 6.3**
|
|
1665
|
+
"""
|
|
1666
|
+
readme_path = Path("README.md")
|
|
1667
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1668
|
+
|
|
1669
|
+
# Get the installation section content
|
|
1670
|
+
section_content = self._get_installation_section_content(content)
|
|
1671
|
+
assert section_content, "Installation section not found"
|
|
1672
|
+
|
|
1673
|
+
# Verify git clone instructions
|
|
1674
|
+
assert "git clone" in section_content, \
|
|
1675
|
+
"git clone instruction not found"
|
|
1676
|
+
assert "https://github.com" in section_content or "github.com" in section_content, \
|
|
1677
|
+
"GitHub repository URL not found"
|
|
1678
|
+
assert "My_1st_library_python" in section_content, \
|
|
1679
|
+
"Repository name not found in git clone instruction"
|
|
1680
|
+
|
|
1681
|
+
def test_pip_install_e_instructions_present(self) -> None:
|
|
1682
|
+
"""Test that pip install -e instructions are provided for development mode.
|
|
1683
|
+
|
|
1684
|
+
**Validates: Requirements 6.4**
|
|
1685
|
+
"""
|
|
1686
|
+
readme_path = Path("README.md")
|
|
1687
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1688
|
+
|
|
1689
|
+
# Get the installation section content
|
|
1690
|
+
section_content = self._get_installation_section_content(content)
|
|
1691
|
+
assert section_content, "Installation section not found"
|
|
1692
|
+
|
|
1693
|
+
# Verify pip install -e instructions
|
|
1694
|
+
assert "pip install -e" in section_content, \
|
|
1695
|
+
"pip install -e instruction not found"
|
|
1696
|
+
assert "pip install -e ." in section_content, \
|
|
1697
|
+
"pip install -e . instruction not found"
|
|
1698
|
+
|
|
1699
|
+
def test_installation_section_has_subsections(self) -> None:
|
|
1700
|
+
"""Test that the installation section has proper subsections."""
|
|
1701
|
+
readme_path = Path("README.md")
|
|
1702
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1703
|
+
|
|
1704
|
+
# Get the installation section content
|
|
1705
|
+
section_content = self._get_installation_section_content(content)
|
|
1706
|
+
assert section_content, "Installation section not found"
|
|
1707
|
+
|
|
1708
|
+
# Verify subsections exist
|
|
1709
|
+
assert "###" in section_content, \
|
|
1710
|
+
"Installation section should have subsections (###)"
|
|
1711
|
+
|
|
1712
|
+
def test_installation_section_has_code_blocks(self) -> None:
|
|
1713
|
+
"""Test that the installation section includes code blocks."""
|
|
1714
|
+
readme_path = Path("README.md")
|
|
1715
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1716
|
+
|
|
1717
|
+
# Get the installation section content
|
|
1718
|
+
section_content = self._get_installation_section_content(content)
|
|
1719
|
+
assert section_content, "Installation section not found"
|
|
1720
|
+
|
|
1721
|
+
# Verify code blocks are present
|
|
1722
|
+
assert "```bash" in section_content, \
|
|
1723
|
+
"Bash code blocks not found in installation section"
|
|
1724
|
+
|
|
1725
|
+
def test_installation_from_source_subsection_exists(self) -> None:
|
|
1726
|
+
"""Test that there's a subsection for installing from source."""
|
|
1727
|
+
readme_path = Path("README.md")
|
|
1728
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1729
|
+
|
|
1730
|
+
# Get the installation section content
|
|
1731
|
+
section_content = self._get_installation_section_content(content)
|
|
1732
|
+
assert section_content, "Installation section not found"
|
|
1733
|
+
|
|
1734
|
+
# Verify subsection for installing from source
|
|
1735
|
+
assert "исходник" in section_content.lower() or "source" in section_content.lower(), \
|
|
1736
|
+
"Subsection for installing from source not found"
|
|
1737
|
+
|
|
1738
|
+
def test_installation_development_mode_subsection_exists(self) -> None:
|
|
1739
|
+
"""Test that there's a subsection for development mode installation."""
|
|
1740
|
+
readme_path = Path("README.md")
|
|
1741
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1742
|
+
|
|
1743
|
+
# Get the installation section content
|
|
1744
|
+
section_content = self._get_installation_section_content(content)
|
|
1745
|
+
assert section_content, "Installation section not found"
|
|
1746
|
+
|
|
1747
|
+
# Verify subsection for development mode
|
|
1748
|
+
assert "разработк" in section_content.lower() or "development" in section_content.lower(), \
|
|
1749
|
+
"Subsection for development mode installation not found"
|
|
1750
|
+
|
|
1751
|
+
def test_installation_section_has_note_about_publication(self) -> None:
|
|
1752
|
+
"""Test that there's a note about when v0.3.1 will be published."""
|
|
1753
|
+
readme_path = Path("README.md")
|
|
1754
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1755
|
+
|
|
1756
|
+
# Get the installation section content
|
|
1757
|
+
section_content = self._get_installation_section_content(content)
|
|
1758
|
+
assert section_content, "Installation section not found"
|
|
1759
|
+
|
|
1760
|
+
# Verify note about publication
|
|
1761
|
+
assert "Примечание" in section_content or "Note" in section_content or \
|
|
1762
|
+
"опубликована" in section_content or "published" in section_content, \
|
|
1763
|
+
"Note about when v0.3.1 will be published not found"
|
|
1764
|
+
|
|
1765
|
+
def test_installation_section_mentions_pip_install_fishertools(self) -> None:
|
|
1766
|
+
"""Test that instructions for installing v0.2.1 from PyPI are present."""
|
|
1767
|
+
readme_path = Path("README.md")
|
|
1768
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1769
|
+
|
|
1770
|
+
# Get the installation section content
|
|
1771
|
+
section_content = self._get_installation_section_content(content)
|
|
1772
|
+
assert section_content, "Installation section not found"
|
|
1773
|
+
|
|
1774
|
+
# Verify pip install fishertools instruction
|
|
1775
|
+
assert "pip install fishertools" in section_content, \
|
|
1776
|
+
"pip install fishertools instruction not found"
|
|
1777
|
+
|
|
1778
|
+
def test_installation_section_formatting(self) -> None:
|
|
1779
|
+
"""Test that the installation section uses proper markdown formatting."""
|
|
1780
|
+
readme_path = Path("README.md")
|
|
1781
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1782
|
+
|
|
1783
|
+
# Find the installation section
|
|
1784
|
+
section_start = content.find("📦 Установка")
|
|
1785
|
+
assert section_start != -1, "Installation section not found"
|
|
1786
|
+
|
|
1787
|
+
# Verify it's a main section (##)
|
|
1788
|
+
assert "##" in content[max(0, section_start - 50):section_start + 50], \
|
|
1789
|
+
"Section should be formatted as a main section (##)"
|
|
1790
|
+
|
|
1791
|
+
def test_installation_section_has_warning_icon(self) -> None:
|
|
1792
|
+
"""Test that the PyPI status includes a warning icon."""
|
|
1793
|
+
readme_path = Path("README.md")
|
|
1794
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1795
|
+
|
|
1796
|
+
# Get the installation section content
|
|
1797
|
+
section_content = self._get_installation_section_content(content)
|
|
1798
|
+
assert section_content, "Installation section not found"
|
|
1799
|
+
|
|
1800
|
+
# Verify warning icon is present
|
|
1801
|
+
assert "⚠️" in section_content or "⚠" in section_content, \
|
|
1802
|
+
"Warning icon not found in PyPI status"
|
|
1803
|
+
|
|
1804
|
+
def test_installation_section_has_important_marker(self) -> None:
|
|
1805
|
+
"""Test that the PyPI status is marked as important."""
|
|
1806
|
+
readme_path = Path("README.md")
|
|
1807
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1808
|
+
|
|
1809
|
+
# Get the installation section content
|
|
1810
|
+
section_content = self._get_installation_section_content(content)
|
|
1811
|
+
assert section_content, "Installation section not found"
|
|
1812
|
+
|
|
1813
|
+
# Verify important marker
|
|
1814
|
+
assert "Важно" in section_content or "Important" in section_content, \
|
|
1815
|
+
"Important marker not found in PyPI status"
|
|
1816
|
+
|
|
1817
|
+
def test_git_clone_includes_cd_command(self) -> None:
|
|
1818
|
+
"""Test that git clone instructions include cd command."""
|
|
1819
|
+
readme_path = Path("README.md")
|
|
1820
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1821
|
+
|
|
1822
|
+
# Get the installation section content
|
|
1823
|
+
section_content = self._get_installation_section_content(content)
|
|
1824
|
+
assert section_content, "Installation section not found"
|
|
1825
|
+
|
|
1826
|
+
# Verify cd command is present
|
|
1827
|
+
assert "cd" in section_content, \
|
|
1828
|
+
"cd command not found in git clone instructions"
|
|
1829
|
+
|
|
1830
|
+
def test_installation_section_has_multiple_installation_methods(self) -> None:
|
|
1831
|
+
"""Test that multiple installation methods are provided."""
|
|
1832
|
+
readme_path = Path("README.md")
|
|
1833
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1834
|
+
|
|
1835
|
+
# Get the installation section content
|
|
1836
|
+
section_content = self._get_installation_section_content(content)
|
|
1837
|
+
assert section_content, "Installation section not found"
|
|
1838
|
+
|
|
1839
|
+
# Count the number of subsections (installation methods)
|
|
1840
|
+
subsection_count = section_content.count("###")
|
|
1841
|
+
|
|
1842
|
+
# Should have at least 3 subsections: status, from source, from PyPI, development
|
|
1843
|
+
assert subsection_count >= 3, \
|
|
1844
|
+
f"Installation section should have at least 3 subsections, found {subsection_count}"
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
class TestContentDuplicationProperty:
|
|
1848
|
+
"""Property-based tests for content duplication in README.
|
|
1849
|
+
|
|
1850
|
+
Feature: readme-enhancements-v0.3.1, Property 4: No Content Duplication
|
|
1851
|
+
**Validates: Requirements 7.3**
|
|
1852
|
+
"""
|
|
1853
|
+
|
|
1854
|
+
@given(st.just(None))
|
|
1855
|
+
def test_no_significant_content_duplication_property(self, _) -> None:
|
|
1856
|
+
"""Property test: No significant content duplication across sections.
|
|
1857
|
+
|
|
1858
|
+
**Validates: Requirements 7.3**
|
|
1859
|
+
|
|
1860
|
+
This property verifies that for any valid README.md file, information
|
|
1861
|
+
should not be significantly duplicated across multiple sections as specified
|
|
1862
|
+
in requirement 7.3: "THE System SHALL avoid duplication of information"
|
|
1863
|
+
|
|
1864
|
+
The property checks that:
|
|
1865
|
+
1. Installation instructions appear only in the "📦 Установка" section
|
|
1866
|
+
2. Error types are documented only in the "📖 Документация" section
|
|
1867
|
+
3. Learning tools are explained in detail only in the "📚 Обучающие инструменты v0.3.1" section
|
|
1868
|
+
4. Patterns are documented only in the "🔧 Готовые паттерны" section
|
|
1869
|
+
5. Key concepts are not repeated verbatim across multiple sections
|
|
1870
|
+
"""
|
|
1871
|
+
readme_path = Path("README.md")
|
|
1872
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1873
|
+
|
|
1874
|
+
# Property 1: Installation instructions should not be duplicated excessively
|
|
1875
|
+
# Count occurrences of "pip install" outside of code examples
|
|
1876
|
+
pip_install_count = content.count("pip install")
|
|
1877
|
+
# Should appear multiple times (different installation methods) but not excessively
|
|
1878
|
+
assert pip_install_count <= 10, \
|
|
1879
|
+
f"Property violated: 'pip install' appears {pip_install_count} times (excessive duplication)"
|
|
1880
|
+
|
|
1881
|
+
# Property 2: Error type explanations should not be duplicated
|
|
1882
|
+
# Count occurrences of "TypeError" - should appear mainly in documentation section
|
|
1883
|
+
type_error_count = content.count("TypeError")
|
|
1884
|
+
# Should appear a few times but not excessively
|
|
1885
|
+
assert type_error_count <= 5, \
|
|
1886
|
+
f"Property violated: 'TypeError' appears {type_error_count} times (excessive duplication)"
|
|
1887
|
+
|
|
1888
|
+
# Property 3: Learning tools documentation should not be duplicated
|
|
1889
|
+
# Count occurrences of "explain()" - should appear mainly in learning tools section
|
|
1890
|
+
explain_count = content.count("explain()")
|
|
1891
|
+
# Should appear multiple times but not excessively
|
|
1892
|
+
assert explain_count <= 15, \
|
|
1893
|
+
f"Property violated: 'explain()' appears {explain_count} times (excessive duplication)"
|
|
1894
|
+
|
|
1895
|
+
# Property 4: Patterns documentation should not be duplicated
|
|
1896
|
+
# Count occurrences of "simple_menu" - should appear mainly in patterns section
|
|
1897
|
+
simple_menu_count = content.count("simple_menu")
|
|
1898
|
+
# Should appear a few times but not excessively
|
|
1899
|
+
assert simple_menu_count <= 8, \
|
|
1900
|
+
f"Property violated: 'simple_menu' appears {simple_menu_count} times (excessive duplication)"
|
|
1901
|
+
|
|
1902
|
+
# Property 5: Check for verbatim duplication of long phrases
|
|
1903
|
+
# Split content into sentences and check for duplicates
|
|
1904
|
+
sentences = [s.strip() for s in content.split('.') if len(s.strip()) > 50]
|
|
1905
|
+
|
|
1906
|
+
# Count sentence occurrences
|
|
1907
|
+
sentence_counts = {}
|
|
1908
|
+
for sentence in sentences:
|
|
1909
|
+
# Normalize sentence for comparison
|
|
1910
|
+
normalized = sentence.lower().strip()
|
|
1911
|
+
if normalized:
|
|
1912
|
+
sentence_counts[normalized] = sentence_counts.get(normalized, 0) + 1
|
|
1913
|
+
|
|
1914
|
+
# Check that no long sentence appears more than twice
|
|
1915
|
+
# (allowing for some repetition in examples and documentation)
|
|
1916
|
+
for sentence, count in sentence_counts.items():
|
|
1917
|
+
assert count <= 2, \
|
|
1918
|
+
f"Property violated: Long phrase appears {count} times (verbatim duplication): '{sentence[:100]}...'"
|
|
1919
|
+
|
|
1920
|
+
# Property 6: Verify key sections exist and contain expected content
|
|
1921
|
+
assert "## 📦 Установка" in content, "Installation section not found"
|
|
1922
|
+
assert "## 📖 Документация" in content, "Documentation section not found"
|
|
1923
|
+
assert "## 📚 Обучающие инструменты v0.3.1" in content, "Learning tools section not found"
|
|
1924
|
+
assert "## 🔧 Готовые паттерны" in content, "Patterns section not found"
|
|
1925
|
+
|
|
1926
|
+
# Property 7: Verify that installation section contains installation keywords
|
|
1927
|
+
assert "pip install" in content, "Installation instructions not found"
|
|
1928
|
+
assert "git clone" in content, "Git clone instructions not found"
|
|
1929
|
+
|
|
1930
|
+
def test_installation_section_unique_content(self) -> None:
|
|
1931
|
+
"""Test that installation section has unique content not duplicated elsewhere."""
|
|
1932
|
+
readme_path = Path("README.md")
|
|
1933
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1934
|
+
|
|
1935
|
+
# Get installation section
|
|
1936
|
+
section_start = content.find("## 📦 Установка")
|
|
1937
|
+
assert section_start != -1, "Installation section not found"
|
|
1938
|
+
|
|
1939
|
+
section_end = content.find("##", section_start + 2)
|
|
1940
|
+
if section_end == -1:
|
|
1941
|
+
section_end = len(content)
|
|
1942
|
+
|
|
1943
|
+
section_content = content[section_start:section_end]
|
|
1944
|
+
|
|
1945
|
+
# Get content before and after installation section
|
|
1946
|
+
before_content = content[:section_start]
|
|
1947
|
+
after_content = content[section_end:]
|
|
1948
|
+
|
|
1949
|
+
# Check that key installation phrases don't appear excessively before
|
|
1950
|
+
# (they might appear in quick start, but not as detailed instructions)
|
|
1951
|
+
git_clone_before = before_content.count("git clone")
|
|
1952
|
+
|
|
1953
|
+
# git clone should appear mainly in installation section
|
|
1954
|
+
# Allow up to 1 mention before (in quick start or examples)
|
|
1955
|
+
assert git_clone_before <= 1, \
|
|
1956
|
+
"git clone instructions appear before installation section (duplication)"
|
|
1957
|
+
|
|
1958
|
+
def test_documentation_section_unique_error_types(self) -> None:
|
|
1959
|
+
"""Test that error types are documented uniquely in documentation section."""
|
|
1960
|
+
readme_path = Path("README.md")
|
|
1961
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1962
|
+
|
|
1963
|
+
# Get documentation section
|
|
1964
|
+
section_start = content.find("📖 Документация")
|
|
1965
|
+
assert section_start != -1, "Documentation section not found"
|
|
1966
|
+
|
|
1967
|
+
section_end = content.find("##", section_start + 1)
|
|
1968
|
+
if section_end == -1:
|
|
1969
|
+
section_end = len(content)
|
|
1970
|
+
|
|
1971
|
+
section_content = content[section_start:section_end]
|
|
1972
|
+
|
|
1973
|
+
# Get content before documentation section
|
|
1974
|
+
before_content = content[:section_start]
|
|
1975
|
+
|
|
1976
|
+
# Check that detailed error type explanations don't appear before
|
|
1977
|
+
# (they might be mentioned in examples, but not as detailed documentation)
|
|
1978
|
+
error_types = ["TypeError", "ValueError", "AttributeError", "IndexError", "KeyError"]
|
|
1979
|
+
|
|
1980
|
+
for error_type in error_types:
|
|
1981
|
+
# Count detailed explanations (with "возникает" or "occurs")
|
|
1982
|
+
detailed_before = before_content.count(f"{error_type}") - before_content.count(f"except {error_type}")
|
|
1983
|
+
detailed_in_section = section_content.count(f"{error_type}")
|
|
1984
|
+
|
|
1985
|
+
# Detailed explanations should be mainly in documentation section
|
|
1986
|
+
# (allowing for some mentions in examples)
|
|
1987
|
+
assert detailed_before <= 2, \
|
|
1988
|
+
f"Detailed explanation of {error_type} appears before documentation section (duplication)"
|
|
1989
|
+
|
|
1990
|
+
def test_learning_tools_section_unique_content(self) -> None:
|
|
1991
|
+
"""Test that learning tools documentation is unique to its section."""
|
|
1992
|
+
readme_path = Path("README.md")
|
|
1993
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
1994
|
+
|
|
1995
|
+
# Verify the detailed topics table exists in the README
|
|
1996
|
+
assert "| Категория | Тема | Описание |" in content, \
|
|
1997
|
+
"Detailed topics table not found in README"
|
|
1998
|
+
|
|
1999
|
+
# Verify it's in the learning tools section (after the learning tools header)
|
|
2000
|
+
learning_tools_start = content.find("## 📚 Обучающие инструменты v0.3.1")
|
|
2001
|
+
table_start = content.find("| Категория | Тема | Описание |")
|
|
2002
|
+
|
|
2003
|
+
assert learning_tools_start != -1, "Learning tools section not found"
|
|
2004
|
+
assert table_start != -1, "Topics table not found"
|
|
2005
|
+
assert table_start > learning_tools_start, \
|
|
2006
|
+
"Topics table should be in learning tools section"
|
|
2007
|
+
|
|
2008
|
+
def test_patterns_section_unique_content(self) -> None:
|
|
2009
|
+
"""Test that patterns documentation is unique to its section."""
|
|
2010
|
+
readme_path = Path("README.md")
|
|
2011
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
2012
|
+
|
|
2013
|
+
# Get patterns section
|
|
2014
|
+
section_start = content.find("🔧 Готовые паттерны")
|
|
2015
|
+
assert section_start != -1, "Patterns section not found"
|
|
2016
|
+
|
|
2017
|
+
section_end = content.find("##", section_start + 1)
|
|
2018
|
+
if section_end == -1:
|
|
2019
|
+
section_end = len(content)
|
|
2020
|
+
|
|
2021
|
+
section_content = content[section_start:section_end]
|
|
2022
|
+
|
|
2023
|
+
# Get content before patterns section
|
|
2024
|
+
before_content = content[:section_start]
|
|
2025
|
+
|
|
2026
|
+
# Check that detailed pattern documentation doesn't appear before
|
|
2027
|
+
patterns = ["simple_menu", "JSONStorage", "SimpleLogger", "SimpleCLI"]
|
|
2028
|
+
|
|
2029
|
+
for pattern in patterns:
|
|
2030
|
+
# Count detailed explanations (with "Особенности" or "Features")
|
|
2031
|
+
detailed_before = before_content.count(f"{pattern}") - before_content.count(f"from fishertools.patterns import")
|
|
2032
|
+
detailed_in_section = section_content.count(f"{pattern}")
|
|
2033
|
+
|
|
2034
|
+
# Detailed documentation should be mainly in patterns section
|
|
2035
|
+
assert detailed_before <= 2, \
|
|
2036
|
+
f"Detailed documentation of {pattern} appears before patterns section (duplication)"
|