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:
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# Python bytecode
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
tests/LOG
|
||||||
|
tests/*.log
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Nix
|
||||||
|
result
|
||||||
|
result-*
|
||||||
@@ -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
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user