Document test findings and fix mount script

Test Results (74 tests):
- 12 passed, 56 failures, 3 errors, 3 skipped

Bugs Detected:
1. Nested methods bug: lines 758-1144 indented inside access()
   - FUSE operations (readdir, open, read, write) unreachable
   - os.listdir() returns ENOSYS (Function not implemented)

2. Directory tree building: KeyError in FSNode.getnode()
   - Mount fails when library contains tracks

3. Unmount not clean: filesystem not releasing properly

Changes:
- Fix conftest.py: inline sanitization (no module-level sanitize fn)
- Add test findings to e2e-test-plan.md
- Add .gitignore for .pyc and test artifacts
This commit is contained in:
Alexander
2026-05-12 14:29:05 +02:00
parent 81df4790bf
commit f8666ae8c6
3 changed files with 142 additions and 31 deletions
+17
View File
@@ -0,0 +1,17 @@
# Python bytecode
*.pyc
*.pyo
__pycache__/
# Test artifacts
tests/LOG
tests/*.log
# Editor
*.swp
*.swo
*~
# Nix
result
result-*
+79
View File
@@ -2,6 +2,36 @@
> **Reviewed by Oracle** - Critical bug discovered, plan updated accordingly > **Reviewed by Oracle** - Critical bug discovered, plan updated accordingly
## Test Results (Latest Run)
```
Tests run: 74
Passed: 12
Failures: 56
Errors: 3
Skipped: 3
Duration: ~103 seconds
```
### Bugs Detected by Tests
| Bug | Tests Affected | Description |
|-----|----------------|-------------|
| **Nested Methods** | 56 | Lines 758-1144 indented inside `access()` - FUSE operations unreachable |
| **Directory Tree Building** | 3 | `KeyError` in `FSNode.getnode()` when adding files |
| **Unmount** | 1 | Filesystem not unmounting cleanly |
### Passing Tests (12)
- `test_fuse_available` - FUSE/fusermount detected
- `test_library_fixture_created` - SQLite DB and music dir created
- `test_temp_directory_created` - Temp dirs set up correctly
- `test_mount_empty_library` - **Mount works with empty library!**
- `test_list_empty_root` - Empty root returns empty list
- `test_list_root_returns_list` - Returns list type
- `test_access_empty_path` - Handles empty path
- Plus 5 nested bug detection tests (confirming bug exists)
## Executive Summary ## Executive Summary
E2E tests for beetfs FUSE filesystem using real music files from qBittorrent container. No mocks - actual filesystem operations against mounted beetfs. E2E tests for beetfs FUSE filesystem using real music files from qBittorrent container. No mocks - actual filesystem operations against mounted beetfs.
@@ -405,6 +435,55 @@ pkgs.flac # For FLAC encoding
--- ---
## Findings from Test Execution
### Bug #1: Nested Methods (CRITICAL)
**Location**: `beetFs.py` lines 758-1144
**Problem**: All FUSE operation methods are indented inside the `access()` method, making them local functions instead of class methods.
**Evidence**:
```python
def access(self, path, flags): # Line 723 - correct class method
...
return 0
def readdir(self, path, ...): # Line 931 - WRONG! Nested inside access()
...
def open(self, path, flags): # Line 988 - Also nested
...
def read(self, path, ...): # Line 1077 - Also nested
...
```
**Symptom**: `os.listdir()` returns `OSError: [Errno 38] Function not implemented`
**Fix Required**: Dedent lines 758-1144 by 8 spaces to make them class methods.
### Bug #2: Directory Tree Building
**Location**: `beetFs.py` lines 403-414 (`FSNode.getnode()` and `FSNode.adddir()`)
**Problem**: When adding files to the directory structure, the code assumes parent directories already exist.
**Evidence**:
```
KeyError: u'Test Artist'
File "beetFs.py", line 403, in getnode
return self.getnode(elements, root=root.dirs[topdir])
```
**Symptom**: Mount fails when library contains tracks.
### Bug #3: Unmount Not Clean
**Problem**: After unmounting, `os.path.ismount()` still returns `True`.
**Likely Cause**: FUSE process not terminating properly, or lazy unmount not completing.
---
## Notes from Oracle Review ## Notes from Oracle Review
1. **MP3 is not "readonly"** - metadata overlay is disabled (`bound=0`), but reads still work 1. **MP3 is not "readonly"** - metadata overlay is disabled (`bound=0`), but reads still work
+46 -31
View File
@@ -338,64 +338,79 @@ class BeetFSTestCase(unittest.TestCase):
env = os.environ.copy() env = os.environ.copy()
env['BEETSDIR'] = self.library.temp_dir env['BEETSDIR'] = self.library.temp_dir
cmd = [ mount_script = '''
sys.executable, # Python interpreter
'-c',
'''
import sys import sys
sys.path.insert(0, "{beetfs_root}") sys.path.insert(0, "{beetfs_root}")
sys.path.insert(0, "{beetsplug}") sys.path.insert(0, "{beetsplug}")
import os import os
import re
os.environ["BEETSDIR"] = "{beetsdir}" os.environ["BEETSDIR"] = "{beetsdir}"
from beets import config from beets import config
from beets.library import Library from beets.library import Library
# Load our test config
config.read(user=False) config.read(user=False)
config["directory"] = "{music_dir}" config["directory"] = "{music_dir}"
config["library"] = "{db_path}" config["library"] = "{db_path}"
# Open library
lib = Library("{db_path}") lib = Library("{db_path}")
# Import and run beetfs import beetFs
from beetFs import beetFileSystem
import fuse import fuse
fuse.fuse_python_api = (0, 2) fuse.fuse_python_api = (0, 2)
fs = beetFileSystem(
version="%prog " + fuse.__version__,
usage="Test mount",
dash_s_do='setsingle'
)
fs.parse(errex=1)
fs.flags = 0
fs.multithreaded = False
# Set global library reference
import beetFs
beetFs.library = lib beetFs.library = lib
beetFs.structure_depth = 4
beetFs.structure_split = [0, 1, 2, 3]
beetFs.directory_structure = beetFs.FSNode({{}}, {{}})
# Build directory structure
from beetFs import directory_structure, template_mapping
for item in lib.items(): for item in lib.items():
mapping = beetFs.template_mapping(lib, item) mapping = beetFs.template_mapping(lib, item)
path_str = beetFs.PATH_FORMAT
for key, val in mapping.items():
if val is not None:
clean_val = re.sub(r"[\\\\/:]|^\\.", "_", unicode(val))
path_str = path_str.replace("$" + key, clean_val)
elements = path_str.split("/")
sub_elements = elements[0:beetFs.structure_depth-1]
for level in range(len(sub_elements)):
level_subbed = sub_elements[0:level+1]
beetFs.directory_structure.adddir(sub_elements, level_subbed[level])
beetFs.directory_structure.addfile(
sub_elements,
elements[beetFs.structure_depth-1],
item.id
)
fs = beetFs.beetFileSystem(
version="%prog " + fuse.__version__,
usage="beetfs test mount",
dash_s_do="setsingle"
)
fs.parser.add_option(mountopt="root", metavar="PATH", default="{music_dir}",
help="music library root path")
fs.parse(args=["{mount_dir}"], errex=1)
fs.flags = 0
fs.multithreaded = False
fs.fuse_args.setmod("foreground")
fs.fuse_args.add("fsname=beetfs")
fs.fuse_args.add("nonempty")
fs.lib = lib
fs.main() fs.main()
'''.format( '''.format(
beetfs_root=BEETFS_ROOT, beetfs_root=BEETFS_ROOT,
beetsplug=os.path.join(BEETFS_ROOT, 'beetsplug'), beetsplug=os.path.join(BEETFS_ROOT, 'beetsplug'),
beetsdir=self.library.temp_dir, beetsdir=self.library.temp_dir,
music_dir=self.library.music_dir, music_dir=self.library.music_dir,
db_path=self.library.db_path db_path=self.library.db_path,
), mount_dir=self.mount_dir
self.mount_dir )
]
if foreground: cmd = [sys.executable, '-c', mount_script]
cmd.insert(-1, '-f')
# Start process # Start process
self.fs_process = subprocess.Popen( self.fs_process = subprocess.Popen(