""" Tests are named 'test_nnn_xxxx' where 'nn n' is a number. This is used to ensure that the tests run in order as we rely (in some cases) upon the results of an earlier test. Yes, we shouldn't do that. """ # Standard library imports import os import shutil import tempfile import unittest from unittest.mock import MagicMock, patch # PyQt imports from PyQt6.QtWidgets import QDialog, QFileDialog # Third party imports from mutagen.mp3 import MP3 # type: ignore import pytest from pytestqt.plugin import QtBot # type: ignore # App imports from app import ds, musicmuster from app.models import db from config import Config from file_importer import FileImporter # Custom fixture to adapt qtbot for use with unittest.TestCase @pytest.fixture(scope="class") def qtbot_adapter(qapp, request): """Adapt qtbot fixture for usefixtures and unittest.TestCase""" request.cls.qtbot = QtBot(request) # Fixture for tmp_path to be available in the class @pytest.fixture(scope="class") def class_tmp_path(request, tmp_path_factory): """Provide a class-wide tmp_path""" request.cls.tmp_path = tmp_path_factory.mktemp("pytest_tmp") @pytest.mark.usefixtures("qtbot_adapter", "class_tmp_path") class MyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): """Runs once before any test in this class""" db.create_all() cls.widget = musicmuster.Window() # Create a playlist for all tests playlist_name = "file importer playlist" playlist = ds.create_playlist(name=playlist_name, template_id=0) cls.widget._open_playlist(playlist) # Create our musicstore cls.import_source = tempfile.mkdtemp(suffix="_MMsource_pytest", dir="/tmp") Config.REPLACE_FILES_DEFAULT_SOURCE = cls.import_source cls.musicstore = tempfile.mkdtemp(suffix="_MMstore_pytest", dir="/tmp") Config.IMPORT_DESTINATION = cls.musicstore @classmethod def tearDownClass(cls): """Runs once after all tests""" db.drop_all() shutil.rmtree(cls.musicstore) shutil.rmtree(cls.import_source) def setUp(self): """Runs before each test""" with self.qtbot.waitExposed(self.widget): self.widget.show() def tearDown(self): """Runs after each test""" self.widget.close() # Close UI to prevent side effects def wait_for_workers(self, timeout: int = 10000): """ Let import threads workers run to completion """ def workers_empty(): assert FileImporter.workers == {} self.qtbot.waitUntil(workers_empty, timeout=timeout) def test_001_import_no_files(self): """Try importing with no files to import""" with patch("file_importer.show_OK") as mock_show_ok: self.widget.import_files_wrapper() mock_show_ok.assert_called_once_with( "File import", f"No files in {Config.REPLACE_FILES_DEFAULT_SOURCE} to import", None, ) def test_002_import_file_and_cancel(self): """Cancel file import""" test_track_path = "testdata/isa.mp3" shutil.copy(test_track_path, self.import_source) with ( patch("file_importer.PickMatch") as MockPickMatch, patch("file_importer.show_OK") as mock_show_ok, ): # Create a mock instance of PickMatch mock_dialog_instance = MagicMock() MockPickMatch.return_value = mock_dialog_instance # Simulate the user clicking OK in the dialog mock_dialog_instance.exec.return_value = QDialog.DialogCode.Rejected mock_dialog_instance.selected_track_id = -1 # Simulated return value self.widget.import_files_wrapper() # Ensure PickMatch was instantiated correctly MockPickMatch.assert_called_once_with( new_track_description="I'm So Afraid (Fleetwood Mac)", choices=[("Do not import", -1, ""), ("Import as new track", 0, "")], default=1, ) # Verify exec() was called mock_dialog_instance.exec.assert_called_once() # Ensure selected_track_id was accessed after dialog.exec() assert mock_dialog_instance.selected_track_id < 0 mock_show_ok.assert_called_once_with( "File not imported", "isa.mp3 will not be imported because you asked not to import this file", ) def test_003_import_first_file(self): """Import file into empty directory""" test_track_path = "testdata/isa.mp3" shutil.copy(test_track_path, self.import_source) with patch("file_importer.PickMatch") as MockPickMatch: # Create a mock instance of PickMatch mock_dialog_instance = MagicMock() MockPickMatch.return_value = mock_dialog_instance # Simulate the user clicking OK in the dialog mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted mock_dialog_instance.selected_track_id = 0 # Simulated return value self.widget.import_files_wrapper() # Ensure PickMatch was instantiated correctly MockPickMatch.assert_called_once_with( new_track_description="I'm So Afraid (Fleetwood Mac)", choices=[("Do not import", -1, ""), ("Import as new track", 0, "")], default=1, ) # Verify exec() was called mock_dialog_instance.exec.assert_called_once() # Ensure selected_track_id was accessed after dialog.exec() assert mock_dialog_instance.selected_track_id == 0 self.wait_for_workers() # Check track was imported tracks = ds.get_all_tracks() assert len(tracks) == 1 track = tracks[0] assert track.title == "I'm So Afraid" assert track.artist == "Fleetwood Mac" track_file = os.path.join( self.musicstore, os.path.basename(test_track_path) ) assert track.path == track_file assert os.path.exists(track_file) assert os.listdir(self.import_source) == [] def test_004_import_second_file(self): """Import a second file""" test_track_path = "testdata/lovecats.mp3" shutil.copy(test_track_path, self.import_source) with patch("file_importer.PickMatch") as MockPickMatch: # Create a mock instance of PickMatch mock_dialog_instance = MagicMock() MockPickMatch.return_value = mock_dialog_instance # Simulate the user clicking OK in the dialog mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted mock_dialog_instance.selected_track_id = 0 # Simulated return value self.widget.import_files_wrapper() # Ensure PickMatch was instantiated correctly MockPickMatch.assert_called_once_with( new_track_description="The Lovecats (The Cure)", choices=[("Do not import", -1, ""), ("Import as new track", 0, "")], default=1, ) # Verify exec() was called mock_dialog_instance.exec.assert_called_once() # Ensure selected_track_id was accessed after dialog.exec() assert mock_dialog_instance.selected_track_id == 0 self.wait_for_workers() # Check track was imported tracks = ds.get_all_tracks() assert len(tracks) == 2 track = tracks[1] assert track.title == "The Lovecats" assert track.artist == "The Cure" track_file = os.path.join( self.musicstore, os.path.basename(test_track_path) ) assert track.path == track_file assert os.path.exists(track_file) assert os.listdir(self.import_source) == [] def test_005_replace_file(self): """Import the same file again and update existing track""" test_track_path = "testdata/lovecats.mp3" shutil.copy(test_track_path, self.import_source) with patch("file_importer.PickMatch") as MockPickMatch: # Create a mock instance of PickMatch mock_dialog_instance = MagicMock() MockPickMatch.return_value = mock_dialog_instance # Simulate the user clicking OK in the dialog mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted mock_dialog_instance.selected_track_id = 2 # Simulated return value self.widget.import_files_wrapper() # Ensure PickMatch was instantiated correctly MockPickMatch.assert_called_once_with( new_track_description="The Lovecats (The Cure)", choices=[ ("Do not import", -1, ""), ("Import as new track", 0, ""), ( "The Lovecats (The Cure) (100%)", 2, os.path.join( self.musicstore, os.path.basename(test_track_path) ), ), ], default=2, ) # Verify exec() was called mock_dialog_instance.exec.assert_called_once() self.wait_for_workers() # Check track was imported tracks = ds.get_all_tracks() assert len(tracks) == 2 track = tracks[1] assert track.title == "The Lovecats" assert track.artist == "The Cure" assert track.track_id == 2 track_file = os.path.join( self.musicstore, os.path.basename(test_track_path) ) assert track.path == track_file assert os.path.exists(track_file) assert os.listdir(self.import_source) == [] def test_006_import_file_no_tags(self) -> None: """Try to import untagged file""" test_track_path = "testdata/lovecats.mp3" test_filename = os.path.basename(test_track_path) shutil.copy(test_track_path, self.import_source) import_file = os.path.join(self.import_source, test_filename) assert os.path.exists(import_file) # Remove tags src = MP3(import_file) src.delete() src.save() with patch("file_importer.show_OK") as mock_show_ok: self.widget.import_files_wrapper() mock_show_ok.assert_called_once_with( "File not imported", f"{test_filename} will not be imported because of tag errors " f"(Missing tags in {import_file})", ) def test_007_import_unreadable_file(self) -> None: """Import unreadable file""" test_track_path = "testdata/lovecats.mp3" test_filename = os.path.basename(test_track_path) shutil.copy(test_track_path, self.import_source) import_file = os.path.join(self.import_source, test_filename) assert os.path.exists(import_file) # Make undreadable os.chmod(import_file, 0) with patch("file_importer.show_OK") as mock_show_ok: self.widget.import_files_wrapper() mock_show_ok.assert_called_once_with( "File not imported", f"{test_filename} will not be imported because {import_file} is unreadable", ) # clean up os.chmod(import_file, 0o777) os.unlink(import_file) def test_008_import_new_file_existing_destination(self) -> None: """Import duplicate file""" test_track_path = "testdata/lovecats.mp3" test_filename = os.path.basename(test_track_path) new_destination = os.path.join(self.musicstore, "lc2.mp3") shutil.copy(test_track_path, self.import_source) import_file = os.path.join(self.import_source, test_filename) assert os.path.exists(import_file) with ( patch("file_importer.PickMatch") as MockPickMatch, patch.object( QFileDialog, "getSaveFileName", return_value=(new_destination, "") ) as mock_file_dialog, patch("file_importer.show_OK") as mock_show_ok, ): mock_file_dialog.return_value = ( new_destination, "", ) # Ensure mock correctly returns expected value # Create a mock instance of PickMatch mock_dialog_instance = MagicMock() MockPickMatch.return_value = mock_dialog_instance # Simulate the user clicking OK in the dialog mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted mock_dialog_instance.selected_track_id = 0 # Simulated return value self.widget.import_files_wrapper() # Ensure PickMatch was instantiated correctly MockPickMatch.assert_called_once_with( new_track_description="The Lovecats (The Cure)", choices=[ ("Do not import", -1, ""), ("Import as new track", 0, ""), ( "The Lovecats (The Cure) (100%)", 2, os.path.join( self.musicstore, os.path.basename(test_track_path) ), ), ], default=2, ) # Verify exec() was called mock_dialog_instance.exec.assert_called_once() destination = os.path.join(self.musicstore, test_filename) mock_show_ok.assert_called_once_with( title="Desintation path exists", msg=f"New import requested but default destination path ({destination}) " "already exists. Click OK and choose where to save this track", parent=None, ) self.wait_for_workers() # Ensure QFileDialog was called and returned expected value assert mock_file_dialog.called # Ensure the mock was used result = mock_file_dialog() assert result[0] == new_destination # Validate return value # Check track was imported tracks = ds.get_all_tracks() track = tracks[2] assert track.title == "The Lovecats" assert track.artist == "The Cure" assert track.track_id == 3 assert track.path == new_destination assert os.path.exists(new_destination) assert os.listdir(self.import_source) == [] # Remove file so as not to interfere with later tests ds.delete(track) tracks = ds.get_all_tracks() assert len(tracks) == 2 os.unlink(new_destination) assert not os.path.exists(new_destination) def test_009_import_similar_file(self) -> None: """Import file with similar, but different, title""" test_track_path = "testdata/lovecats.mp3" test_filename = os.path.basename(test_track_path) shutil.copy(test_track_path, self.import_source) import_file = os.path.join(self.import_source, test_filename) assert os.path.exists(import_file) # Change title tag src = MP3(import_file) src["TIT2"].text[0] += " xyz" src.save() with patch("file_importer.PickMatch") as MockPickMatch: # Create a mock instance of PickMatch mock_dialog_instance = MagicMock() MockPickMatch.return_value = mock_dialog_instance # Simulate the user clicking OK in the dialog mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted mock_dialog_instance.selected_track_id = 2 # Simulated return value self.widget.import_files_wrapper() # Ensure PickMatch was instantiated correctly MockPickMatch.assert_called_once_with( new_track_description="The Lovecats xyz (The Cure)", choices=[ ("Do not import", -1, ""), ("Import as new track", 0, ""), ( "The Lovecats (The Cure) (93%)", 2, os.path.join( self.musicstore, os.path.basename(test_track_path) ), ), ], default=2, ) # Verify exec() was called mock_dialog_instance.exec.assert_called_once() self.wait_for_workers() # Check track was imported tracks = ds.get_all_tracks() assert len(tracks) == 2 track = tracks[1] assert track.title == "The Lovecats xyz" assert track.artist == "The Cure" assert track.track_id == 2 track_file = os.path.join( self.musicstore, os.path.basename(test_track_path) ) assert track.path == track_file assert os.path.exists(track_file) assert os.listdir(self.import_source) == []