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.
Files changed (69) hide show
  1. fishertools/__init__.py +16 -5
  2. fishertools/errors/__init__.py +11 -3
  3. fishertools/errors/exception_types.py +282 -0
  4. fishertools/errors/explainer.py +87 -1
  5. fishertools/errors/models.py +73 -1
  6. fishertools/errors/patterns.py +40 -0
  7. fishertools/examples/cli_example.py +156 -0
  8. fishertools/examples/learn_example.py +65 -0
  9. fishertools/examples/logger_example.py +176 -0
  10. fishertools/examples/menu_example.py +101 -0
  11. fishertools/examples/storage_example.py +175 -0
  12. fishertools/input_utils.py +185 -0
  13. fishertools/learn/__init__.py +19 -2
  14. fishertools/learn/examples.py +88 -1
  15. fishertools/learn/knowledge_engine.py +321 -0
  16. fishertools/learn/repl/__init__.py +19 -0
  17. fishertools/learn/repl/cli.py +31 -0
  18. fishertools/learn/repl/code_sandbox.py +229 -0
  19. fishertools/learn/repl/command_handler.py +544 -0
  20. fishertools/learn/repl/command_parser.py +165 -0
  21. fishertools/learn/repl/engine.py +479 -0
  22. fishertools/learn/repl/models.py +121 -0
  23. fishertools/learn/repl/session_manager.py +284 -0
  24. fishertools/learn/repl/test_code_sandbox.py +261 -0
  25. fishertools/learn/repl/test_code_sandbox_pbt.py +148 -0
  26. fishertools/learn/repl/test_command_handler.py +224 -0
  27. fishertools/learn/repl/test_command_handler_pbt.py +189 -0
  28. fishertools/learn/repl/test_command_parser.py +160 -0
  29. fishertools/learn/repl/test_command_parser_pbt.py +100 -0
  30. fishertools/learn/repl/test_engine.py +190 -0
  31. fishertools/learn/repl/test_session_manager.py +310 -0
  32. fishertools/learn/repl/test_session_manager_pbt.py +182 -0
  33. fishertools/learn/test_knowledge_engine.py +241 -0
  34. fishertools/learn/test_knowledge_engine_pbt.py +180 -0
  35. fishertools/patterns/__init__.py +46 -0
  36. fishertools/patterns/cli.py +175 -0
  37. fishertools/patterns/logger.py +140 -0
  38. fishertools/patterns/menu.py +99 -0
  39. fishertools/patterns/storage.py +127 -0
  40. fishertools/readme_transformer.py +631 -0
  41. fishertools/safe/__init__.py +6 -1
  42. fishertools/safe/files.py +329 -1
  43. fishertools/transform_readme.py +105 -0
  44. fishertools-0.4.0.dist-info/METADATA +104 -0
  45. fishertools-0.4.0.dist-info/RECORD +131 -0
  46. {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/WHEEL +1 -1
  47. tests/test_documentation_properties.py +329 -0
  48. tests/test_documentation_structure.py +349 -0
  49. tests/test_errors/test_exception_types.py +446 -0
  50. tests/test_errors/test_exception_types_pbt.py +333 -0
  51. tests/test_errors/test_patterns.py +52 -0
  52. tests/test_input_utils/__init__.py +1 -0
  53. tests/test_input_utils/test_input_utils.py +65 -0
  54. tests/test_learn/test_examples.py +179 -1
  55. tests/test_learn/test_explain_properties.py +307 -0
  56. tests/test_patterns_cli.py +611 -0
  57. tests/test_patterns_docstrings.py +473 -0
  58. tests/test_patterns_logger.py +465 -0
  59. tests/test_patterns_menu.py +440 -0
  60. tests/test_patterns_storage.py +447 -0
  61. tests/test_readme_enhancements_v0_3_1.py +2036 -0
  62. tests/test_readme_transformer/__init__.py +1 -0
  63. tests/test_readme_transformer/test_readme_infrastructure.py +1023 -0
  64. tests/test_readme_transformer/test_transform_readme_integration.py +431 -0
  65. tests/test_safe/test_files.py +726 -1
  66. fishertools-0.2.1.dist-info/METADATA +0 -256
  67. fishertools-0.2.1.dist-info/RECORD +0 -81
  68. {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/licenses/LICENSE +0 -0
  69. {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)"