""" 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 # Third party imports import pytest from pytestqt.plugin import QtBot # type: ignore # App imports from app import musicmuster from app.models import ( db, Playlists, Tracks, ) 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) @pytest.mark.usefixtures("qtbot_adapter") 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" with db.Session() as session: playlist = Playlists(session, playlist_name) cls.widget.create_playlist_tab(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 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 # Wait until workers have run to completion # self.qtbot.wait(3000) def workers_empty(): assert FileImporter.workers == {} self.qtbot.waitUntil(workers_empty, timeout=10000) # Check track was imported with db.Session() as session: tracks = Tracks.get_all(session) 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 # Allow time for import thread to run self.qtbot.wait(3000) # Check track was imported with db.Session() as session: tracks = Tracks.get_all(session) 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""" 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 = 1 # 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() # Ensure selected_track_id was accessed after dialog.exec() assert mock_dialog_instance.selected_track_id == 1 # Allow time for import thread to run self.qtbot.wait(3000) # Check track was imported with db.Session() as session: tracks = Tracks.get_all(session) 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_import_replace_file # def test_import_similar_file # def test_import_new_file_existing_destination # def test_import_file_and_cancel # def test_import_file_no_tags # def test_import_unreadable_file