diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ea5ba6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Python bytecode +*.pyc +*.pyo +__pycache__/ + +# Test artifacts +tests/LOG +tests/*.log + +# Editor +*.swp +*.swo +*~ + +# Nix +result +result-* diff --git a/docs/e2e-test-plan.md b/docs/e2e-test-plan.md index dec506c..1c5b2b3 100644 --- a/docs/e2e-test-plan.md +++ b/docs/e2e-test-plan.md @@ -2,6 +2,36 @@ > **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 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 1. **MP3 is not "readonly"** - metadata overlay is disabled (`bound=0`), but reads still work diff --git a/tests/conftest.py b/tests/conftest.py index 97e9911..c442d70 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -338,64 +338,79 @@ class BeetFSTestCase(unittest.TestCase): env = os.environ.copy() env['BEETSDIR'] = self.library.temp_dir - cmd = [ - sys.executable, # Python interpreter - '-c', - ''' + mount_script = ''' import sys sys.path.insert(0, "{beetfs_root}") sys.path.insert(0, "{beetsplug}") import os +import re os.environ["BEETSDIR"] = "{beetsdir}" from beets import config from beets.library import Library -# Load our test config config.read(user=False) config["directory"] = "{music_dir}" config["library"] = "{db_path}" -# Open library lib = Library("{db_path}") -# Import and run beetfs -from beetFs import beetFileSystem +import beetFs import fuse 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.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(): 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() '''.format( - beetfs_root=BEETFS_ROOT, - beetsplug=os.path.join(BEETFS_ROOT, 'beetsplug'), - beetsdir=self.library.temp_dir, - music_dir=self.library.music_dir, - db_path=self.library.db_path - ), - self.mount_dir - ] + beetfs_root=BEETFS_ROOT, + beetsplug=os.path.join(BEETFS_ROOT, 'beetsplug'), + beetsdir=self.library.temp_dir, + music_dir=self.library.music_dir, + db_path=self.library.db_path, + mount_dir=self.mount_dir + ) - if foreground: - cmd.insert(-1, '-f') + cmd = [sys.executable, '-c', mount_script] # Start process self.fs_process = subprocess.Popen(