beetfs E2E Test Plan
Reviewed by Oracle - Critical bug discovered, plan updated accordingly
Executive Summary
E2E tests for beetfs FUSE filesystem using real music files from qBittorrent container. No mocks - actual filesystem operations against mounted beetfs.
Critical Finding
BUG DISCOVERED: Lines 758-1144 in beetFs.py are indented inside access() method, making these FUSE operations unreachable as class methods:
readdir, open, read, write, mkdir, unlink, rmdir, symlink, link, rename, chmod, chown, truncate, opendir, releasedir, fsyncdir, create, fgetattr, release, fsync, flush, ftruncate
Tests will expose this immediately - write test_readdir.py first.
Test Environment
| Component |
Status |
Details |
| Real Music |
Available |
Metallica "72 Seasons" (12 FLAC, 650MB) at /home/fujin/.local/share/docker/volumes/containers_downloads/_data/Metallica - 72 Seasons (2023) [FLAC] 88/ |
| Synthetic Music |
Create |
5-10MB FLACs for most tests (avoid RAM explosion) |
| Beets Config |
Create |
~/.config/beets/config.yaml for test isolation |
| Beets Library |
Empty |
Needs import of test files |
| Python |
2.7.15 |
Via Nix flake (nixpkgs-18.09) |
| Test Framework |
unittest |
stdlib, no external deps for Py2.7 |
Test Architecture
Test Tiers
Tier 1: Unit-ish (Synthetic FLACs, ~500KB each)
- Fast execution
- No memory issues (FileHandler loads entire file to RAM)
- Run on every commit
Tier 2: Integration (Subset of real files, 1-2 tracks)
- Uses real Metallica FLACs
- Tests real-world metadata
- Run before merge
Tier 3: E2E (All 12 tracks, 650MB)
- Full album processing
- Memory stress testing
- Run via
E2E=1 python -m unittest discover
- Skip by default
Test Isolation Strategy
| Resource |
Strategy |
Rationale |
| Audio Files |
Symlinks for reads |
beetfs NEVER writes to source files, only to beets DB |
| Beets DB |
Copy per test |
Writes mutate DB; need isolation |
| Mount Point |
Fresh tempdir |
Each test gets clean mount |
| Global State |
Fresh subprocess |
library, directory_structure are module globals |
Implementation Order
Reordered per Oracle recommendation: smoke → nested-bug → read → write → errors → edge
Phase 1: Infrastructure (Day 1 AM)
- Create
tests/ directory structure
- Implement
BeetFSTestCase base class with:
- Subprocess timeout via
threading.Timer (Py2.7 compatible)
- Mount wait polling (
os.path.ismount())
- Proper cleanup (
fusermount -u)
- Create synthetic FLAC generator using ffmpeg + flac CLI
- Setup isolated beets config and library
Phase 2: Bug Detection (Day 1 PM)
test_smoke.py - Mount/unmount lifecycle
test_nested_bug.py - Verify readdir, open are callable (will fail, exposing bug)
Phase 3: Core Tests (Day 2)
test_readdir.py - Directory listing
test_read.py - Metadata overlay verification (critical)
test_stat.py - File/directory attributes
Phase 4: Write & Errors (Day 3)
test_write.py - Metadata modification, DB persistence
test_error_handling.py - ENOENT, EOPNOTSUPP
Phase 5: Edge Cases (Day 3-4)
test_edge_cases.py - Unicode, concurrent opens, special chars
test_integration.py - Real 650MB files (optional tier)
Test Categories
1. Smoke Tests (test_smoke.py)
| Test |
Operation |
Expected |
test_mount_success |
Mount beetfs |
os.path.ismount() returns True |
test_unmount_clean |
Unmount |
Process exits 0, dir accessible |
test_mount_empty_library |
Mount with 0 items |
Mounts successfully, root empty |
test_mount_invalid_path |
Mount to non-existent |
Fails gracefully |
test_fsinit_called |
Check initialization |
No crash on mount |
2. Nested Methods Bug (test_nested_bug.py)
| Test |
Operation |
Expected |
test_readdir_exists |
hasattr(beetFileSystem, 'readdir') |
True (currently False!) |
test_open_exists |
hasattr(beetFileSystem, 'open') |
True (currently False!) |
test_read_exists |
hasattr(beetFileSystem, 'read') |
True (currently False!) |
test_readdir_callable |
os.listdir(mount) |
Returns list (currently fails!) |
3. Directory Operations (test_readdir.py)
| Test |
Operation |
Expected |
test_list_root |
os.listdir(mount) |
Returns artist directories |
test_list_artist |
os.listdir(mount/artist) |
Returns album directories |
test_list_album |
os.listdir(mount/artist/album) |
Returns track files |
test_path_format |
Check structure |
Matches $artist/$album ($year) [$format_upper]/$track - $artist - $title.$format |
test_unicode_paths |
Non-ASCII chars |
Handles "Lux Aeterna" correctly |
4. Read Operations (test_read.py) - CORE FEATURE
| Test |
Operation |
Expected |
test_read_header_overlay |
Read + parse with mutagen |
Tags match DB, not file |
test_read_audio_passthrough |
Compare audio bytes |
Identical to original after header |
test_read_full_file |
Read entire file |
Header from DB + audio from file |
test_metadata_artist |
Check artist tag |
DB value, not file value |
test_metadata_title |
Check title tag |
DB value, not file value |
test_metadata_album |
Check album tag |
DB value, not file value |
test_metadata_genre |
Check genre tag |
DB value, not file value |
test_original_unchanged |
Read original file |
Original metadata intact |
Metadata Overlay Verification Pattern
5. Stat Operations (test_stat.py)
| Test |
Operation |
Expected |
test_stat_file |
os.stat(file) |
Valid stat with size, mtime |
test_stat_directory |
os.stat(dir) |
Directory mode (S_IFDIR) |
test_statfs |
os.statvfs(mount) |
Valid filesystem stats |
test_access_read |
os.access(file, R_OK) |
True |
test_access_write |
os.access(file, W_OK) |
True (header writable) |
6. Write Operations (test_write.py)
| Test |
Operation |
Expected |
test_write_title |
Modify title in header |
DB updated, file unchanged |
test_write_artist |
Modify artist |
DB updated |
test_write_album |
Modify album |
DB updated |
test_write_genre |
Modify genre |
DB updated |
test_write_audio_discarded |
Write at offset > bound |
Silently discarded |
test_write_persistence |
Write -> unmount -> remount |
Changes persisted in DB |
test_write_mp3_noop |
Write to MP3 header |
No error, but no effect (bound=0) |
7. Error Handling (test_error_handling.py)
| Test |
Operation |
Expected |
test_enoent_file |
Read non-existent |
OSError(ENOENT) |
test_enoent_dir |
List non-existent |
OSError(ENOENT) |
test_eopnotsupp_mkdir |
os.mkdir() |
OSError(EOPNOTSUPP) |
test_eopnotsupp_unlink |
os.unlink() |
OSError(EOPNOTSUPP) |
test_eopnotsupp_rename |
os.rename() |
OSError(EOPNOTSUPP) |
test_eopnotsupp_symlink |
os.symlink() |
OSError(EOPNOTSUPP) |
8. Edge Cases (test_edge_cases.py)
| Test |
Operation |
Expected |
test_special_chars_sanitized |
Path with ?/ |
Sanitized via sanitize() |
test_concurrent_opens |
Open same file twice |
instance_count increments |
test_concurrent_release |
Release after double open |
File stays cached until count=0 |
test_unicode_metadata |
Non-ASCII in artist/title |
Handled correctly |
test_empty_metadata |
None/empty fields |
Doesn't crash |
test_mp3_no_interpolation |
Read MP3 |
Returns original file (no overlay) |
9. Integration (test_integration.py)
| Test |
Env Var |
Expected |
test_real_album_listing |
E2E=1 |
Lists all 12 Metallica tracks |
test_real_file_read |
E2E=1 |
Reads 67MB file successfully |
test_memory_usage |
E2E=1 |
Documents but doesn't fail on high RAM |
Test Infrastructure Code
Base Test Class (Python 2.7 Compatible)
Synthetic FLAC Generator
Dependencies to Add to flake.nix
Risks & Mitigations
| Risk |
Impact |
Mitigation |
| Memory explosion |
High |
Use 5-10MB synthetic FLACs, skip 650MB tests by default |
| Nested methods bug |
Critical |
Tests will expose; fix required before other tests pass |
| Python 2.7 EOL |
Medium |
Nix provides isolated environment |
| Global state pollution |
Medium |
Fresh subprocess per test |
| FUSE permissions |
Low |
Run as regular user, skip privileged tests |
| Concurrent access |
Low |
Single-threaded mode, sequential tests |
Success Criteria
- All smoke tests pass - beetfs mounts and unmounts cleanly
- Nested bug exposed and fixed - All FUSE methods callable
- Metadata overlay verified - Reads return DB metadata, not file metadata
- Writes update DB - Metadata changes persist
- Errors handled gracefully - Correct errno for unsupported ops
- No crashes on edge cases - Unicode, special chars, concurrent access
Notes from Oracle Review
- MP3 is not "readonly" - metadata overlay is disabled (
bound=0), but reads still work
- Write returns None for MP3 - no explicit return in MP3 path (falls through)
- Path format is hardcoded - tests must match
$artist/$album ($year) [$format_upper]/$track - $artist - $title.$format
- basestring vs str - use
isinstance(x, basestring) for Py2.7 string checks
- Global variables -
library, directory_structure must be reset between tests (use subprocesses)