216 lines
8.4 KiB
Python
216 lines
8.4 KiB
Python
"""Tests for :class:`IsoTreeModel` root-handling and lazy fetch.
|
|
|
|
The model root's children are the entries of the ISO ``/`` directory,
|
|
surfaced directly so the view displays them without manual expansion.
|
|
rowCount() returns 0 for an unloaded root and lets Qt drive
|
|
canFetchMore()/fetchMore(); index_from_path('/') yields the invalid
|
|
model-root index; refresh_parent('/') drops and re-fetches the virtual
|
|
root without crashing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
PySide6 = pytest.importorskip("PySide6")
|
|
pycdlib = pytest.importorskip("pycdlib")
|
|
|
|
from PySide6.QtCore import QModelIndex
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def qapp():
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
app = QApplication.instance() or QApplication([])
|
|
yield app
|
|
|
|
|
|
@pytest.fixture
|
|
def tiny_iso_path(tmp_path):
|
|
"""Create a small ISO with two files and one subdirectory at /."""
|
|
iso_path = tmp_path / "tiny.iso"
|
|
iso = pycdlib.PyCdlib()
|
|
iso.new()
|
|
iso.add_fp(BytesIO(b"hello"), 5, "/HELLO.TXT;1")
|
|
iso.add_fp(BytesIO(b"world"), 5, "/WORLD.TXT;1")
|
|
iso.add_directory("/SUBDIR")
|
|
iso.add_fp(BytesIO(b"sub"), 3, "/SUBDIR/INSIDE.TXT;1")
|
|
iso.write(str(iso_path))
|
|
iso.close()
|
|
return str(iso_path)
|
|
|
|
|
|
@pytest.fixture
|
|
def open_handler(tiny_iso_path):
|
|
from iso_scalpel.iso_handler import IsoHandler
|
|
h = IsoHandler()
|
|
h.open(tiny_iso_path)
|
|
yield h
|
|
h.close()
|
|
|
|
|
|
# ==========================================================================
|
|
# IsoTreeModel: rowCount(QModelIndex()) must reflect / contents directly
|
|
# ==========================================================================
|
|
class TestModelRootShowsDirectoryContents:
|
|
"""The model root's children are the entries of the ISO ``/`` directory.
|
|
|
|
rowCount(QModelIndex()) reflects the count of root entries directly --
|
|
there is no virtual ``"/"`` placeholder row that the user must expand.
|
|
"""
|
|
|
|
def test_rowCount_of_model_root_is_zero_before_fetch(self, open_handler, qapp):
|
|
from iso_scalpel.iso_model import IsoTreeModel
|
|
m = IsoTreeModel(open_handler)
|
|
# Before fetchMore is called, the root isn't loaded -- rowCount
|
|
# must return 0 (not 1) so the view triggers canFetchMore/fetchMore.
|
|
assert m.rowCount(QModelIndex()) == 0
|
|
|
|
def test_canFetchMore_returns_true_for_unloaded_root(self, open_handler, qapp):
|
|
from iso_scalpel.iso_model import IsoTreeModel
|
|
m = IsoTreeModel(open_handler)
|
|
assert m.canFetchMore(QModelIndex()) is True
|
|
|
|
def test_fetchMore_populates_root_children(self, open_handler, qapp):
|
|
from iso_scalpel.iso_model import IsoTreeModel
|
|
m = IsoTreeModel(open_handler)
|
|
m.fetchMore(QModelIndex())
|
|
# The ISO has 3 entries at /: HELLO.TXT, WORLD.TXT, SUBDIR
|
|
assert m.rowCount(QModelIndex()) == 3
|
|
|
|
def test_root_children_names_are_correct(self, open_handler, qapp):
|
|
from iso_scalpel.iso_model import IsoTreeModel
|
|
m = IsoTreeModel(open_handler)
|
|
m.fetchMore(QModelIndex())
|
|
names = [m.index(r, 0, QModelIndex()).data() for r in range(m.rowCount(QModelIndex()))]
|
|
assert set(names) == {"HELLO.TXT", "WORLD.TXT", "SUBDIR"}
|
|
|
|
def test_no_infinite_recursion_in_rowCount(self, open_handler, qapp):
|
|
"""rowCount() must never call fetchMore().
|
|
|
|
Calling fetchMore() from inside rowCount() recurses through
|
|
beginInsertRows and blows the stack. The contract is: rowCount()
|
|
returns 0 for an unloaded root; Qt then calls canFetchMore() /
|
|
fetchMore() on its own schedule.
|
|
"""
|
|
from iso_scalpel.iso_model import IsoTreeModel
|
|
m = IsoTreeModel(open_handler)
|
|
# Calling rowCount on an unloaded root must return immediately
|
|
# without recursing.
|
|
result = m.rowCount(QModelIndex())
|
|
assert isinstance(result, int)
|
|
|
|
|
|
# ==========================================================================
|
|
# index_from_path: "/" maps to the model root (invalid QModelIndex)
|
|
# ==========================================================================
|
|
class TestIndexFromPath:
|
|
"""``index_from_path("/")`` must return the model root (an invalid
|
|
:class:`QModelIndex`) -- not ``createIndex(0, 0, self._root)`` -- so
|
|
that the view's default root index correctly shows "/" contents.
|
|
"""
|
|
|
|
def test_root_path_returns_invalid_index(self, open_handler, qapp):
|
|
from iso_scalpel.iso_model import IsoTreeModel
|
|
m = IsoTreeModel(open_handler)
|
|
idx = m.index_from_path("/")
|
|
assert not idx.isValid(), (
|
|
"index_from_path('/') should return the model root "
|
|
"(invalid QModelIndex), not a createIndex"
|
|
)
|
|
|
|
def test_subdir_path_returns_valid_index(self, open_handler, qapp):
|
|
from iso_scalpel.iso_model import IsoTreeModel
|
|
m = IsoTreeModel(open_handler)
|
|
m.fetchMore(QModelIndex())
|
|
idx = m.index_from_path("/SUBDIR")
|
|
assert idx.isValid()
|
|
assert idx.data() == "SUBDIR"
|
|
|
|
def test_nonexistent_path_returns_invalid_index(self, open_handler, qapp):
|
|
from iso_scalpel.iso_model import IsoTreeModel
|
|
m = IsoTreeModel(open_handler)
|
|
m.fetchMore(QModelIndex())
|
|
idx = m.index_from_path("/DOES_NOT_EXIST")
|
|
assert not idx.isValid()
|
|
|
|
|
|
# ==========================================================================
|
|
# refresh_parent: handles the root path correctly
|
|
# ==========================================================================
|
|
class TestRefreshParent:
|
|
"""``refresh_parent("/")`` must not crash when the model's
|
|
``index_from_path("/")`` returns an invalid index.
|
|
"""
|
|
|
|
def test_refresh_parent_root_does_not_crash(self, open_handler, qapp):
|
|
from iso_scalpel.iso_model import IsoTreeModel
|
|
m = IsoTreeModel(open_handler)
|
|
m.fetchMore(QModelIndex())
|
|
# Should not raise.
|
|
m.refresh_parent("/")
|
|
# After refresh, root should be unloaded again.
|
|
assert m.rowCount(QModelIndex()) == 0
|
|
# Re-fetching should still work.
|
|
m.fetchMore(QModelIndex())
|
|
assert m.rowCount(QModelIndex()) == 3
|
|
|
|
def test_refresh_parent_subdir(self, open_handler, qapp):
|
|
from iso_scalpel.iso_model import IsoTreeModel
|
|
m = IsoTreeModel(open_handler)
|
|
m.fetchMore(QModelIndex())
|
|
subdir_idx = m.index_from_path("/SUBDIR")
|
|
assert subdir_idx.isValid()
|
|
m.fetchMore(subdir_idx)
|
|
assert m.rowCount(subdir_idx) == 1 # INSIDE.TXT
|
|
m.refresh_parent("/SUBDIR")
|
|
# After refresh, subdir is unloaded.
|
|
assert m.rowCount(subdir_idx) == 0
|
|
|
|
|
|
# ==========================================================================
|
|
# End-to-end: IsoPane displays entries after open
|
|
# ==========================================================================
|
|
class TestIsoPaneShowsEntriesAfterOpen:
|
|
"""End-to-end regression: after ``handler.open()`` and
|
|
``pane.refresh()``, the ISO pane's view must report a non-zero
|
|
row count for the model root.
|
|
"""
|
|
|
|
def test_pane_shows_entries_after_refresh(self, open_handler, qapp):
|
|
from iso_scalpel.config import Settings
|
|
from iso_scalpel.widgets.iso_pane import IsoPane
|
|
s = Settings()
|
|
pane = IsoPane(open_handler, s)
|
|
pane.refresh()
|
|
# Force the view's model to fetch the root children.
|
|
pane.view.model().fetchMore(pane.view.rootIndex())
|
|
qapp.processEvents()
|
|
# The view's root index is the model root (invalid QModelIndex);
|
|
# its row count must be 3 (HELLO.TXT, WORLD.TXT, SUBDIR).
|
|
rc = pane.view.model().rowCount(pane.view.rootIndex())
|
|
assert rc == 3, f"ISO pane should show 3 entries, got {rc}"
|
|
|
|
def test_pane_breadcrumb_shows_iso_filename_after_open(self, open_handler, qapp):
|
|
"""The breadcrumb bar at the top of the ISO pane shows the ISO
|
|
filename after opening. The pane must reach the
|
|
``_rebuild_breadcrumbs`` call path on refresh.
|
|
"""
|
|
from iso_scalpel.config import Settings
|
|
from iso_scalpel.widgets.iso_pane import IsoPane
|
|
s = Settings()
|
|
pane = IsoPane(open_handler, s)
|
|
pane.refresh()
|
|
# The breadcrumb frame must have at least one child widget (the
|
|
# root button showing the ISO filename).
|
|
assert pane._crumb_layout.count() > 0
|