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
tests/test_safe/test_files.py
CHANGED
|
@@ -101,4 +101,729 @@ class TestSafeFileOperations:
|
|
|
101
101
|
safe_read_file("test.txt", encoding=123)
|
|
102
102
|
|
|
103
103
|
with pytest.raises(SafeUtilityError, match="должно быть строкой"):
|
|
104
|
-
safe_write_file("test.txt", 123)
|
|
104
|
+
safe_write_file("test.txt", 123)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TestProjectRoot:
|
|
109
|
+
"""Tests for project_root function."""
|
|
110
|
+
|
|
111
|
+
def test_project_root_from_current_directory(self):
|
|
112
|
+
"""Test finding project root from current directory."""
|
|
113
|
+
from fishertools.safe.files import project_root
|
|
114
|
+
|
|
115
|
+
root = project_root()
|
|
116
|
+
assert root is not None
|
|
117
|
+
assert Path(root).exists()
|
|
118
|
+
# Should find one of the markers
|
|
119
|
+
markers = ['setup.py', 'pyproject.toml', '.git', '.gitignore']
|
|
120
|
+
assert any((Path(root) / marker).exists() for marker in markers)
|
|
121
|
+
|
|
122
|
+
def test_project_root_from_subdirectory(self):
|
|
123
|
+
"""Test finding project root from a subdirectory."""
|
|
124
|
+
from fishertools.safe.files import project_root
|
|
125
|
+
|
|
126
|
+
# Get root from current directory
|
|
127
|
+
root1 = project_root()
|
|
128
|
+
|
|
129
|
+
# Get root from a subdirectory
|
|
130
|
+
subdir = Path(root1) / "fishertools"
|
|
131
|
+
if subdir.exists():
|
|
132
|
+
root2 = project_root(subdir)
|
|
133
|
+
assert root1 == root2
|
|
134
|
+
|
|
135
|
+
def test_project_root_not_found(self):
|
|
136
|
+
"""Test that RuntimeError is raised when project root cannot be found."""
|
|
137
|
+
from fishertools.safe.files import project_root
|
|
138
|
+
|
|
139
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
140
|
+
# Create a temporary directory with no markers
|
|
141
|
+
with pytest.raises(RuntimeError, match="Could not determine project root"):
|
|
142
|
+
project_root(temp_dir)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TestFindFile:
|
|
146
|
+
"""Tests for find_file function."""
|
|
147
|
+
|
|
148
|
+
def test_find_file_existing_file(self):
|
|
149
|
+
"""Test finding an existing file."""
|
|
150
|
+
from fishertools.safe.files import find_file
|
|
151
|
+
|
|
152
|
+
# Find setup.py which should exist in project root
|
|
153
|
+
path = find_file("setup.py")
|
|
154
|
+
assert path is not None
|
|
155
|
+
assert Path(path).exists()
|
|
156
|
+
assert Path(path).name == "setup.py"
|
|
157
|
+
|
|
158
|
+
def test_find_file_nonexistent_file(self):
|
|
159
|
+
"""Test finding a non-existent file returns None."""
|
|
160
|
+
from fishertools.safe.files import find_file
|
|
161
|
+
|
|
162
|
+
path = find_file("nonexistent_file_12345.txt")
|
|
163
|
+
assert path is None
|
|
164
|
+
|
|
165
|
+
def test_find_file_from_subdirectory(self):
|
|
166
|
+
"""Test finding a file starting from a subdirectory."""
|
|
167
|
+
from fishertools.safe.files import find_file
|
|
168
|
+
|
|
169
|
+
# Create a test file in a temporary subdirectory
|
|
170
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
171
|
+
temp_path = Path(temp_dir)
|
|
172
|
+
subdir = temp_path / "subdir"
|
|
173
|
+
subdir.mkdir()
|
|
174
|
+
|
|
175
|
+
# Create a test file in the subdirectory
|
|
176
|
+
test_file = subdir / "test.txt"
|
|
177
|
+
test_file.write_text("test content")
|
|
178
|
+
|
|
179
|
+
# Find the file from the subdirectory
|
|
180
|
+
path = find_file("test.txt", subdir)
|
|
181
|
+
assert path is not None
|
|
182
|
+
assert Path(path).exists()
|
|
183
|
+
assert Path(path).name == "test.txt"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestSafeOpen:
|
|
187
|
+
"""Tests for safe_open function."""
|
|
188
|
+
|
|
189
|
+
def test_safe_open_existing_file(self):
|
|
190
|
+
"""Test opening an existing file."""
|
|
191
|
+
from fishertools.safe.files import safe_open
|
|
192
|
+
|
|
193
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
194
|
+
f.write("Test content")
|
|
195
|
+
temp_path = f.name
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
with safe_open(temp_path) as f:
|
|
199
|
+
content = f.read()
|
|
200
|
+
assert content == "Test content"
|
|
201
|
+
finally:
|
|
202
|
+
os.unlink(temp_path)
|
|
203
|
+
|
|
204
|
+
def test_safe_open_nonexistent_file(self):
|
|
205
|
+
"""Test opening a non-existent file raises FileNotFoundError."""
|
|
206
|
+
from fishertools.safe.files import safe_open
|
|
207
|
+
|
|
208
|
+
with pytest.raises(FileNotFoundError):
|
|
209
|
+
safe_open("nonexistent_file_12345.txt")
|
|
210
|
+
|
|
211
|
+
def test_safe_open_write_mode(self):
|
|
212
|
+
"""Test opening a file in write mode."""
|
|
213
|
+
from fishertools.safe.files import safe_open
|
|
214
|
+
|
|
215
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
216
|
+
file_path = Path(temp_dir) / "test_file.txt"
|
|
217
|
+
|
|
218
|
+
with safe_open(file_path, mode='w') as f:
|
|
219
|
+
f.write("Hello World")
|
|
220
|
+
|
|
221
|
+
assert file_path.exists()
|
|
222
|
+
assert file_path.read_text(encoding='utf-8') == "Hello World"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ============================================================================
|
|
227
|
+
# Tests for fishertools-file-utils spec functions
|
|
228
|
+
# ============================================================================
|
|
229
|
+
|
|
230
|
+
from hypothesis import given, strategies as st, settings
|
|
231
|
+
import hashlib
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class TestEnsureDir:
|
|
235
|
+
"""Unit tests for ensure_dir function."""
|
|
236
|
+
|
|
237
|
+
def test_ensure_dir_creates_new_directory(self):
|
|
238
|
+
"""Test creating a new directory."""
|
|
239
|
+
from fishertools.safe.files import ensure_dir
|
|
240
|
+
|
|
241
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
242
|
+
new_dir = Path(temp_dir) / "new_directory"
|
|
243
|
+
result = ensure_dir(new_dir)
|
|
244
|
+
|
|
245
|
+
assert isinstance(result, Path)
|
|
246
|
+
assert new_dir.exists()
|
|
247
|
+
assert new_dir.is_dir()
|
|
248
|
+
|
|
249
|
+
def test_ensure_dir_creates_nested_directories(self):
|
|
250
|
+
"""Test creating nested directories."""
|
|
251
|
+
from fishertools.safe.files import ensure_dir
|
|
252
|
+
|
|
253
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
254
|
+
nested_dir = Path(temp_dir) / "level1" / "level2" / "level3"
|
|
255
|
+
result = ensure_dir(nested_dir)
|
|
256
|
+
|
|
257
|
+
assert isinstance(result, Path)
|
|
258
|
+
assert nested_dir.exists()
|
|
259
|
+
assert nested_dir.is_dir()
|
|
260
|
+
|
|
261
|
+
def test_ensure_dir_idempotent(self):
|
|
262
|
+
"""Test that ensure_dir is idempotent."""
|
|
263
|
+
from fishertools.safe.files import ensure_dir
|
|
264
|
+
|
|
265
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
266
|
+
test_dir = Path(temp_dir) / "test_dir"
|
|
267
|
+
|
|
268
|
+
# First call
|
|
269
|
+
result1 = ensure_dir(test_dir)
|
|
270
|
+
assert test_dir.exists()
|
|
271
|
+
|
|
272
|
+
# Second call should not raise error
|
|
273
|
+
result2 = ensure_dir(test_dir)
|
|
274
|
+
assert test_dir.exists()
|
|
275
|
+
assert result1 == result2
|
|
276
|
+
|
|
277
|
+
def test_ensure_dir_accepts_string_path(self):
|
|
278
|
+
"""Test that ensure_dir accepts string paths."""
|
|
279
|
+
from fishertools.safe.files import ensure_dir
|
|
280
|
+
|
|
281
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
282
|
+
test_dir = str(Path(temp_dir) / "test_dir")
|
|
283
|
+
result = ensure_dir(test_dir)
|
|
284
|
+
|
|
285
|
+
assert isinstance(result, Path)
|
|
286
|
+
assert Path(test_dir).exists()
|
|
287
|
+
|
|
288
|
+
def test_ensure_dir_accepts_path_object(self):
|
|
289
|
+
"""Test that ensure_dir accepts Path objects."""
|
|
290
|
+
from fishertools.safe.files import ensure_dir
|
|
291
|
+
|
|
292
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
293
|
+
test_dir = Path(temp_dir) / "test_dir"
|
|
294
|
+
result = ensure_dir(test_dir)
|
|
295
|
+
|
|
296
|
+
assert isinstance(result, Path)
|
|
297
|
+
assert test_dir.exists()
|
|
298
|
+
|
|
299
|
+
def test_ensure_dir_returns_path_object(self):
|
|
300
|
+
"""Test that ensure_dir returns a Path object."""
|
|
301
|
+
from fishertools.safe.files import ensure_dir
|
|
302
|
+
|
|
303
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
304
|
+
test_dir = Path(temp_dir) / "test_dir"
|
|
305
|
+
result = ensure_dir(test_dir)
|
|
306
|
+
|
|
307
|
+
assert isinstance(result, Path)
|
|
308
|
+
assert result == test_dir
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class TestGetFileHash:
|
|
312
|
+
"""Unit tests for get_file_hash function."""
|
|
313
|
+
|
|
314
|
+
def test_get_file_hash_sha256_default(self):
|
|
315
|
+
"""Test computing SHA256 hash (default algorithm)."""
|
|
316
|
+
from fishertools.safe.files import get_file_hash
|
|
317
|
+
|
|
318
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
319
|
+
f.write("test content")
|
|
320
|
+
temp_path = f.name
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
result = get_file_hash(temp_path)
|
|
324
|
+
# Verify it's a valid hex string
|
|
325
|
+
assert isinstance(result, str)
|
|
326
|
+
assert len(result) == 64 # SHA256 produces 64 hex characters
|
|
327
|
+
assert all(c in '0123456789abcdef' for c in result)
|
|
328
|
+
finally:
|
|
329
|
+
os.unlink(temp_path)
|
|
330
|
+
|
|
331
|
+
def test_get_file_hash_md5(self):
|
|
332
|
+
"""Test computing MD5 hash."""
|
|
333
|
+
from fishertools.safe.files import get_file_hash
|
|
334
|
+
|
|
335
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
336
|
+
f.write("test content")
|
|
337
|
+
temp_path = f.name
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
result = get_file_hash(temp_path, algorithm='md5')
|
|
341
|
+
assert isinstance(result, str)
|
|
342
|
+
assert len(result) == 32 # MD5 produces 32 hex characters
|
|
343
|
+
finally:
|
|
344
|
+
os.unlink(temp_path)
|
|
345
|
+
|
|
346
|
+
def test_get_file_hash_sha1(self):
|
|
347
|
+
"""Test computing SHA1 hash."""
|
|
348
|
+
from fishertools.safe.files import get_file_hash
|
|
349
|
+
|
|
350
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
351
|
+
f.write("test content")
|
|
352
|
+
temp_path = f.name
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
result = get_file_hash(temp_path, algorithm='sha1')
|
|
356
|
+
assert isinstance(result, str)
|
|
357
|
+
assert len(result) == 40 # SHA1 produces 40 hex characters
|
|
358
|
+
finally:
|
|
359
|
+
os.unlink(temp_path)
|
|
360
|
+
|
|
361
|
+
def test_get_file_hash_sha512(self):
|
|
362
|
+
"""Test computing SHA512 hash."""
|
|
363
|
+
from fishertools.safe.files import get_file_hash
|
|
364
|
+
|
|
365
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
366
|
+
f.write("test content")
|
|
367
|
+
temp_path = f.name
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
result = get_file_hash(temp_path, algorithm='sha512')
|
|
371
|
+
assert isinstance(result, str)
|
|
372
|
+
assert len(result) == 128 # SHA512 produces 128 hex characters
|
|
373
|
+
finally:
|
|
374
|
+
os.unlink(temp_path)
|
|
375
|
+
|
|
376
|
+
def test_get_file_hash_blake2b(self):
|
|
377
|
+
"""Test computing BLAKE2b hash."""
|
|
378
|
+
from fishertools.safe.files import get_file_hash
|
|
379
|
+
|
|
380
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
381
|
+
f.write("test content")
|
|
382
|
+
temp_path = f.name
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
result = get_file_hash(temp_path, algorithm='blake2b')
|
|
386
|
+
assert isinstance(result, str)
|
|
387
|
+
assert len(result) == 128 # BLAKE2b produces 128 hex characters
|
|
388
|
+
finally:
|
|
389
|
+
os.unlink(temp_path)
|
|
390
|
+
|
|
391
|
+
def test_get_file_hash_nonexistent_file(self):
|
|
392
|
+
"""Test that FileNotFoundError is raised for non-existent file."""
|
|
393
|
+
from fishertools.safe.files import get_file_hash
|
|
394
|
+
|
|
395
|
+
with pytest.raises(FileNotFoundError, match="File not found"):
|
|
396
|
+
get_file_hash("nonexistent_file_12345.txt")
|
|
397
|
+
|
|
398
|
+
def test_get_file_hash_unsupported_algorithm(self):
|
|
399
|
+
"""Test that ValueError is raised for unsupported algorithm."""
|
|
400
|
+
from fishertools.safe.files import get_file_hash
|
|
401
|
+
|
|
402
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
403
|
+
f.write("test content")
|
|
404
|
+
temp_path = f.name
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
with pytest.raises(ValueError, match="Unsupported algorithm"):
|
|
408
|
+
get_file_hash(temp_path, algorithm='unsupported')
|
|
409
|
+
finally:
|
|
410
|
+
os.unlink(temp_path)
|
|
411
|
+
|
|
412
|
+
def test_get_file_hash_deterministic(self):
|
|
413
|
+
"""Test that hash is deterministic (same file produces same hash)."""
|
|
414
|
+
from fishertools.safe.files import get_file_hash
|
|
415
|
+
|
|
416
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
417
|
+
f.write("test content")
|
|
418
|
+
temp_path = f.name
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
hash1 = get_file_hash(temp_path)
|
|
422
|
+
hash2 = get_file_hash(temp_path)
|
|
423
|
+
assert hash1 == hash2
|
|
424
|
+
finally:
|
|
425
|
+
os.unlink(temp_path)
|
|
426
|
+
|
|
427
|
+
def test_get_file_hash_accepts_string_path(self):
|
|
428
|
+
"""Test that get_file_hash accepts string paths."""
|
|
429
|
+
from fishertools.safe.files import get_file_hash
|
|
430
|
+
|
|
431
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
432
|
+
f.write("test content")
|
|
433
|
+
temp_path = f.name
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
result = get_file_hash(temp_path)
|
|
437
|
+
assert isinstance(result, str)
|
|
438
|
+
finally:
|
|
439
|
+
os.unlink(temp_path)
|
|
440
|
+
|
|
441
|
+
def test_get_file_hash_accepts_path_object(self):
|
|
442
|
+
"""Test that get_file_hash accepts Path objects."""
|
|
443
|
+
from fishertools.safe.files import get_file_hash
|
|
444
|
+
|
|
445
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
446
|
+
f.write("test content")
|
|
447
|
+
temp_path = Path(f.name)
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
result = get_file_hash(temp_path)
|
|
451
|
+
assert isinstance(result, str)
|
|
452
|
+
finally:
|
|
453
|
+
os.unlink(temp_path)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class TestReadLastLines:
|
|
457
|
+
"""Unit tests for read_last_lines function."""
|
|
458
|
+
|
|
459
|
+
def test_read_last_lines_basic(self):
|
|
460
|
+
"""Test reading last 10 lines from a file."""
|
|
461
|
+
from fishertools.safe.files import read_last_lines
|
|
462
|
+
|
|
463
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
464
|
+
for i in range(20):
|
|
465
|
+
f.write(f"line {i}\n")
|
|
466
|
+
temp_path = f.name
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
result = read_last_lines(temp_path, n=10)
|
|
470
|
+
assert len(result) == 10
|
|
471
|
+
assert result[0] == "line 10"
|
|
472
|
+
assert result[-1] == "line 19"
|
|
473
|
+
finally:
|
|
474
|
+
os.unlink(temp_path)
|
|
475
|
+
|
|
476
|
+
def test_read_last_lines_single_line(self):
|
|
477
|
+
"""Test reading last 1 line."""
|
|
478
|
+
from fishertools.safe.files import read_last_lines
|
|
479
|
+
|
|
480
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
481
|
+
for i in range(10):
|
|
482
|
+
f.write(f"line {i}\n")
|
|
483
|
+
temp_path = f.name
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
result = read_last_lines(temp_path, n=1)
|
|
487
|
+
assert len(result) == 1
|
|
488
|
+
assert result[0] == "line 9"
|
|
489
|
+
finally:
|
|
490
|
+
os.unlink(temp_path)
|
|
491
|
+
|
|
492
|
+
def test_read_last_lines_n_greater_than_file_lines(self):
|
|
493
|
+
"""Test reading when n is greater than number of lines."""
|
|
494
|
+
from fishertools.safe.files import read_last_lines
|
|
495
|
+
|
|
496
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
497
|
+
for i in range(5):
|
|
498
|
+
f.write(f"line {i}\n")
|
|
499
|
+
temp_path = f.name
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
result = read_last_lines(temp_path, n=10)
|
|
503
|
+
assert len(result) == 5
|
|
504
|
+
assert result[0] == "line 0"
|
|
505
|
+
assert result[-1] == "line 4"
|
|
506
|
+
finally:
|
|
507
|
+
os.unlink(temp_path)
|
|
508
|
+
|
|
509
|
+
def test_read_last_lines_empty_file(self):
|
|
510
|
+
"""Test reading from an empty file."""
|
|
511
|
+
from fishertools.safe.files import read_last_lines
|
|
512
|
+
|
|
513
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
514
|
+
temp_path = f.name
|
|
515
|
+
|
|
516
|
+
try:
|
|
517
|
+
result = read_last_lines(temp_path, n=10)
|
|
518
|
+
assert result == []
|
|
519
|
+
finally:
|
|
520
|
+
os.unlink(temp_path)
|
|
521
|
+
|
|
522
|
+
def test_read_last_lines_single_line_file(self):
|
|
523
|
+
"""Test reading from a file with single line."""
|
|
524
|
+
from fishertools.safe.files import read_last_lines
|
|
525
|
+
|
|
526
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
527
|
+
f.write("only line\n")
|
|
528
|
+
temp_path = f.name
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
result = read_last_lines(temp_path, n=10)
|
|
532
|
+
assert len(result) == 1
|
|
533
|
+
assert result[0] == "only line"
|
|
534
|
+
finally:
|
|
535
|
+
os.unlink(temp_path)
|
|
536
|
+
|
|
537
|
+
def test_read_last_lines_strips_newlines(self):
|
|
538
|
+
"""Test that newlines are stripped from lines."""
|
|
539
|
+
from fishertools.safe.files import read_last_lines
|
|
540
|
+
|
|
541
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
542
|
+
f.write("line 1\nline 2\nline 3\n")
|
|
543
|
+
temp_path = f.name
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
result = read_last_lines(temp_path, n=3)
|
|
547
|
+
assert all('\n' not in line for line in result)
|
|
548
|
+
assert all('\r' not in line for line in result)
|
|
549
|
+
finally:
|
|
550
|
+
os.unlink(temp_path)
|
|
551
|
+
|
|
552
|
+
def test_read_last_lines_nonexistent_file(self):
|
|
553
|
+
"""Test that FileNotFoundError is raised for non-existent file."""
|
|
554
|
+
from fishertools.safe.files import read_last_lines
|
|
555
|
+
|
|
556
|
+
with pytest.raises(FileNotFoundError, match="File not found"):
|
|
557
|
+
read_last_lines("nonexistent_file_12345.txt")
|
|
558
|
+
|
|
559
|
+
def test_read_last_lines_accepts_string_path(self):
|
|
560
|
+
"""Test that read_last_lines accepts string paths."""
|
|
561
|
+
from fishertools.safe.files import read_last_lines
|
|
562
|
+
|
|
563
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
564
|
+
f.write("line 1\nline 2\n")
|
|
565
|
+
temp_path = f.name
|
|
566
|
+
|
|
567
|
+
try:
|
|
568
|
+
result = read_last_lines(temp_path, n=2)
|
|
569
|
+
assert len(result) == 2
|
|
570
|
+
finally:
|
|
571
|
+
os.unlink(temp_path)
|
|
572
|
+
|
|
573
|
+
def test_read_last_lines_accepts_path_object(self):
|
|
574
|
+
"""Test that read_last_lines accepts Path objects."""
|
|
575
|
+
from fishertools.safe.files import read_last_lines
|
|
576
|
+
|
|
577
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
578
|
+
f.write("line 1\nline 2\n")
|
|
579
|
+
temp_path = Path(f.name)
|
|
580
|
+
|
|
581
|
+
try:
|
|
582
|
+
result = read_last_lines(temp_path, n=2)
|
|
583
|
+
assert len(result) == 2
|
|
584
|
+
finally:
|
|
585
|
+
os.unlink(temp_path)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# ============================================================================
|
|
589
|
+
# Property-Based Tests using Hypothesis
|
|
590
|
+
# ============================================================================
|
|
591
|
+
|
|
592
|
+
class TestEnsureDirProperties:
|
|
593
|
+
"""Property-based tests for ensure_dir function."""
|
|
594
|
+
|
|
595
|
+
@given(st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_characters='/\x00:<>"|?*\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f')))
|
|
596
|
+
@settings(max_examples=100)
|
|
597
|
+
def test_ensure_dir_returns_path_object(self, dirname):
|
|
598
|
+
"""Property 1: ensure_dir returns Path object
|
|
599
|
+
|
|
600
|
+
**Validates: Requirements 1.1, 1.2**
|
|
601
|
+
"""
|
|
602
|
+
from fishertools.safe.files import ensure_dir
|
|
603
|
+
|
|
604
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
605
|
+
test_path = Path(temp_dir) / dirname
|
|
606
|
+
result = ensure_dir(test_path)
|
|
607
|
+
assert isinstance(result, Path)
|
|
608
|
+
|
|
609
|
+
@given(st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_characters='/\x00:<>"|?*\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f')))
|
|
610
|
+
@settings(max_examples=100)
|
|
611
|
+
def test_ensure_dir_creates_directory(self, dirname):
|
|
612
|
+
"""Property 2: ensure_dir creates directory
|
|
613
|
+
|
|
614
|
+
**Validates: Requirements 1.3**
|
|
615
|
+
"""
|
|
616
|
+
from fishertools.safe.files import ensure_dir
|
|
617
|
+
|
|
618
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
619
|
+
test_path = Path(temp_dir) / dirname
|
|
620
|
+
ensure_dir(test_path)
|
|
621
|
+
assert test_path.exists()
|
|
622
|
+
assert test_path.is_dir()
|
|
623
|
+
|
|
624
|
+
@given(st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_characters='/\x00:<>"|?*\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f')))
|
|
625
|
+
@settings(max_examples=100)
|
|
626
|
+
def test_ensure_dir_idempotent(self, dirname):
|
|
627
|
+
"""Property 3: ensure_dir is idempotent
|
|
628
|
+
|
|
629
|
+
**Validates: Requirements 1.4**
|
|
630
|
+
"""
|
|
631
|
+
from fishertools.safe.files import ensure_dir
|
|
632
|
+
|
|
633
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
634
|
+
test_path = Path(temp_dir) / dirname
|
|
635
|
+
result1 = ensure_dir(test_path)
|
|
636
|
+
result2 = ensure_dir(test_path)
|
|
637
|
+
assert result1 == result2
|
|
638
|
+
assert test_path.exists()
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class TestGetFileHashProperties:
|
|
642
|
+
"""Property-based tests for get_file_hash function."""
|
|
643
|
+
|
|
644
|
+
@given(
|
|
645
|
+
st.binary(min_size=0, max_size=1000),
|
|
646
|
+
st.sampled_from(['md5', 'sha1', 'sha256', 'sha512', 'blake2b'])
|
|
647
|
+
)
|
|
648
|
+
@settings(max_examples=100)
|
|
649
|
+
def test_get_file_hash_supports_all_algorithms(self, content, algorithm):
|
|
650
|
+
"""Property 4: get_file_hash supports all algorithms
|
|
651
|
+
|
|
652
|
+
**Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5**
|
|
653
|
+
"""
|
|
654
|
+
from fishertools.safe.files import get_file_hash
|
|
655
|
+
|
|
656
|
+
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
657
|
+
f.write(content)
|
|
658
|
+
temp_path = f.name
|
|
659
|
+
|
|
660
|
+
try:
|
|
661
|
+
result = get_file_hash(temp_path, algorithm=algorithm)
|
|
662
|
+
assert isinstance(result, str)
|
|
663
|
+
assert len(result) > 0
|
|
664
|
+
assert all(c in '0123456789abcdef' for c in result)
|
|
665
|
+
finally:
|
|
666
|
+
os.unlink(temp_path)
|
|
667
|
+
|
|
668
|
+
@given(st.binary(min_size=0, max_size=1000))
|
|
669
|
+
@settings(max_examples=100)
|
|
670
|
+
def test_get_file_hash_accepts_str_and_path(self, content):
|
|
671
|
+
"""Property 5: get_file_hash accepts str and Path
|
|
672
|
+
|
|
673
|
+
**Validates: Requirements 2.10, 2.11**
|
|
674
|
+
"""
|
|
675
|
+
from fishertools.safe.files import get_file_hash
|
|
676
|
+
|
|
677
|
+
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
678
|
+
f.write(content)
|
|
679
|
+
temp_path = f.name
|
|
680
|
+
|
|
681
|
+
try:
|
|
682
|
+
hash_from_str = get_file_hash(temp_path)
|
|
683
|
+
hash_from_path = get_file_hash(Path(temp_path))
|
|
684
|
+
assert hash_from_str == hash_from_path
|
|
685
|
+
finally:
|
|
686
|
+
os.unlink(temp_path)
|
|
687
|
+
|
|
688
|
+
@given(st.binary(min_size=0, max_size=1000))
|
|
689
|
+
@settings(max_examples=100)
|
|
690
|
+
def test_get_file_hash_deterministic(self, content):
|
|
691
|
+
"""Property 6: get_file_hash is deterministic
|
|
692
|
+
|
|
693
|
+
**Validates: Requirements 2.1**
|
|
694
|
+
"""
|
|
695
|
+
from fishertools.safe.files import get_file_hash
|
|
696
|
+
|
|
697
|
+
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
698
|
+
f.write(content)
|
|
699
|
+
temp_path = f.name
|
|
700
|
+
|
|
701
|
+
try:
|
|
702
|
+
hash1 = get_file_hash(temp_path)
|
|
703
|
+
hash2 = get_file_hash(temp_path)
|
|
704
|
+
assert hash1 == hash2
|
|
705
|
+
finally:
|
|
706
|
+
os.unlink(temp_path)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class TestReadLastLinesProperties:
|
|
710
|
+
"""Property-based tests for read_last_lines function."""
|
|
711
|
+
|
|
712
|
+
@given(
|
|
713
|
+
st.lists(st.text(min_size=0, max_size=100, alphabet=st.characters(blacklist_characters='\n\r')), min_size=0, max_size=100),
|
|
714
|
+
st.integers(min_value=1, max_value=50)
|
|
715
|
+
)
|
|
716
|
+
@settings(max_examples=100)
|
|
717
|
+
def test_read_last_lines_correct_count(self, lines, n):
|
|
718
|
+
"""Property 7: read_last_lines returns correct number of lines
|
|
719
|
+
|
|
720
|
+
**Validates: Requirements 3.1**
|
|
721
|
+
"""
|
|
722
|
+
from fishertools.safe.files import read_last_lines
|
|
723
|
+
|
|
724
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
725
|
+
for line in lines:
|
|
726
|
+
f.write(line + '\n')
|
|
727
|
+
temp_path = f.name
|
|
728
|
+
|
|
729
|
+
try:
|
|
730
|
+
result = read_last_lines(temp_path, n=n)
|
|
731
|
+
expected_count = min(n, len(lines))
|
|
732
|
+
assert len(result) == expected_count
|
|
733
|
+
finally:
|
|
734
|
+
os.unlink(temp_path)
|
|
735
|
+
|
|
736
|
+
@given(
|
|
737
|
+
st.lists(st.text(min_size=0, max_size=100, alphabet=st.characters(blacklist_characters='\n\r', codec='utf-8')), min_size=0, max_size=50)
|
|
738
|
+
)
|
|
739
|
+
@settings(max_examples=100)
|
|
740
|
+
def test_read_last_lines_returns_all_when_n_greater(self, lines):
|
|
741
|
+
"""Property 8: read_last_lines returns all lines when n > total
|
|
742
|
+
|
|
743
|
+
**Validates: Requirements 3.3**
|
|
744
|
+
"""
|
|
745
|
+
from fishertools.safe.files import read_last_lines
|
|
746
|
+
|
|
747
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
748
|
+
for line in lines:
|
|
749
|
+
f.write(line + '\n')
|
|
750
|
+
temp_path = f.name
|
|
751
|
+
|
|
752
|
+
try:
|
|
753
|
+
result = read_last_lines(temp_path, n=1000)
|
|
754
|
+
assert len(result) == len(lines)
|
|
755
|
+
finally:
|
|
756
|
+
os.unlink(temp_path)
|
|
757
|
+
|
|
758
|
+
@given(
|
|
759
|
+
st.lists(st.text(min_size=0, max_size=100, alphabet=st.characters(blacklist_characters='\n\r', codec='utf-8')), min_size=0, max_size=100),
|
|
760
|
+
st.integers(min_value=1, max_value=50)
|
|
761
|
+
)
|
|
762
|
+
@settings(max_examples=100)
|
|
763
|
+
def test_read_last_lines_strips_newlines(self, lines, n):
|
|
764
|
+
"""Property 9: read_last_lines strips newlines
|
|
765
|
+
|
|
766
|
+
**Validates: Requirements 3.11**
|
|
767
|
+
"""
|
|
768
|
+
from fishertools.safe.files import read_last_lines
|
|
769
|
+
|
|
770
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
771
|
+
for line in lines:
|
|
772
|
+
f.write(line + '\n')
|
|
773
|
+
temp_path = f.name
|
|
774
|
+
|
|
775
|
+
try:
|
|
776
|
+
result = read_last_lines(temp_path, n=n)
|
|
777
|
+
assert all('\n' not in line for line in result)
|
|
778
|
+
assert all('\r' not in line for line in result)
|
|
779
|
+
finally:
|
|
780
|
+
os.unlink(temp_path)
|
|
781
|
+
|
|
782
|
+
@given(
|
|
783
|
+
st.lists(st.text(min_size=0, max_size=100, alphabet=st.characters(blacklist_characters='\n\r', codec='utf-8')), min_size=0, max_size=100),
|
|
784
|
+
st.integers(min_value=1, max_value=50)
|
|
785
|
+
)
|
|
786
|
+
@settings(max_examples=100)
|
|
787
|
+
def test_read_last_lines_accepts_str_and_path(self, lines, n):
|
|
788
|
+
"""Property 10: read_last_lines accepts str and Path
|
|
789
|
+
|
|
790
|
+
**Validates: Requirements 3.9, 3.10**
|
|
791
|
+
"""
|
|
792
|
+
from fishertools.safe.files import read_last_lines
|
|
793
|
+
|
|
794
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
|
|
795
|
+
for line in lines:
|
|
796
|
+
f.write(line + '\n')
|
|
797
|
+
temp_path = f.name
|
|
798
|
+
|
|
799
|
+
try:
|
|
800
|
+
result_from_str = read_last_lines(temp_path, n=n)
|
|
801
|
+
result_from_path = read_last_lines(Path(temp_path), n=n)
|
|
802
|
+
assert result_from_str == result_from_path
|
|
803
|
+
finally:
|
|
804
|
+
os.unlink(temp_path)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
class TestModuleExports:
|
|
808
|
+
"""Tests for module exports."""
|
|
809
|
+
|
|
810
|
+
def test_module_exports_all(self):
|
|
811
|
+
"""Property 11: Module exports correct functions
|
|
812
|
+
|
|
813
|
+
**Validates: Requirements 4.3**
|
|
814
|
+
"""
|
|
815
|
+
from fishertools.safe import files
|
|
816
|
+
|
|
817
|
+
assert hasattr(files, '__all__')
|
|
818
|
+
assert 'ensure_dir' in files.__all__
|
|
819
|
+
assert 'get_file_hash' in files.__all__
|
|
820
|
+
assert 'read_last_lines' in files.__all__
|
|
821
|
+
assert len(files.__all__) == 3
|
|
822
|
+
|
|
823
|
+
def test_module_functions_importable(self):
|
|
824
|
+
"""Test that all functions can be imported."""
|
|
825
|
+
from fishertools.safe.files import ensure_dir, get_file_hash, read_last_lines
|
|
826
|
+
|
|
827
|
+
assert callable(ensure_dir)
|
|
828
|
+
assert callable(get_file_hash)
|
|
829
|
+
assert callable(read_last_lines)
|