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,1023 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for README transformation infrastructure.
|
|
3
|
+
|
|
4
|
+
Tests the core components: ReadmeParser, BackupManager, ReadmeStructure,
|
|
5
|
+
and ReadmeTransformer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from fishertools.readme_transformer import (
|
|
15
|
+
BackupManager,
|
|
16
|
+
FeatureEntry,
|
|
17
|
+
ReadmeParser,
|
|
18
|
+
ReadmeSection,
|
|
19
|
+
ReadmeStructure,
|
|
20
|
+
ReadmeTransformer,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestReadmeParser:
|
|
25
|
+
"""Tests for ReadmeParser class."""
|
|
26
|
+
|
|
27
|
+
def test_read_file_success(self, tmp_path: Path) -> None:
|
|
28
|
+
"""Test successful reading of README file."""
|
|
29
|
+
readme_path = tmp_path / "README.md"
|
|
30
|
+
test_content = "# Test README\n\nThis is a test."
|
|
31
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
parser = ReadmeParser(str(readme_path))
|
|
34
|
+
content = parser.read_file()
|
|
35
|
+
|
|
36
|
+
assert content == test_content
|
|
37
|
+
|
|
38
|
+
def test_read_file_not_found(self, tmp_path: Path) -> None:
|
|
39
|
+
"""Test error when README file does not exist."""
|
|
40
|
+
readme_path = tmp_path / "nonexistent.md"
|
|
41
|
+
|
|
42
|
+
parser = ReadmeParser(str(readme_path))
|
|
43
|
+
|
|
44
|
+
with pytest.raises(FileNotFoundError):
|
|
45
|
+
parser.read_file()
|
|
46
|
+
|
|
47
|
+
def test_extract_first_sentence(self, tmp_path: Path) -> None:
|
|
48
|
+
"""Test extraction of first introductory sentence."""
|
|
49
|
+
readme_path = tmp_path / "README.md"
|
|
50
|
+
test_content = "First sentence here\n\n# Heading\n\nMore content"
|
|
51
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
parser = ReadmeParser(str(readme_path))
|
|
54
|
+
parser.read_file()
|
|
55
|
+
first_sentence = parser.extract_first_sentence()
|
|
56
|
+
|
|
57
|
+
assert first_sentence == "First sentence here"
|
|
58
|
+
|
|
59
|
+
def test_extract_first_sentence_with_heading(self, tmp_path: Path) -> None:
|
|
60
|
+
"""Test extraction skips headings to find first sentence."""
|
|
61
|
+
readme_path = tmp_path / "README.md"
|
|
62
|
+
test_content = "# Title\n\nFirst real sentence\n\nMore content"
|
|
63
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
64
|
+
|
|
65
|
+
parser = ReadmeParser(str(readme_path))
|
|
66
|
+
parser.read_file()
|
|
67
|
+
first_sentence = parser.extract_first_sentence()
|
|
68
|
+
|
|
69
|
+
assert first_sentence == "First real sentence"
|
|
70
|
+
|
|
71
|
+
def test_parse_sections(self, tmp_path: Path) -> None:
|
|
72
|
+
"""Test parsing of README sections."""
|
|
73
|
+
readme_path = tmp_path / "README.md"
|
|
74
|
+
test_content = "# Heading 1\n\nContent 1\n\n## Heading 2\n\nContent 2"
|
|
75
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
76
|
+
|
|
77
|
+
parser = ReadmeParser(str(readme_path))
|
|
78
|
+
parser.read_file()
|
|
79
|
+
sections = parser.parse_sections()
|
|
80
|
+
|
|
81
|
+
assert len(sections) >= 2
|
|
82
|
+
assert any(s.title == "Heading 1" for s in sections)
|
|
83
|
+
assert any(s.title == "Heading 2" for s in sections)
|
|
84
|
+
|
|
85
|
+
def test_parse_sections_preserves_content(self, tmp_path: Path) -> None:
|
|
86
|
+
"""Test that parsing preserves section content."""
|
|
87
|
+
readme_path = tmp_path / "README.md"
|
|
88
|
+
test_content = "# Section\n\nThis is content\nWith multiple lines"
|
|
89
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
90
|
+
|
|
91
|
+
parser = ReadmeParser(str(readme_path))
|
|
92
|
+
parser.read_file()
|
|
93
|
+
sections = parser.parse_sections()
|
|
94
|
+
|
|
95
|
+
assert any("This is content" in s.content for s in sections)
|
|
96
|
+
|
|
97
|
+
def test_extract_detailed_content(self, tmp_path: Path) -> None:
|
|
98
|
+
"""Test extraction of detailed content after introduction."""
|
|
99
|
+
readme_path = tmp_path / "README.md"
|
|
100
|
+
test_content = "Introduction line\n\n# Section\n\nDetailed content here"
|
|
101
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
102
|
+
|
|
103
|
+
parser = ReadmeParser(str(readme_path))
|
|
104
|
+
parser.read_file()
|
|
105
|
+
detailed = parser.extract_detailed_content()
|
|
106
|
+
|
|
107
|
+
assert "Detailed content here" in detailed
|
|
108
|
+
assert "Introduction line" not in detailed or detailed.count("Introduction") == 0
|
|
109
|
+
|
|
110
|
+
def test_identify_introduction_boundary(self, tmp_path: Path) -> None:
|
|
111
|
+
"""Test identification of introduction boundary."""
|
|
112
|
+
readme_path = tmp_path / "README.md"
|
|
113
|
+
test_content = "First intro\n\n# Heading\n\nContent"
|
|
114
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
115
|
+
|
|
116
|
+
parser = ReadmeParser(str(readme_path))
|
|
117
|
+
parser.read_file()
|
|
118
|
+
boundary = parser.identify_introduction_boundary()
|
|
119
|
+
|
|
120
|
+
assert boundary > 0
|
|
121
|
+
|
|
122
|
+
def test_identify_feature_list_section(self, tmp_path: Path) -> None:
|
|
123
|
+
"""Test identification of feature list section."""
|
|
124
|
+
readme_path = tmp_path / "README.md"
|
|
125
|
+
test_content = "Intro\n\n## Основные возможности\n\nFeature 1\nFeature 2\n\n## Other"
|
|
126
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
127
|
+
|
|
128
|
+
parser = ReadmeParser(str(readme_path))
|
|
129
|
+
parser.read_file()
|
|
130
|
+
feature_section = parser.identify_feature_list_section()
|
|
131
|
+
|
|
132
|
+
assert feature_section is not None
|
|
133
|
+
assert feature_section[0] >= 0
|
|
134
|
+
assert feature_section[1] > feature_section[0]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TestBackupManager:
|
|
138
|
+
"""Tests for BackupManager class."""
|
|
139
|
+
|
|
140
|
+
def test_create_backup_success(self, tmp_path: Path) -> None:
|
|
141
|
+
"""Test successful backup creation."""
|
|
142
|
+
readme_path = tmp_path / "README.md"
|
|
143
|
+
readme_path.write_text("Test content", encoding="utf-8")
|
|
144
|
+
|
|
145
|
+
backup_manager = BackupManager(str(readme_path), str(tmp_path / "backups"))
|
|
146
|
+
backup_path = backup_manager.create_backup()
|
|
147
|
+
|
|
148
|
+
assert backup_path.exists()
|
|
149
|
+
assert backup_path.read_text(encoding="utf-8") == "Test content"
|
|
150
|
+
|
|
151
|
+
def test_create_backup_file_not_found(self, tmp_path: Path) -> None:
|
|
152
|
+
"""Test error when README file does not exist."""
|
|
153
|
+
readme_path = tmp_path / "nonexistent.md"
|
|
154
|
+
|
|
155
|
+
backup_manager = BackupManager(str(readme_path), str(tmp_path / "backups"))
|
|
156
|
+
|
|
157
|
+
with pytest.raises(FileNotFoundError):
|
|
158
|
+
backup_manager.create_backup()
|
|
159
|
+
|
|
160
|
+
def test_list_backups(self, tmp_path: Path) -> None:
|
|
161
|
+
"""Test listing of backup files."""
|
|
162
|
+
import time
|
|
163
|
+
|
|
164
|
+
readme_path = tmp_path / "README.md"
|
|
165
|
+
readme_path.write_text("Test content", encoding="utf-8")
|
|
166
|
+
|
|
167
|
+
backup_manager = BackupManager(str(readme_path), str(tmp_path / "backups"))
|
|
168
|
+
|
|
169
|
+
# Create multiple backups with delay to ensure different timestamps
|
|
170
|
+
backup_manager.create_backup()
|
|
171
|
+
time.sleep(1.1) # Sleep to ensure different timestamp
|
|
172
|
+
backup_manager.create_backup()
|
|
173
|
+
|
|
174
|
+
backups = backup_manager.list_backups()
|
|
175
|
+
|
|
176
|
+
assert len(backups) >= 2
|
|
177
|
+
|
|
178
|
+
def test_restore_backup(self, tmp_path: Path) -> None:
|
|
179
|
+
"""Test restoration from backup."""
|
|
180
|
+
readme_path = tmp_path / "README.md"
|
|
181
|
+
original_content = "Original content"
|
|
182
|
+
readme_path.write_text(original_content, encoding="utf-8")
|
|
183
|
+
|
|
184
|
+
backup_manager = BackupManager(str(readme_path), str(tmp_path / "backups"))
|
|
185
|
+
backup_path = backup_manager.create_backup()
|
|
186
|
+
|
|
187
|
+
# Modify the README
|
|
188
|
+
readme_path.write_text("Modified content", encoding="utf-8")
|
|
189
|
+
|
|
190
|
+
# Restore from backup
|
|
191
|
+
backup_manager.restore_backup(backup_path)
|
|
192
|
+
|
|
193
|
+
assert readme_path.read_text(encoding="utf-8") == original_content
|
|
194
|
+
|
|
195
|
+
def test_restore_backup_not_found(self, tmp_path: Path) -> None:
|
|
196
|
+
"""Test error when backup file does not exist."""
|
|
197
|
+
readme_path = tmp_path / "README.md"
|
|
198
|
+
readme_path.write_text("Test content", encoding="utf-8")
|
|
199
|
+
|
|
200
|
+
backup_manager = BackupManager(str(readme_path), str(tmp_path / "backups"))
|
|
201
|
+
nonexistent_backup = tmp_path / "backups" / "nonexistent.md"
|
|
202
|
+
|
|
203
|
+
with pytest.raises(FileNotFoundError):
|
|
204
|
+
backup_manager.restore_backup(nonexistent_backup)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestReadmeStructure:
|
|
208
|
+
"""Tests for ReadmeStructure class."""
|
|
209
|
+
|
|
210
|
+
def test_set_introduction(self) -> None:
|
|
211
|
+
"""Test setting introduction section."""
|
|
212
|
+
structure = ReadmeStructure()
|
|
213
|
+
intro = "This is the introduction"
|
|
214
|
+
|
|
215
|
+
structure.set_introduction(intro)
|
|
216
|
+
|
|
217
|
+
assert structure.introduction == intro
|
|
218
|
+
|
|
219
|
+
def test_set_installation_block_default(self) -> None:
|
|
220
|
+
"""Test setting installation block with default command."""
|
|
221
|
+
structure = ReadmeStructure()
|
|
222
|
+
|
|
223
|
+
structure.set_installation_block()
|
|
224
|
+
|
|
225
|
+
assert "pip install fishertools" in structure.installation_block
|
|
226
|
+
assert structure.installation_block.startswith("```bash")
|
|
227
|
+
assert structure.installation_block.endswith("```")
|
|
228
|
+
|
|
229
|
+
def test_set_installation_block_custom(self) -> None:
|
|
230
|
+
"""Test setting installation block with custom command."""
|
|
231
|
+
structure = ReadmeStructure()
|
|
232
|
+
custom_command = "pip install custom-package"
|
|
233
|
+
|
|
234
|
+
structure.set_installation_block(custom_command)
|
|
235
|
+
|
|
236
|
+
assert custom_command in structure.installation_block
|
|
237
|
+
|
|
238
|
+
def test_set_feature_table(self) -> None:
|
|
239
|
+
"""Test setting feature table."""
|
|
240
|
+
structure = ReadmeStructure()
|
|
241
|
+
features = [
|
|
242
|
+
FeatureEntry("Объяснить ошибку", "explain_error(e)"),
|
|
243
|
+
FeatureEntry("Безопасно читать файл", "safe_read_file(path)"),
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
structure.set_feature_table(features)
|
|
247
|
+
|
|
248
|
+
assert "Задача" in structure.feature_table
|
|
249
|
+
assert "Что вызвать" in structure.feature_table
|
|
250
|
+
assert "explain_error(e)" in structure.feature_table
|
|
251
|
+
assert "safe_read_file(path)" in structure.feature_table
|
|
252
|
+
|
|
253
|
+
def test_set_feature_table_format(self) -> None:
|
|
254
|
+
"""Test that feature table uses proper markdown format."""
|
|
255
|
+
structure = ReadmeStructure()
|
|
256
|
+
features = [FeatureEntry("Task", "function()")]
|
|
257
|
+
|
|
258
|
+
structure.set_feature_table(features)
|
|
259
|
+
|
|
260
|
+
lines = structure.feature_table.split("\n")
|
|
261
|
+
assert lines[0].startswith("|")
|
|
262
|
+
assert lines[1].startswith("|")
|
|
263
|
+
|
|
264
|
+
def test_set_target_audience_default(self) -> None:
|
|
265
|
+
"""Test setting target audience with default bullets."""
|
|
266
|
+
structure = ReadmeStructure()
|
|
267
|
+
|
|
268
|
+
structure.set_target_audience()
|
|
269
|
+
|
|
270
|
+
assert "Для кого эта библиотека" in structure.target_audience
|
|
271
|
+
assert "Ты только начал изучать Python" in structure.target_audience
|
|
272
|
+
assert "Сообщения об ошибках" in structure.target_audience
|
|
273
|
+
assert "нормальном русском" in structure.target_audience
|
|
274
|
+
|
|
275
|
+
def test_set_target_audience_custom(self) -> None:
|
|
276
|
+
"""Test setting target audience with custom bullets."""
|
|
277
|
+
structure = ReadmeStructure()
|
|
278
|
+
custom_bullets = ["Bullet 1", "Bullet 2", "Bullet 3"]
|
|
279
|
+
|
|
280
|
+
structure.set_target_audience(bullets=custom_bullets)
|
|
281
|
+
|
|
282
|
+
for bullet in custom_bullets:
|
|
283
|
+
assert bullet in structure.target_audience
|
|
284
|
+
|
|
285
|
+
def test_set_existing_content(self) -> None:
|
|
286
|
+
"""Test setting existing content."""
|
|
287
|
+
structure = ReadmeStructure()
|
|
288
|
+
content = "## Existing Section\n\nThis is existing content"
|
|
289
|
+
|
|
290
|
+
structure.set_existing_content(content)
|
|
291
|
+
|
|
292
|
+
assert structure.existing_content == content
|
|
293
|
+
|
|
294
|
+
def test_assemble_structure(self) -> None:
|
|
295
|
+
"""Test assembling all sections into final content."""
|
|
296
|
+
structure = ReadmeStructure()
|
|
297
|
+
structure.set_introduction("Introduction")
|
|
298
|
+
structure.set_installation_block()
|
|
299
|
+
structure.set_feature_table([FeatureEntry("Task", "func()")])
|
|
300
|
+
structure.set_target_audience()
|
|
301
|
+
structure.set_existing_content("Existing content")
|
|
302
|
+
|
|
303
|
+
assembled = structure.assemble()
|
|
304
|
+
|
|
305
|
+
assert "Introduction" in assembled
|
|
306
|
+
assert "pip install fishertools" in assembled
|
|
307
|
+
assert "Task" in assembled
|
|
308
|
+
assert "Для кого эта библиотека" in assembled
|
|
309
|
+
assert "Existing content" in assembled
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TestReadmeTransformer:
|
|
313
|
+
"""Tests for ReadmeTransformer class."""
|
|
314
|
+
|
|
315
|
+
def test_validate_readme_exists(self, tmp_path: Path) -> None:
|
|
316
|
+
"""Test validation of README existence."""
|
|
317
|
+
readme_path = tmp_path / "README.md"
|
|
318
|
+
readme_path.write_text("Test", encoding="utf-8")
|
|
319
|
+
|
|
320
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
321
|
+
|
|
322
|
+
assert transformer.validate_readme_exists() is True
|
|
323
|
+
|
|
324
|
+
def test_validate_readme_not_exists(self, tmp_path: Path) -> None:
|
|
325
|
+
"""Test validation when README does not exist."""
|
|
326
|
+
readme_path = tmp_path / "nonexistent.md"
|
|
327
|
+
|
|
328
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
329
|
+
|
|
330
|
+
assert transformer.validate_readme_exists() is False
|
|
331
|
+
|
|
332
|
+
def test_parse_readme(self, tmp_path: Path) -> None:
|
|
333
|
+
"""Test parsing README file."""
|
|
334
|
+
readme_path = tmp_path / "README.md"
|
|
335
|
+
test_content = "# Title\n\nContent here"
|
|
336
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
337
|
+
|
|
338
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
339
|
+
transformer.parse_readme()
|
|
340
|
+
|
|
341
|
+
assert transformer.parser.content == test_content
|
|
342
|
+
|
|
343
|
+
def test_extract_content(self, tmp_path: Path) -> None:
|
|
344
|
+
"""Test extraction of introduction and existing content."""
|
|
345
|
+
readme_path = tmp_path / "README.md"
|
|
346
|
+
test_content = "First sentence\n\n# Heading\n\nMore content"
|
|
347
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
348
|
+
|
|
349
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
350
|
+
introduction, existing = transformer.extract_content()
|
|
351
|
+
|
|
352
|
+
assert "First sentence" in introduction
|
|
353
|
+
assert "More content" in existing
|
|
354
|
+
|
|
355
|
+
def test_create_backup(self, tmp_path: Path) -> None:
|
|
356
|
+
"""Test backup creation."""
|
|
357
|
+
readme_path = tmp_path / "README.md"
|
|
358
|
+
readme_path.write_text("Test content", encoding="utf-8")
|
|
359
|
+
|
|
360
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
361
|
+
backup_path = transformer.create_backup()
|
|
362
|
+
|
|
363
|
+
assert backup_path.exists()
|
|
364
|
+
|
|
365
|
+
def test_transform_basic(self, tmp_path: Path) -> None:
|
|
366
|
+
"""Test basic README transformation."""
|
|
367
|
+
readme_path = tmp_path / "README.md"
|
|
368
|
+
test_content = "Introduction\n\n# Section\n\nContent"
|
|
369
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
370
|
+
|
|
371
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
372
|
+
transformed = transformer.transform()
|
|
373
|
+
|
|
374
|
+
assert "Introduction" in transformed
|
|
375
|
+
assert "pip install fishertools" in transformed
|
|
376
|
+
assert "Для кого эта библиотека" in transformed
|
|
377
|
+
|
|
378
|
+
def test_transform_with_features(self, tmp_path: Path) -> None:
|
|
379
|
+
"""Test transformation with custom features."""
|
|
380
|
+
readme_path = tmp_path / "README.md"
|
|
381
|
+
readme_path.write_text("Introduction\n\nContent", encoding="utf-8")
|
|
382
|
+
|
|
383
|
+
features = [
|
|
384
|
+
FeatureEntry("Feature 1", "func1()"),
|
|
385
|
+
FeatureEntry("Feature 2", "func2()"),
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
389
|
+
transformed = transformer.transform(features=features)
|
|
390
|
+
|
|
391
|
+
assert "Feature 1" in transformed
|
|
392
|
+
assert "func1()" in transformed
|
|
393
|
+
assert "Feature 2" in transformed
|
|
394
|
+
assert "func2()" in transformed
|
|
395
|
+
|
|
396
|
+
def test_write_transformed_readme(self, tmp_path: Path) -> None:
|
|
397
|
+
"""Test writing transformed README to file."""
|
|
398
|
+
readme_path = tmp_path / "README.md"
|
|
399
|
+
readme_path.write_text("Original", encoding="utf-8")
|
|
400
|
+
|
|
401
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
402
|
+
new_content = "Transformed content"
|
|
403
|
+
transformer.write_transformed_readme(new_content)
|
|
404
|
+
|
|
405
|
+
assert readme_path.read_text(encoding="utf-8") == new_content
|
|
406
|
+
|
|
407
|
+
def test_full_transformation_workflow(self, tmp_path: Path) -> None:
|
|
408
|
+
"""Test complete transformation workflow."""
|
|
409
|
+
readme_path = tmp_path / "README.md"
|
|
410
|
+
original_content = "Original Introduction\n\n# Section\n\nOriginal content"
|
|
411
|
+
readme_path.write_text(original_content, encoding="utf-8")
|
|
412
|
+
|
|
413
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
414
|
+
|
|
415
|
+
# Create backup
|
|
416
|
+
backup_path = transformer.create_backup()
|
|
417
|
+
assert backup_path.exists()
|
|
418
|
+
|
|
419
|
+
# Transform
|
|
420
|
+
features = [FeatureEntry("Task", "function()")]
|
|
421
|
+
transformed = transformer.transform(features=features)
|
|
422
|
+
|
|
423
|
+
# Write
|
|
424
|
+
transformer.write_transformed_readme(transformed)
|
|
425
|
+
|
|
426
|
+
# Verify
|
|
427
|
+
result = readme_path.read_text(encoding="utf-8")
|
|
428
|
+
assert "pip install fishertools" in result
|
|
429
|
+
assert "Для кого эта библиотека" in result
|
|
430
|
+
assert "Task" in result
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# Property-based tests using hypothesis
|
|
434
|
+
from hypothesis import given, strategies as st, settings, HealthCheck
|
|
435
|
+
import tempfile
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class TestReadmeContentPreservation:
|
|
439
|
+
"""Property-based tests for content preservation."""
|
|
440
|
+
|
|
441
|
+
@given(
|
|
442
|
+
intro=st.text(
|
|
443
|
+
min_size=1,
|
|
444
|
+
max_size=100,
|
|
445
|
+
alphabet=st.characters(
|
|
446
|
+
blacklist_categories=("Cc", "Cs"),
|
|
447
|
+
blacklist_characters="\x00\r",
|
|
448
|
+
),
|
|
449
|
+
),
|
|
450
|
+
content=st.text(
|
|
451
|
+
min_size=1,
|
|
452
|
+
max_size=500,
|
|
453
|
+
alphabet=st.characters(
|
|
454
|
+
blacklist_categories=("Cc", "Cs"),
|
|
455
|
+
blacklist_characters="\x00\r",
|
|
456
|
+
),
|
|
457
|
+
),
|
|
458
|
+
)
|
|
459
|
+
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
|
|
460
|
+
def test_property_content_preservation(self, intro: str, content: str) -> None:
|
|
461
|
+
"""
|
|
462
|
+
Property 5: Content Preservation
|
|
463
|
+
|
|
464
|
+
**Validates: Requirements 4.1, 4.3**
|
|
465
|
+
|
|
466
|
+
For any README transformation, the original introductory sentence
|
|
467
|
+
should remain as the first content and all existing detailed
|
|
468
|
+
documentation should be preserved without loss.
|
|
469
|
+
"""
|
|
470
|
+
# Skip if intro or content are empty after stripping
|
|
471
|
+
if not intro.strip() or not content.strip():
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
475
|
+
readme_path = Path(tmp_dir) / "README.md"
|
|
476
|
+
test_content = f"{intro}\n\n# Section\n\n{content}"
|
|
477
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
478
|
+
|
|
479
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
480
|
+
transformed = transformer.transform()
|
|
481
|
+
|
|
482
|
+
# Verify that the introduction is preserved (or its first sentence)
|
|
483
|
+
intro_stripped = intro.strip()
|
|
484
|
+
first_sentence = intro_stripped.split(".")[0].split("!")[0].split("?")[0].strip()
|
|
485
|
+
|
|
486
|
+
assert (
|
|
487
|
+
intro_stripped in transformed
|
|
488
|
+
or first_sentence in transformed
|
|
489
|
+
), f"Introduction '{intro_stripped}' not found in transformed content"
|
|
490
|
+
|
|
491
|
+
# Verify that the existing content is preserved
|
|
492
|
+
content_stripped = content.strip()
|
|
493
|
+
assert (
|
|
494
|
+
content_stripped in transformed
|
|
495
|
+
), f"Content '{content_stripped}' not found in transformed content"
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
class TestInstallationBlockProperty:
|
|
499
|
+
"""Property-based tests for installation block."""
|
|
500
|
+
|
|
501
|
+
@given(
|
|
502
|
+
intro=st.text(
|
|
503
|
+
min_size=1,
|
|
504
|
+
max_size=100,
|
|
505
|
+
alphabet=st.characters(
|
|
506
|
+
blacklist_categories=("Cc", "Cs"),
|
|
507
|
+
blacklist_characters="\x00\r",
|
|
508
|
+
),
|
|
509
|
+
),
|
|
510
|
+
content=st.text(
|
|
511
|
+
min_size=1,
|
|
512
|
+
max_size=500,
|
|
513
|
+
alphabet=st.characters(
|
|
514
|
+
blacklist_categories=("Cc", "Cs"),
|
|
515
|
+
blacklist_characters="\x00\r",
|
|
516
|
+
),
|
|
517
|
+
),
|
|
518
|
+
)
|
|
519
|
+
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
|
|
520
|
+
def test_property_installation_block_content_and_format(
|
|
521
|
+
self, intro: str, content: str
|
|
522
|
+
) -> None:
|
|
523
|
+
"""
|
|
524
|
+
Property 2: Installation Block Content and Format
|
|
525
|
+
|
|
526
|
+
**Validates: Requirements 1.2, 1.3**
|
|
527
|
+
|
|
528
|
+
For any README file, the installation block should contain exactly
|
|
529
|
+
"pip install fishertools" and be formatted as a proper markdown code block.
|
|
530
|
+
"""
|
|
531
|
+
# Skip if intro or content are empty after stripping
|
|
532
|
+
if not intro.strip() or not content.strip():
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
536
|
+
readme_path = Path(tmp_dir) / "README.md"
|
|
537
|
+
test_content = f"{intro}\n\n# Section\n\n{content}"
|
|
538
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
539
|
+
|
|
540
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
541
|
+
transformed = transformer.transform()
|
|
542
|
+
|
|
543
|
+
# Verify installation block contains the exact command
|
|
544
|
+
assert (
|
|
545
|
+
"pip install fishertools" in transformed
|
|
546
|
+
), "Installation command not found in transformed content"
|
|
547
|
+
|
|
548
|
+
# Verify it's formatted as a code block
|
|
549
|
+
assert (
|
|
550
|
+
"```bash" in transformed
|
|
551
|
+
), "Installation block not formatted as bash code block"
|
|
552
|
+
assert (
|
|
553
|
+
"```" in transformed
|
|
554
|
+
), "Installation block missing closing code block marker"
|
|
555
|
+
|
|
556
|
+
# Verify the code block structure
|
|
557
|
+
lines = transformed.split("\n")
|
|
558
|
+
bash_block_start = None
|
|
559
|
+
bash_block_end = None
|
|
560
|
+
|
|
561
|
+
for i, line in enumerate(lines):
|
|
562
|
+
if line.strip() == "```bash":
|
|
563
|
+
bash_block_start = i
|
|
564
|
+
elif bash_block_start is not None and line.strip() == "```":
|
|
565
|
+
bash_block_end = i
|
|
566
|
+
break
|
|
567
|
+
|
|
568
|
+
assert (
|
|
569
|
+
bash_block_start is not None
|
|
570
|
+
), "Opening bash code block marker not found"
|
|
571
|
+
assert (
|
|
572
|
+
bash_block_end is not None
|
|
573
|
+
), "Closing code block marker not found"
|
|
574
|
+
assert (
|
|
575
|
+
bash_block_end > bash_block_start
|
|
576
|
+
), "Code block end before start"
|
|
577
|
+
|
|
578
|
+
# Verify the command is between the markers
|
|
579
|
+
command_found = False
|
|
580
|
+
for i in range(bash_block_start + 1, bash_block_end):
|
|
581
|
+
if "pip install fishertools" in lines[i]:
|
|
582
|
+
command_found = True
|
|
583
|
+
break
|
|
584
|
+
|
|
585
|
+
assert (
|
|
586
|
+
command_found
|
|
587
|
+
), "Installation command not found between code block markers"
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
class TestFeatureTableProperty:
|
|
591
|
+
"""Property-based tests for feature table."""
|
|
592
|
+
|
|
593
|
+
@given(
|
|
594
|
+
intro=st.text(
|
|
595
|
+
min_size=1,
|
|
596
|
+
max_size=100,
|
|
597
|
+
alphabet=st.characters(
|
|
598
|
+
blacklist_categories=("Cc", "Cs"),
|
|
599
|
+
blacklist_characters="\x00\r|",
|
|
600
|
+
),
|
|
601
|
+
),
|
|
602
|
+
content=st.text(
|
|
603
|
+
min_size=1,
|
|
604
|
+
max_size=500,
|
|
605
|
+
alphabet=st.characters(
|
|
606
|
+
blacklist_categories=("Cc", "Cs"),
|
|
607
|
+
blacklist_characters="\x00\r|",
|
|
608
|
+
),
|
|
609
|
+
),
|
|
610
|
+
)
|
|
611
|
+
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
|
|
612
|
+
def test_property_feature_table_structure_and_content(
|
|
613
|
+
self, intro: str, content: str
|
|
614
|
+
) -> None:
|
|
615
|
+
"""
|
|
616
|
+
Property 3: Feature Table Structure and Content
|
|
617
|
+
|
|
618
|
+
**Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5**
|
|
619
|
+
|
|
620
|
+
For any README transformation, the feature table should have columns
|
|
621
|
+
"Задача" and "Что вызвать", use proper markdown table format, and
|
|
622
|
+
contain entries for "explain_error(e)" and "safe_read_file(path)".
|
|
623
|
+
"""
|
|
624
|
+
# Skip if intro or content are empty after stripping
|
|
625
|
+
if not intro.strip() or not content.strip():
|
|
626
|
+
return
|
|
627
|
+
|
|
628
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
629
|
+
readme_path = Path(tmp_dir) / "README.md"
|
|
630
|
+
test_content = f"{intro}\n\n# Section\n\n{content}"
|
|
631
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
632
|
+
|
|
633
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
634
|
+
transformed = transformer.transform()
|
|
635
|
+
|
|
636
|
+
# Verify table headers are present
|
|
637
|
+
assert (
|
|
638
|
+
"Задача" in transformed
|
|
639
|
+
), "Feature table header 'Задача' not found"
|
|
640
|
+
assert (
|
|
641
|
+
"Что вызвать" in transformed
|
|
642
|
+
), "Feature table header 'Что вызвать' not found"
|
|
643
|
+
|
|
644
|
+
# Verify markdown table format (pipe characters)
|
|
645
|
+
lines = transformed.split("\n")
|
|
646
|
+
|
|
647
|
+
# Find table lines - they should have both headers and separator
|
|
648
|
+
header_line_idx = None
|
|
649
|
+
separator_line_idx = None
|
|
650
|
+
|
|
651
|
+
for i, line in enumerate(lines):
|
|
652
|
+
if "Задача" in line and "Что вызвать" in line:
|
|
653
|
+
header_line_idx = i
|
|
654
|
+
elif header_line_idx is not None and "-----" in line:
|
|
655
|
+
separator_line_idx = i
|
|
656
|
+
break
|
|
657
|
+
|
|
658
|
+
assert (
|
|
659
|
+
header_line_idx is not None
|
|
660
|
+
), "Feature table header line not found"
|
|
661
|
+
assert (
|
|
662
|
+
separator_line_idx is not None
|
|
663
|
+
), "Feature table separator line not found"
|
|
664
|
+
assert (
|
|
665
|
+
separator_line_idx > header_line_idx
|
|
666
|
+
), "Separator line should come after header line"
|
|
667
|
+
|
|
668
|
+
# Verify required function entries
|
|
669
|
+
assert (
|
|
670
|
+
"explain_error(e)" in transformed
|
|
671
|
+
), "Feature table missing 'explain_error(e)' entry"
|
|
672
|
+
assert (
|
|
673
|
+
"safe_read_file(path)" in transformed
|
|
674
|
+
), "Feature table missing 'safe_read_file(path)' entry"
|
|
675
|
+
|
|
676
|
+
# Verify table structure: header and separator should have pipes
|
|
677
|
+
header_line = lines[header_line_idx]
|
|
678
|
+
separator_line = lines[separator_line_idx]
|
|
679
|
+
|
|
680
|
+
assert (
|
|
681
|
+
header_line.count("|") >= 2
|
|
682
|
+
), f"Header line does not have proper structure: {header_line}"
|
|
683
|
+
assert (
|
|
684
|
+
separator_line.count("|") >= 2
|
|
685
|
+
), f"Separator line does not have proper structure: {separator_line}"
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class TestTargetAudienceSectionProperty:
|
|
689
|
+
"""Property-based tests for target audience section."""
|
|
690
|
+
|
|
691
|
+
@given(
|
|
692
|
+
intro=st.text(
|
|
693
|
+
min_size=1,
|
|
694
|
+
max_size=100,
|
|
695
|
+
alphabet=st.characters(
|
|
696
|
+
blacklist_categories=("Cc", "Cs"),
|
|
697
|
+
blacklist_characters="\x00\r",
|
|
698
|
+
),
|
|
699
|
+
),
|
|
700
|
+
content=st.text(
|
|
701
|
+
min_size=1,
|
|
702
|
+
max_size=500,
|
|
703
|
+
alphabet=st.characters(
|
|
704
|
+
blacklist_categories=("Cc", "Cs"),
|
|
705
|
+
blacklist_characters="\x00\r",
|
|
706
|
+
),
|
|
707
|
+
),
|
|
708
|
+
)
|
|
709
|
+
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
|
|
710
|
+
def test_property_target_audience_section_content(
|
|
711
|
+
self, intro: str, content: str
|
|
712
|
+
) -> None:
|
|
713
|
+
"""
|
|
714
|
+
Property 4: Target Audience Section Content
|
|
715
|
+
|
|
716
|
+
**Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5**
|
|
717
|
+
|
|
718
|
+
For any README transformation, the target audience section should have
|
|
719
|
+
the title "Для кого эта библиотека", contain exactly 3 bullet points,
|
|
720
|
+
and include all three specified user descriptions.
|
|
721
|
+
"""
|
|
722
|
+
# Skip if intro or content are empty after stripping
|
|
723
|
+
if not intro.strip() or not content.strip():
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
727
|
+
readme_path = Path(tmp_dir) / "README.md"
|
|
728
|
+
test_content = f"{intro}\n\n# Section\n\n{content}"
|
|
729
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
730
|
+
|
|
731
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
732
|
+
transformed = transformer.transform()
|
|
733
|
+
|
|
734
|
+
# Verify section title is present
|
|
735
|
+
assert (
|
|
736
|
+
"Для кого эта библиотека" in transformed
|
|
737
|
+
), "Target audience section title not found"
|
|
738
|
+
|
|
739
|
+
# Verify section title is formatted as a heading
|
|
740
|
+
lines = transformed.split("\n")
|
|
741
|
+
title_found = False
|
|
742
|
+
for line in lines:
|
|
743
|
+
if "Для кого эта библиотека" in line and line.startswith("##"):
|
|
744
|
+
title_found = True
|
|
745
|
+
break
|
|
746
|
+
|
|
747
|
+
assert (
|
|
748
|
+
title_found
|
|
749
|
+
), "Target audience section title not formatted as heading"
|
|
750
|
+
|
|
751
|
+
# Verify all three required bullet points are present
|
|
752
|
+
required_bullets = [
|
|
753
|
+
"Ты только начал изучать Python",
|
|
754
|
+
"Сообщения об ошибках кажутся страшными и непонятными",
|
|
755
|
+
"Хочешь, чтобы ошибки объяснялись на нормальном русском с примерами",
|
|
756
|
+
]
|
|
757
|
+
|
|
758
|
+
for bullet in required_bullets:
|
|
759
|
+
assert (
|
|
760
|
+
bullet in transformed
|
|
761
|
+
), f"Required bullet point '{bullet}' not found in target audience section"
|
|
762
|
+
|
|
763
|
+
# Verify exactly 3 bullet points in the section
|
|
764
|
+
# Find the target audience section
|
|
765
|
+
section_start = None
|
|
766
|
+
section_end = None
|
|
767
|
+
|
|
768
|
+
for i, line in enumerate(lines):
|
|
769
|
+
if "Для кого эта библиотека" in line:
|
|
770
|
+
section_start = i
|
|
771
|
+
elif section_start is not None and line.startswith("#") and i > section_start:
|
|
772
|
+
section_end = i
|
|
773
|
+
break
|
|
774
|
+
|
|
775
|
+
if section_end is None:
|
|
776
|
+
section_end = len(lines)
|
|
777
|
+
|
|
778
|
+
# Count bullet points in the section
|
|
779
|
+
bullet_count = 0
|
|
780
|
+
for i in range(section_start, section_end):
|
|
781
|
+
if lines[i].strip().startswith("- "):
|
|
782
|
+
bullet_count += 1
|
|
783
|
+
|
|
784
|
+
assert (
|
|
785
|
+
bullet_count == 3
|
|
786
|
+
), f"Expected exactly 3 bullet points, found {bullet_count}"
|
|
787
|
+
|
|
788
|
+
# Verify bullet points are formatted correctly (start with "- ")
|
|
789
|
+
for i in range(section_start, section_end):
|
|
790
|
+
if any(bullet in lines[i] for bullet in required_bullets):
|
|
791
|
+
assert (
|
|
792
|
+
lines[i].strip().startswith("- ")
|
|
793
|
+
), f"Bullet point not properly formatted: {lines[i]}"
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
class TestDocumentStructureOrderingProperty:
|
|
798
|
+
"""Property-based tests for document structure ordering."""
|
|
799
|
+
|
|
800
|
+
@given(
|
|
801
|
+
intro=st.text(
|
|
802
|
+
min_size=10,
|
|
803
|
+
max_size=100,
|
|
804
|
+
alphabet=st.characters(
|
|
805
|
+
blacklist_categories=("Cc", "Cs"),
|
|
806
|
+
blacklist_characters="\x00\r#.",
|
|
807
|
+
),
|
|
808
|
+
),
|
|
809
|
+
content=st.text(
|
|
810
|
+
min_size=10,
|
|
811
|
+
max_size=500,
|
|
812
|
+
alphabet=st.characters(
|
|
813
|
+
blacklist_categories=("Cc", "Cs"),
|
|
814
|
+
blacklist_characters="\x00\r",
|
|
815
|
+
),
|
|
816
|
+
),
|
|
817
|
+
)
|
|
818
|
+
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
|
|
819
|
+
def test_property_document_structure_ordering(
|
|
820
|
+
self, intro: str, content: str
|
|
821
|
+
) -> None:
|
|
822
|
+
"""
|
|
823
|
+
Property 1: Document Structure Ordering
|
|
824
|
+
|
|
825
|
+
**Validates: Requirements 1.1, 1.4, 2.6, 3.6, 4.2**
|
|
826
|
+
|
|
827
|
+
For any valid README transformation, the document sections should appear
|
|
828
|
+
in this exact order: introduction, installation block, feature table,
|
|
829
|
+
target audience section, then existing detailed content.
|
|
830
|
+
"""
|
|
831
|
+
# Skip if intro or content are empty after stripping
|
|
832
|
+
if not intro.strip() or not content.strip():
|
|
833
|
+
return
|
|
834
|
+
|
|
835
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
836
|
+
readme_path = Path(tmp_dir) / "README.md"
|
|
837
|
+
# Ensure intro ends with a period to form a valid sentence
|
|
838
|
+
intro_with_period = intro.strip() + "." if not intro.strip().endswith((".","!","?")) else intro.strip()
|
|
839
|
+
test_content = f"{intro_with_period}\n\n# Section\n\n{content}"
|
|
840
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
841
|
+
|
|
842
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
843
|
+
transformed = transformer.transform()
|
|
844
|
+
|
|
845
|
+
lines = transformed.split("\n")
|
|
846
|
+
|
|
847
|
+
# Find indices of key sections
|
|
848
|
+
intro_idx = None
|
|
849
|
+
install_idx = None
|
|
850
|
+
feature_table_idx = None
|
|
851
|
+
target_audience_idx = None
|
|
852
|
+
existing_content_idx = None
|
|
853
|
+
|
|
854
|
+
for i, line in enumerate(lines):
|
|
855
|
+
# Introduction is the first non-empty, non-heading line
|
|
856
|
+
if intro_idx is None and line.strip() and not line.startswith("#"):
|
|
857
|
+
intro_idx = i
|
|
858
|
+
|
|
859
|
+
# Installation block starts with ```bash
|
|
860
|
+
if install_idx is None and "```bash" in line:
|
|
861
|
+
install_idx = i
|
|
862
|
+
|
|
863
|
+
# Feature table has both headers
|
|
864
|
+
if (
|
|
865
|
+
feature_table_idx is None
|
|
866
|
+
and "Задача" in line
|
|
867
|
+
and "Что вызвать" in line
|
|
868
|
+
):
|
|
869
|
+
feature_table_idx = i
|
|
870
|
+
|
|
871
|
+
# Target audience section
|
|
872
|
+
if (
|
|
873
|
+
target_audience_idx is None
|
|
874
|
+
and "Для кого эта библиотека" in line
|
|
875
|
+
):
|
|
876
|
+
target_audience_idx = i
|
|
877
|
+
|
|
878
|
+
# Existing content appears after target audience
|
|
879
|
+
if (
|
|
880
|
+
existing_content_idx is None
|
|
881
|
+
and target_audience_idx is not None
|
|
882
|
+
and i > target_audience_idx
|
|
883
|
+
and line.strip()
|
|
884
|
+
and not line.startswith("#")
|
|
885
|
+
and "Для кого" not in line
|
|
886
|
+
and "Ты только" not in line
|
|
887
|
+
and "Сообщения" not in line
|
|
888
|
+
and "Хочешь" not in line
|
|
889
|
+
):
|
|
890
|
+
existing_content_idx = i
|
|
891
|
+
|
|
892
|
+
# Verify all sections are present
|
|
893
|
+
assert intro_idx is not None, "Introduction section not found"
|
|
894
|
+
assert install_idx is not None, "Installation block not found"
|
|
895
|
+
assert feature_table_idx is not None, "Feature table not found"
|
|
896
|
+
assert target_audience_idx is not None, "Target audience section not found"
|
|
897
|
+
|
|
898
|
+
# Verify ordering: introduction < installation < feature table < target audience
|
|
899
|
+
assert (
|
|
900
|
+
intro_idx < install_idx
|
|
901
|
+
), f"Introduction ({intro_idx}) should come before installation block ({install_idx})"
|
|
902
|
+
|
|
903
|
+
assert (
|
|
904
|
+
install_idx < feature_table_idx
|
|
905
|
+
), f"Installation block ({install_idx}) should come before feature table ({feature_table_idx})"
|
|
906
|
+
|
|
907
|
+
assert (
|
|
908
|
+
feature_table_idx < target_audience_idx
|
|
909
|
+
), f"Feature table ({feature_table_idx}) should come before target audience ({target_audience_idx})"
|
|
910
|
+
|
|
911
|
+
# Verify markdown syntax is valid
|
|
912
|
+
is_valid, error_msg = transformer.validate_transformed_content(transformed)
|
|
913
|
+
assert (
|
|
914
|
+
is_valid
|
|
915
|
+
), f"Transformed content has invalid markdown syntax: {error_msg}"
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
class TestLanguageConsistencyProperty:
|
|
920
|
+
"""Property-based tests for language consistency."""
|
|
921
|
+
|
|
922
|
+
@given(
|
|
923
|
+
intro=st.text(
|
|
924
|
+
min_size=1,
|
|
925
|
+
max_size=100,
|
|
926
|
+
alphabet=st.characters(
|
|
927
|
+
blacklist_categories=("Cc", "Cs"),
|
|
928
|
+
blacklist_characters="\x00\r",
|
|
929
|
+
),
|
|
930
|
+
),
|
|
931
|
+
content=st.text(
|
|
932
|
+
min_size=1,
|
|
933
|
+
max_size=500,
|
|
934
|
+
alphabet=st.characters(
|
|
935
|
+
blacklist_categories=("Cc", "Cs"),
|
|
936
|
+
blacklist_characters="\x00\r",
|
|
937
|
+
),
|
|
938
|
+
),
|
|
939
|
+
)
|
|
940
|
+
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
|
|
941
|
+
def test_property_language_consistency(self, intro: str, content: str) -> None:
|
|
942
|
+
"""
|
|
943
|
+
Property 6: Language Consistency
|
|
944
|
+
|
|
945
|
+
**Validates: Requirements 4.4**
|
|
946
|
+
|
|
947
|
+
For any new content added to the README, it should use Russian language
|
|
948
|
+
to match the existing documentation style.
|
|
949
|
+
"""
|
|
950
|
+
# Skip if intro or content are empty after stripping
|
|
951
|
+
if not intro.strip() or not content.strip():
|
|
952
|
+
return
|
|
953
|
+
|
|
954
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
955
|
+
readme_path = Path(tmp_dir) / "README.md"
|
|
956
|
+
test_content = f"{intro}\n\n# Section\n\n{content}"
|
|
957
|
+
readme_path.write_text(test_content, encoding="utf-8")
|
|
958
|
+
|
|
959
|
+
transformer = ReadmeTransformer(str(readme_path))
|
|
960
|
+
transformed = transformer.transform()
|
|
961
|
+
|
|
962
|
+
# Define Russian language markers that should be present in new content
|
|
963
|
+
russian_markers = [
|
|
964
|
+
"Задача", # Feature table header
|
|
965
|
+
"Что вызвать", # Feature table header
|
|
966
|
+
"Для кого эта библиотека", # Target audience section title
|
|
967
|
+
"Ты только начал изучать Python", # Target audience bullet 1
|
|
968
|
+
"Сообщения об ошибках", # Target audience bullet 2
|
|
969
|
+
"нормальном русском", # Target audience bullet 3
|
|
970
|
+
]
|
|
971
|
+
|
|
972
|
+
# Verify all Russian language markers are present
|
|
973
|
+
for marker in russian_markers:
|
|
974
|
+
assert (
|
|
975
|
+
marker in transformed
|
|
976
|
+
), f"Russian language marker '{marker}' not found in transformed content"
|
|
977
|
+
|
|
978
|
+
# Verify the installation block uses English (as it's a standard command)
|
|
979
|
+
assert (
|
|
980
|
+
"pip install fishertools" in transformed
|
|
981
|
+
), "Installation command not found"
|
|
982
|
+
|
|
983
|
+
# Verify feature table entries use English function names
|
|
984
|
+
assert (
|
|
985
|
+
"explain_error(e)" in transformed
|
|
986
|
+
), "Function name 'explain_error(e)' not found"
|
|
987
|
+
assert (
|
|
988
|
+
"safe_read_file(path)" in transformed
|
|
989
|
+
), "Function name 'safe_read_file(path)' not found"
|
|
990
|
+
|
|
991
|
+
# Verify that new sections (not from original content) use Russian
|
|
992
|
+
lines = transformed.split("\n")
|
|
993
|
+
|
|
994
|
+
# Find the target audience section
|
|
995
|
+
target_audience_start = None
|
|
996
|
+
for i, line in enumerate(lines):
|
|
997
|
+
if "Для кого эта библиотека" in line:
|
|
998
|
+
target_audience_start = i
|
|
999
|
+
break
|
|
1000
|
+
|
|
1001
|
+
assert (
|
|
1002
|
+
target_audience_start is not None
|
|
1003
|
+
), "Target audience section not found"
|
|
1004
|
+
|
|
1005
|
+
# Verify the section heading is in Russian
|
|
1006
|
+
assert (
|
|
1007
|
+
lines[target_audience_start].startswith("##")
|
|
1008
|
+
), "Target audience should be a level 2 heading"
|
|
1009
|
+
|
|
1010
|
+
# Verify feature table headers are in Russian
|
|
1011
|
+
feature_table_found = False
|
|
1012
|
+
for i, line in enumerate(lines):
|
|
1013
|
+
if "Задача" in line and "Что вызвать" in line:
|
|
1014
|
+
feature_table_found = True
|
|
1015
|
+
# Verify it's a proper table header
|
|
1016
|
+
assert (
|
|
1017
|
+
"|" in line
|
|
1018
|
+
), "Feature table header should use pipe characters"
|
|
1019
|
+
break
|
|
1020
|
+
|
|
1021
|
+
assert (
|
|
1022
|
+
feature_table_found
|
|
1023
|
+
), "Feature table with Russian headers not found"
|