From 601fa58594ebd7c02258f03ce0581b263cbc7f35 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sun, 15 Nov 2020 23:16:10 -0700 Subject: [PATCH 1/2] Fix some upgrade issues - Fix an issue with loading screens from older versions of OpenLP (fixes #655) - Fix an issue where presentations that no longer exist throw an error - Fix a bug in the serialization of Path objects for Pyro4 - Add some tests for untested scenarios - Fix some tests --- openlp/core/common/__init__.py | 6 +- .../plugins/presentations/lib/serializers.py | 7 +- .../openlp_core/common/test_common.py | 339 ----------- .../openlp_core/common/test_init.py | 534 +++++++++++++++++- .../openlp_core/common/test_utils.py | 2 +- .../projectors/test_projector_utilities.py | 185 ------ .../presentations/lib/test_serializers.py | 70 +++ 7 files changed, 606 insertions(+), 537 deletions(-) delete mode 100644 tests/functional/openlp_core/common/test_common.py delete mode 100644 tests/openlp_core/projectors/test_projector_utilities.py create mode 100644 tests/openlp_plugins/presentations/lib/test_serializers.py diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 25a10ae3e..0cd298731 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -308,9 +308,11 @@ def sha256_file_hash(filename): :param filename: Name of the file to hash :returns: str """ - log.debug('sha256_hash(filename="{filename}")'.format(filename=filename)) + log.debug('sha256_file_hash(filename="{filename}")'.format(filename=filename)) hash_obj = hashlib.sha256() - with open(filename, 'rb') as f: + if not filename.exists(): + return None + with filename.open('rb') as f: for chunk in iter(lambda: f.read(65536), b''): hash_obj.update(chunk) return hash_obj.hexdigest() diff --git a/openlp/plugins/presentations/lib/serializers.py b/openlp/plugins/presentations/lib/serializers.py index 0cfea28fb..9ac959921 100644 --- a/openlp/plugins/presentations/lib/serializers.py +++ b/openlp/plugins/presentations/lib/serializers.py @@ -21,10 +21,7 @@ """ This module contains some helpers for serializing Path objects in Pyro4 """ -try: - from openlp.core.common.path import Path -except ImportError: - from pathlib import Path +from openlp.core.common.path import Path from Pyro4.util import SerializerBase @@ -40,7 +37,7 @@ def path_class_to_dict(obj): def path_dict_to_class(classname, d): - return Path(d['parts']) + return Path(*d['parts']) def register_classes(): diff --git a/tests/functional/openlp_core/common/test_common.py b/tests/functional/openlp_core/common/test_common.py deleted file mode 100644 index f4db48367..000000000 --- a/tests/functional/openlp_core/common/test_common.py +++ /dev/null @@ -1,339 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 OpenLP Developers # -# ---------------------------------------------------------------------- # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -########################################################################## -""" -Functional tests to test the AppLocation class and related methods. -""" -from pathlib import Path -from unittest import skipUnless -from unittest.mock import MagicMock, call, patch - -from openlp.core.common import Singleton, clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \ - is_64bit_instance, normalize_str, path_to_module, trace_error_handler - - -def test_extension_loader_no_files_found(): - """ - Test the `extension_loader` function when no files are found - """ - # GIVEN: A mocked `Path.glob` method which does not match any files - with patch('openlp.core.common.applocation.AppLocation.get_directory', - return_value=Path('/', 'app', 'dir', 'openlp')), \ - patch.object(Path, 'glob', return_value=[]), \ - patch('openlp.core.common.importlib.import_module') as mocked_import_module: - - # WHEN: Calling `extension_loader` - extension_loader('glob', ['file2.py', 'file3.py']) - - # THEN: `extension_loader` should not try to import any files - assert mocked_import_module.called is False - - -def test_extension_loader_files_found(): - """ - Test the `extension_loader` function when it successfully finds and loads some files - """ - # GIVEN: A mocked `Path.glob` method which returns a list of files - with patch('openlp.core.common.applocation.AppLocation.get_directory', - return_value=Path('/', 'app', 'dir', 'openlp')), \ - patch.object(Path, 'glob', return_value=[ - Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file1.py'), - Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file2.py'), - Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file3.py'), - Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file4.py')]), \ - patch('openlp.core.common.importlib.import_module') as mocked_import_module: - - # WHEN: Calling `extension_loader` with a list of files to exclude - extension_loader('glob', ['file2.py', 'file3.py']) - - # THEN: `extension_loader` should only try to import the files that are matched by the blob, excluding the - # files listed in the `excluded_files` argument - mocked_import_module.assert_has_calls([call('openlp.import_dir.file1'), - call('openlp.import_dir.file4')]) - - -def test_extension_loader_import_error(): - """ - Test the `extension_loader` function when `SourceFileLoader` raises a `ImportError` - """ - # GIVEN: A mocked `import_module` which raises an `ImportError` - with patch('openlp.core.common.applocation.AppLocation.get_directory', - return_value=Path('/', 'app', 'dir', 'openlp')), \ - patch.object(Path, 'glob', return_value=[ - Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file1.py')]), \ - patch('openlp.core.common.importlib.import_module', side_effect=ImportError()), \ - patch('openlp.core.common.log') as mocked_logger: - - # WHEN: Calling `extension_loader` - extension_loader('glob') - - # THEN: The `ImportError` should be caught and logged - assert mocked_logger.exception.called - - -def test_extension_loader_os_error(): - """ - Test the `extension_loader` function when `import_module` raises a `ImportError` - """ - # GIVEN: A mocked `SourceFileLoader` which raises an `OSError` - with patch('openlp.core.common.applocation.AppLocation.get_directory', - return_value=Path('/', 'app', 'dir', 'openlp')), \ - patch.object(Path, 'glob', return_value=[ - Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file1.py')]), \ - patch('openlp.core.common.importlib.import_module', side_effect=OSError()), \ - patch('openlp.core.common.log') as mocked_logger: - - # WHEN: Calling `extension_loader` - extension_loader('glob') - - # THEN: The `OSError` should be caught and logged - assert mocked_logger.exception.called - - -def test_de_hump_conversion(): - """ - Test the de_hump function with a class name - """ - # GIVEN: a Class name in Camel Case - string = "MyClass" - - # WHEN: we call de_hump - new_string = de_hump(string) - - # THEN: the new string should be converted to python format - assert new_string == "my_class", 'The class name should have been converted' - - -def test_de_hump_static(): - """ - Test the de_hump function with a python string - """ - # GIVEN: a Class name in Camel Case - string = "my_class" - - # WHEN: we call de_hump - new_string = de_hump(string) - - # THEN: the new string should be converted to python format - assert new_string == "my_class", 'The class name should have been preserved' - - -def test_path_to_module(): - """ - Test `path_to_module` when supplied with a `Path` object - """ - # GIVEN: A `Path` object - path = Path('core', 'ui', 'media', 'vlcplayer.py') - - # WHEN: Calling path_to_module with the `Path` object - result = path_to_module(path) - - # THEN: path_to_module should return the module name - assert result == 'openlp.core.ui.media.vlcplayer' - - -def test_trace_error_handler(): - """ - Test the trace_error_handler() method - """ - # GIVEN: Mocked out objects - with patch('openlp.core.common.traceback') as mocked_traceback: - mocked_traceback.extract_stack.return_value = [('openlp.fake', 56, None, 'trace_error_handler_test')] - mocked_logger = MagicMock() - - # WHEN: trace_error_handler() is called - trace_error_handler(mocked_logger) - - # THEN: The mocked_logger.error() method should have been called with the correct parameters - mocked_logger.error.assert_called_with( - 'OpenLP Error trace\n File openlp.fake at line 56 \n\t called trace_error_handler_test') - - -def test_singleton_metaclass_multiple_init(): - """ - Test that a class using the Singleton Metaclass is only initialised once despite being called several times and - that the same instance is returned each time.. - """ - # GIVEN: The Singleton Metaclass and a test class using it - class SingletonClass(metaclass=Singleton): - def __init__(self): - pass - - with patch.object(SingletonClass, '__init__', return_value=None) as patched_init: - - # WHEN: Initialising the class multiple times - inst_1 = SingletonClass() - inst_2 = SingletonClass() - - # THEN: The __init__ method of the SingletonClass should have only been called once, and both returned values - # should be the same instance. - assert inst_1 is inst_2 - assert patched_init.call_count == 1 - - -def test_singleton_metaclass_multiple_classes(): - """ - Test that multiple classes using the Singleton Metaclass return the different an appropriate instances. - """ - # GIVEN: Two different classes using the Singleton Metaclass - class SingletonClass1(metaclass=Singleton): - def __init__(self): - pass - - class SingletonClass2(metaclass=Singleton): - def __init__(self): - pass - - # WHEN: Initialising both classes - s_c1 = SingletonClass1() - s_c2 = SingletonClass2() - - # THEN: The instances should be an instance of the appropriate class - assert isinstance(s_c1, SingletonClass1) - assert isinstance(s_c2, SingletonClass2) - - -def test_is_win(): - """ - Test the is_win() function - """ - # GIVEN: Mocked out objects - with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: - - # WHEN: The mocked os.name and sys.platform are set to 'nt' and 'win32' repectivly - mocked_os.name = 'nt' - mocked_sys.platform = 'win32' - - # THEN: The three platform functions should perform properly - assert is_win() is True, 'is_win() should return True' - assert is_macosx() is False, 'is_macosx() should return False' - assert is_linux() is False, 'is_linux() should return False' - - -def test_is_macosx(): - """ - Test the is_macosx() function - """ - # GIVEN: Mocked out objects - with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: - - # WHEN: The mocked os.name and sys.platform are set to 'posix' and 'darwin' repectivly - mocked_os.name = 'posix' - mocked_sys.platform = 'darwin' - - # THEN: The three platform functions should perform properly - assert is_macosx() is True, 'is_macosx() should return True' - assert is_win() is False, 'is_win() should return False' - assert is_linux() is False, 'is_linux() should return False' - - -def test_is_linux(): - """ - Test the is_linux() function - """ - # GIVEN: Mocked out objects - with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: - - # WHEN: The mocked os.name and sys.platform are set to 'posix' and 'linux3' repectively - mocked_os.name = 'posix' - mocked_sys.platform = 'linux3' - - # THEN: The three platform functions should perform properly - assert is_linux() is True, 'is_linux() should return True' - assert is_win() is False, 'is_win() should return False' - assert is_macosx() is False, 'is_macosx() should return False' - - -@skipUnless(is_linux(), 'This can only run on Linux') -def test_is_linux_distro(): - """ - Test the is_linux() function for a particular Linux distribution - """ - # GIVEN: Mocked out objects - with patch('openlp.core.common.os') as mocked_os, \ - patch('openlp.core.common.sys') as mocked_sys, \ - patch('openlp.core.common.distro_id') as mocked_distro_id: - - # WHEN: The mocked os.name and sys.platform are set to 'posix' and 'linux3' repectively - # and the distro is Fedora - mocked_os.name = 'posix' - mocked_sys.platform = 'linux3' - mocked_distro_id.return_value = 'fedora' - - # THEN: The three platform functions should perform properly - assert is_linux(distro='fedora') is True, 'is_linux(distro="fedora") should return True' - assert is_win() is False, 'is_win() should return False' - assert is_macosx() is False, 'is_macosx() should return False' - - -def test_is_64bit_instance(): - """ - Test the is_64bit_instance() function - """ - # GIVEN: Mocked out objects - with patch('openlp.core.common.sys') as mocked_sys: - - # WHEN: The mocked sys.maxsize is set to 32-bit - mocked_sys.maxsize = 2**32 - - # THEN: The result should be False - assert is_64bit_instance() is False, 'is_64bit_instance() should return False' - - -def test_normalize_str_leaves_newlines(): - # GIVEN: a string containing newlines - string = 'something\nelse' - # WHEN: normalize is called - normalized_string = normalize_str(string) - # THEN: string is unchanged - assert normalized_string == string - - -def test_normalize_str_removes_null_byte(): - # GIVEN: a string containing a null byte - string = 'somet\x00hing' - # WHEN: normalize is called - normalized_string = normalize_str(string) - # THEN: nullbyte is removed - assert normalized_string == 'something' - - -def test_normalize_str_replaces_crlf_with_lf(): - # GIVEN: a string containing crlf - string = 'something\r\nelse' - # WHEN: normalize is called - normalized_string = normalize_str(string) - # THEN: crlf is replaced with lf - assert normalized_string == 'something\nelse' - - -def test_clean_button_text(): - """ - Test the clean_button_text() function. - """ - # GIVEN: Button text - input_text = '&Next >' - expected_text = 'Next' - - # WHEN: The button caption is sent through the clean_button_text function - actual_text = clean_button_text(input_text) - - # THEN: The text should have been cleaned - assert expected_text == actual_text, 'The text should be clean' diff --git a/tests/functional/openlp_core/common/test_init.py b/tests/functional/openlp_core/common/test_init.py index 0dcc5b6b7..980fe04e4 100644 --- a/tests/functional/openlp_core/common/test_init.py +++ b/tests/functional/openlp_core/common/test_init.py @@ -23,10 +23,534 @@ Functional tests to test the AppLocation class and related methods. """ from io import BytesIO from pathlib import Path +from unittest import skipUnless from unittest.mock import MagicMock, PropertyMock, call, patch -from openlp.core.common import add_actions, clean_filename, delete_file, get_file_encoding, get_filesystem_encoding, \ - get_uno_command, get_uno_instance +import pytest + +from openlp.core.common import Singleton, add_actions, clean_filename, clean_button_text, de_hump, delete_file, \ + extension_loader, get_file_encoding, get_filesystem_encoding, get_uno_command, get_uno_instance, is_linux, \ + is_macosx, is_win, is_64bit_instance, md5_hash, normalize_str, path_to_module, qmd5_hash, sha256_file_hash, \ + trace_error_handler, verify_ip_address + +from tests.resources.projector.data import TEST_HASH, TEST_PIN, TEST_SALT + + +test_non_ascii_string = '이것은 한국어 시험 문자열' +test_non_ascii_hash = 'fc00c7912976f6e9c19099b514ced201' + +ip4_loopback = '127.0.0.1' +ip4_local = '192.168.1.1' +ip4_broadcast = '255.255.255.255' +ip4_bad = '192.168.1.256' + +ip6_loopback = '::1' +ip6_link_local = 'fe80::223:14ff:fe99:d315' +ip6_bad = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + + +def test_extension_loader_no_files_found(): + """ + Test the `extension_loader` function when no files are found + """ + # GIVEN: A mocked `Path.glob` method which does not match any files + with patch('openlp.core.common.applocation.AppLocation.get_directory', + return_value=Path('/', 'app', 'dir', 'openlp')), \ + patch.object(Path, 'glob', return_value=[]), \ + patch('openlp.core.common.importlib.import_module') as mocked_import_module: + + # WHEN: Calling `extension_loader` + extension_loader('glob', ['file2.py', 'file3.py']) + + # THEN: `extension_loader` should not try to import any files + assert mocked_import_module.called is False + + +def test_extension_loader_files_found(): + """ + Test the `extension_loader` function when it successfully finds and loads some files + """ + # GIVEN: A mocked `Path.glob` method which returns a list of files + with patch('openlp.core.common.applocation.AppLocation.get_directory', + return_value=Path('/', 'app', 'dir', 'openlp')), \ + patch.object(Path, 'glob', return_value=[ + Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file1.py'), + Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file2.py'), + Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file3.py'), + Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file4.py')]), \ + patch('openlp.core.common.importlib.import_module') as mocked_import_module: + + # WHEN: Calling `extension_loader` with a list of files to exclude + extension_loader('glob', ['file2.py', 'file3.py']) + + # THEN: `extension_loader` should only try to import the files that are matched by the blob, excluding the + # files listed in the `excluded_files` argument + mocked_import_module.assert_has_calls([call('openlp.import_dir.file1'), + call('openlp.import_dir.file4')]) + + +def test_extension_loader_import_error(): + """ + Test the `extension_loader` function when `SourceFileLoader` raises a `ImportError` + """ + # GIVEN: A mocked `import_module` which raises an `ImportError` + with patch('openlp.core.common.applocation.AppLocation.get_directory', + return_value=Path('/', 'app', 'dir', 'openlp')), \ + patch.object(Path, 'glob', return_value=[ + Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file1.py')]), \ + patch('openlp.core.common.importlib.import_module', side_effect=ImportError()), \ + patch('openlp.core.common.log') as mocked_logger: + + # WHEN: Calling `extension_loader` + extension_loader('glob') + + # THEN: The `ImportError` should be caught and logged + assert mocked_logger.exception.called + + +def test_extension_loader_os_error(): + """ + Test the `extension_loader` function when `import_module` raises a `ImportError` + """ + # GIVEN: A mocked `SourceFileLoader` which raises an `OSError` + with patch('openlp.core.common.applocation.AppLocation.get_directory', + return_value=Path('/', 'app', 'dir', 'openlp')), \ + patch.object(Path, 'glob', return_value=[ + Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file1.py')]), \ + patch('openlp.core.common.importlib.import_module', side_effect=OSError()), \ + patch('openlp.core.common.log') as mocked_logger: + + # WHEN: Calling `extension_loader` + extension_loader('glob') + + # THEN: The `OSError` should be caught and logged + assert mocked_logger.exception.called + + +def test_de_hump_conversion(): + """ + Test the de_hump function with a class name + """ + # GIVEN: a Class name in Camel Case + string = "MyClass" + + # WHEN: we call de_hump + new_string = de_hump(string) + + # THEN: the new string should be converted to python format + assert new_string == "my_class", 'The class name should have been converted' + + +def test_de_hump_static(): + """ + Test the de_hump function with a python string + """ + # GIVEN: a Class name in Camel Case + string = "my_class" + + # WHEN: we call de_hump + new_string = de_hump(string) + + # THEN: the new string should be converted to python format + assert new_string == "my_class", 'The class name should have been preserved' + + +def test_path_to_module(): + """ + Test `path_to_module` when supplied with a `Path` object + """ + # GIVEN: A `Path` object + path = Path('core', 'ui', 'media', 'vlcplayer.py') + + # WHEN: Calling path_to_module with the `Path` object + result = path_to_module(path) + + # THEN: path_to_module should return the module name + assert result == 'openlp.core.ui.media.vlcplayer' + + +def test_trace_error_handler(): + """ + Test the trace_error_handler() method + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.traceback') as mocked_traceback: + mocked_traceback.extract_stack.return_value = [('openlp.fake', 56, None, 'trace_error_handler_test')] + mocked_logger = MagicMock() + + # WHEN: trace_error_handler() is called + trace_error_handler(mocked_logger) + + # THEN: The mocked_logger.error() method should have been called with the correct parameters + mocked_logger.error.assert_called_with( + 'OpenLP Error trace\n File openlp.fake at line 56 \n\t called trace_error_handler_test') + + +def test_singleton_metaclass_multiple_init(): + """ + Test that a class using the Singleton Metaclass is only initialised once despite being called several times and + that the same instance is returned each time.. + """ + # GIVEN: The Singleton Metaclass and a test class using it + class SingletonClass(metaclass=Singleton): + def __init__(self): + pass + + with patch.object(SingletonClass, '__init__', return_value=None) as patched_init: + + # WHEN: Initialising the class multiple times + inst_1 = SingletonClass() + inst_2 = SingletonClass() + + # THEN: The __init__ method of the SingletonClass should have only been called once, and both returned values + # should be the same instance. + assert inst_1 is inst_2 + assert patched_init.call_count == 1 + + +def test_singleton_metaclass_multiple_classes(): + """ + Test that multiple classes using the Singleton Metaclass return the different an appropriate instances. + """ + # GIVEN: Two different classes using the Singleton Metaclass + class SingletonClass1(metaclass=Singleton): + def __init__(self): + pass + + class SingletonClass2(metaclass=Singleton): + def __init__(self): + pass + + # WHEN: Initialising both classes + s_c1 = SingletonClass1() + s_c2 = SingletonClass2() + + # THEN: The instances should be an instance of the appropriate class + assert isinstance(s_c1, SingletonClass1) + assert isinstance(s_c2, SingletonClass2) + + +def test_is_win(): + """ + Test the is_win() function + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: + + # WHEN: The mocked os.name and sys.platform are set to 'nt' and 'win32' repectivly + mocked_os.name = 'nt' + mocked_sys.platform = 'win32' + + # THEN: The three platform functions should perform properly + assert is_win() is True, 'is_win() should return True' + assert is_macosx() is False, 'is_macosx() should return False' + assert is_linux() is False, 'is_linux() should return False' + + +def test_is_macosx(): + """ + Test the is_macosx() function + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: + + # WHEN: The mocked os.name and sys.platform are set to 'posix' and 'darwin' repectivly + mocked_os.name = 'posix' + mocked_sys.platform = 'darwin' + + # THEN: The three platform functions should perform properly + assert is_macosx() is True, 'is_macosx() should return True' + assert is_win() is False, 'is_win() should return False' + assert is_linux() is False, 'is_linux() should return False' + + +def test_is_linux(): + """ + Test the is_linux() function + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: + + # WHEN: The mocked os.name and sys.platform are set to 'posix' and 'linux3' repectively + mocked_os.name = 'posix' + mocked_sys.platform = 'linux3' + + # THEN: The three platform functions should perform properly + assert is_linux() is True, 'is_linux() should return True' + assert is_win() is False, 'is_win() should return False' + assert is_macosx() is False, 'is_macosx() should return False' + + +@skipUnless(is_linux(), 'This can only run on Linux') +def test_is_linux_distro(): + """ + Test the is_linux() function for a particular Linux distribution + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.os') as mocked_os, \ + patch('openlp.core.common.sys') as mocked_sys, \ + patch('openlp.core.common.distro_id') as mocked_distro_id: + + # WHEN: The mocked os.name and sys.platform are set to 'posix' and 'linux3' repectively + # and the distro is Fedora + mocked_os.name = 'posix' + mocked_sys.platform = 'linux3' + mocked_distro_id.return_value = 'fedora' + + # THEN: The three platform functions should perform properly + assert is_linux(distro='fedora') is True, 'is_linux(distro="fedora") should return True' + assert is_win() is False, 'is_win() should return False' + assert is_macosx() is False, 'is_macosx() should return False' + + +def test_is_64bit_instance(): + """ + Test the is_64bit_instance() function + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.sys') as mocked_sys: + + # WHEN: The mocked sys.maxsize is set to 32-bit + mocked_sys.maxsize = 2**32 + + # THEN: The result should be False + assert is_64bit_instance() is False, 'is_64bit_instance() should return False' + + +def test_normalize_str_leaves_newlines(): + # GIVEN: a string containing newlines + string = 'something\nelse' + # WHEN: normalize is called + normalized_string = normalize_str(string) + # THEN: string is unchanged + assert normalized_string == string + + +def test_normalize_str_removes_null_byte(): + # GIVEN: a string containing a null byte + string = 'somet\x00hing' + # WHEN: normalize is called + normalized_string = normalize_str(string) + # THEN: nullbyte is removed + assert normalized_string == 'something' + + +def test_normalize_str_replaces_crlf_with_lf(): + # GIVEN: a string containing crlf + string = 'something\r\nelse' + # WHEN: normalize is called + normalized_string = normalize_str(string) + # THEN: crlf is replaced with lf + assert normalized_string == 'something\nelse' + + +def test_clean_button_text(): + """ + Test the clean_button_text() function. + """ + # GIVEN: Button text + input_text = '&Next >' + expected_text = 'Next' + + # WHEN: The button caption is sent through the clean_button_text function + actual_text = clean_button_text(input_text) + + # THEN: The text should have been cleaned + assert expected_text == actual_text, 'The text should be clean' + + +def test_ip4_loopback_valid(): + """ + Test IPv4 loopbackvalid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_loopback) + + # THEN: Verify we received True + assert valid, 'IPv4 loopback address should have been valid' + + +def test_ip4_local_valid(): + """ + Test IPv4 local valid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_local) + + # THEN: Verify we received True + assert valid is True, 'IPv4 local address should have been valid' + + +def test_ip4_broadcast_valid(): + """ + Test IPv4 broadcast valid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_broadcast) + + # THEN: Verify we received True + assert valid is True, 'IPv4 broadcast address should have been valid' + + +def test_ip4_address_invalid(): + """ + Test IPv4 address invalid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_bad) + + # THEN: Verify we received True + assert valid is False, 'Bad IPv4 address should not have been valid' + + +def test_ip6_loopback_valid(): + """ + Test IPv6 loopback valid + """ + # WHEN: Test IPv6 loopback address + valid = verify_ip_address(addr=ip6_loopback) + + # THEN: Validate return + assert valid is True, 'IPv6 loopback address should have been valid' + + +def test_ip6_local_valid(): + """ + Test IPv6 link-local valid + """ + # WHEN: Test IPv6 link-local address + valid = verify_ip_address(addr=ip6_link_local) + + # THEN: Validate return + assert valid is True, 'IPv6 link-local address should have been valid' + + +def test_ip6_address_invalid(): + """ + Test NetworkUtils IPv6 address invalid + """ + # WHEN: Given an invalid IPv6 address + valid = verify_ip_address(addr=ip6_bad) + + # THEN: Validate bad return + assert valid is False, 'IPv6 bad address should have been invalid' + + +def test_sha256_file_hash(): + """ + Test SHA256 file hash + """ + # GIVEN: A mocked Path object + filename = Path('tests/resources/presentations/test.ppt') + + # WHEN: Given a known salt+data + result = sha256_file_hash(filename) + + # THEN: Validate return has is same + assert result == 'be6d7bdca25d1662d7faa1f856bfc224646dbad3b65ebff800d9ae70537968f9' + + +def test_sha256_file_hash_no_exist(): + """ + Test SHA256 file hash when the file doesn't exist + """ + # GIVEN: A mocked Path object + mocked_path = MagicMock() + mocked_path.exists.return_value = False + + # WHEN: Given a known salt+data + result = sha256_file_hash(mocked_path) + + # THEN: Validate return has is same + assert result is None + + +def test_md5_hash(): + """ + Test MD5 hash from salt+data pass (python) + """ + # WHEN: Given a known salt+data + hash_ = md5_hash(salt=TEST_SALT.encode('utf-8'), data=TEST_PIN.encode('utf-8')) + + # THEN: Validate return has is same + assert hash_ == TEST_HASH, 'MD5 should have returned a good hash' + + +def test_md5_hash_no_salt_data(): + """ + Test MD5 hash with no salt or data (Python) + """ + # WHEN: Given a known salt+data + hash_ = md5_hash(None, None) + + # THEN: Validate return has is same + assert hash_ is None, 'MD5 should have returned None' + + +def test_md5_hash_bad(): + """ + Test MD5 hash from salt+data fail (python) + """ + # WHEN: Given a different salt+hash + hash_ = md5_hash(salt=TEST_PIN.encode('utf-8'), data=TEST_SALT.encode('utf-8')) + + # THEN: return data is different + assert hash_ is not TEST_HASH, 'MD5 should have returned a bad hash' + + +def test_qmd5_hash(): + """ + Test MD5 hash from salt+data pass (Qt) + """ + # WHEN: Given a known salt+data + hash_ = qmd5_hash(salt=TEST_SALT.encode('utf-8'), data=TEST_PIN.encode('utf-8')) + + # THEN: Validate return has is same + assert hash_ == TEST_HASH, 'Qt-MD5 should have returned a good hash' + + +def test_qmd5_hash_no_salt_data(): + """ + Test MD5 hash with no salt or data (Qt) + """ + # WHEN: Given a known salt+data + hash_ = qmd5_hash(None, None) + + # THEN: Validate return has is same + assert hash_ is None, 'Qt-MD5 should have returned None' + + +def test_qmd5_hash_bad(): + """ + Test MD5 hash from salt+hash fail (Qt) + """ + # WHEN: Given a different salt+hash + hash_ = qmd5_hash(salt=TEST_PIN.encode('utf-8'), data=TEST_SALT.encode('utf-8')) + + # THEN: return data is different + assert hash_ is not TEST_HASH, 'Qt-MD5 should have returned a bad hash' + + +def test_md5_non_ascii_string(): + """ + Test MD5 hash with non-ascii string - bug 1417809 + """ + # WHEN: Non-ascii string is hashed + hash_ = md5_hash(salt=test_non_ascii_string.encode('utf-8'), data=None) + + # THEN: Valid MD5 hash should be returned + assert hash_ == test_non_ascii_hash, 'MD5 should have returned a valid hash' + + +def test_qmd5_non_ascii_string(): + """ + Test MD5 hash with non-ascii string - bug 1417809 + """ + # WHEN: Non-ascii string is hashed + hash_ = md5_hash(data=test_non_ascii_string.encode('utf-8')) + + # THEN: Valid MD5 hash should be returned + assert hash_ == test_non_ascii_hash, 'Qt-MD5 should have returned a valid hash' def test_add_actions_empty_list(): @@ -165,11 +689,11 @@ def test_get_uno_command_when_no_command_exists(): """ # GIVEN: A patched 'which' method which returns None - with patch('openlp.core.common.which', **{'return_value': None}): + with pytest.raises(FileNotFoundError), \ + patch('openlp.core.common.which', **{'return_value': None}): # WHEN: Calling get_uno_command - # THEN: a FileNotFoundError exception should be raised - assert FileNotFoundError, get_uno_command + get_uno_command() def test_get_uno_command_connection_type(): diff --git a/tests/interfaces/openlp_core/common/test_utils.py b/tests/interfaces/openlp_core/common/test_utils.py index ef75fb674..0a911872e 100644 --- a/tests/interfaces/openlp_core/common/test_utils.py +++ b/tests/interfaces/openlp_core/common/test_utils.py @@ -59,7 +59,7 @@ def test_is_not_image_with_none_image_file(): Test the method handles a non image file """ # Given and empty string - file_path = RESOURCE_PATH / 'serviceitem_custom_1.osj' + file_path = RESOURCE_PATH / 'presentations' / 'test.ppt' # WHEN testing for it result = is_not_image_file(file_path) diff --git a/tests/openlp_core/projectors/test_projector_utilities.py b/tests/openlp_core/projectors/test_projector_utilities.py deleted file mode 100644 index 8caef0c8b..000000000 --- a/tests/openlp_core/projectors/test_projector_utilities.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 OpenLP Developers # -# ---------------------------------------------------------------------- # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -########################################################################## -""" -Package to test the openlp.core.ui.projector.networkutils package. -""" - -from openlp.core.common import md5_hash, qmd5_hash, verify_ip_address -from tests.resources.projector.data import TEST_HASH, TEST_PIN, TEST_SALT - - -salt = TEST_SALT -pin = TEST_PIN -test_hash = TEST_HASH -test_non_ascii_string = '이것은 한국어 시험 문자열' -test_non_ascii_hash = 'fc00c7912976f6e9c19099b514ced201' - -ip4_loopback = '127.0.0.1' -ip4_local = '192.168.1.1' -ip4_broadcast = '255.255.255.255' -ip4_bad = '192.168.1.256' - -ip6_loopback = '::1' -ip6_link_local = 'fe80::223:14ff:fe99:d315' -ip6_bad = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' - - -def test_ip4_loopback_valid(): - """ - Test IPv4 loopbackvalid - """ - # WHEN: Test with a local loopback test - valid = verify_ip_address(addr=ip4_loopback) - - # THEN: Verify we received True - assert valid, 'IPv4 loopback address should have been valid' - - -def test_ip4_local_valid(): - """ - Test IPv4 local valid - """ - # WHEN: Test with a local loopback test - valid = verify_ip_address(addr=ip4_local) - - # THEN: Verify we received True - assert valid is True, 'IPv4 local address should have been valid' - - -def test_ip4_broadcast_valid(): - """ - Test IPv4 broadcast valid - """ - # WHEN: Test with a local loopback test - valid = verify_ip_address(addr=ip4_broadcast) - - # THEN: Verify we received True - assert valid is True, 'IPv4 broadcast address should have been valid' - - -def test_ip4_address_invalid(): - """ - Test IPv4 address invalid - """ - # WHEN: Test with a local loopback test - valid = verify_ip_address(addr=ip4_bad) - - # THEN: Verify we received True - assert valid is False, 'Bad IPv4 address should not have been valid' - - -def test_ip6_loopback_valid(): - """ - Test IPv6 loopback valid - """ - # WHEN: Test IPv6 loopback address - valid = verify_ip_address(addr=ip6_loopback) - - # THEN: Validate return - assert valid is True, 'IPv6 loopback address should have been valid' - - -def test_ip6_local_valid(): - """ - Test IPv6 link-local valid - """ - # WHEN: Test IPv6 link-local address - valid = verify_ip_address(addr=ip6_link_local) - - # THEN: Validate return - assert valid is True, 'IPv6 link-local address should have been valid' - - -def test_ip6_address_invalid(): - """ - Test NetworkUtils IPv6 address invalid - """ - # WHEN: Given an invalid IPv6 address - valid = verify_ip_address(addr=ip6_bad) - - # THEN: Validate bad return - assert valid is False, 'IPv6 bad address should have been invalid' - - -def test_md5_hash(): - """ - Test MD5 hash from salt+data pass (python) - """ - # WHEN: Given a known salt+data - hash_ = md5_hash(salt=salt.encode('utf-8'), data=pin.encode('utf-8')) - - # THEN: Validate return has is same - assert hash_ == test_hash, 'MD5 should have returned a good hash' - - -def test_md5_hash_bad(): - """ - Test MD5 hash from salt+data fail (python) - """ - # WHEN: Given a different salt+hash - hash_ = md5_hash(salt=pin.encode('utf-8'), data=salt.encode('utf-8')) - - # THEN: return data is different - assert hash_ is not test_hash, 'MD5 should have returned a bad hash' - - -def test_qmd5_hash(): - """ - Test MD5 hash from salt+data pass (Qt) - """ - # WHEN: Given a known salt+data - hash_ = qmd5_hash(salt=salt.encode('utf-8'), data=pin.encode('utf-8')) - - # THEN: Validate return has is same - assert hash_ == test_hash, 'Qt-MD5 should have returned a good hash' - - -def test_qmd5_hash_bad(): - """ - Test MD5 hash from salt+hash fail (Qt) - """ - # WHEN: Given a different salt+hash - hash_ = qmd5_hash(salt=pin.encode('utf-8'), data=salt.encode('utf-8')) - - # THEN: return data is different - assert hash_ is not test_hash, 'Qt-MD5 should have returned a bad hash' - - -def test_md5_non_ascii_string(): - """ - Test MD5 hash with non-ascii string - bug 1417809 - """ - # WHEN: Non-ascii string is hashed - hash_ = md5_hash(salt=test_non_ascii_string.encode('utf-8'), data=None) - - # THEN: Valid MD5 hash should be returned - assert hash_ == test_non_ascii_hash, 'MD5 should have returned a valid hash' - - -def test_qmd5_non_ascii_string(): - """ - Test MD5 hash with non-ascii string - bug 1417809 - """ - # WHEN: Non-ascii string is hashed - hash_ = md5_hash(data=test_non_ascii_string.encode('utf-8')) - - # THEN: Valid MD5 hash should be returned - assert hash_ == test_non_ascii_hash, 'Qt-MD5 should have returned a valid hash' diff --git a/tests/openlp_plugins/presentations/lib/test_serializers.py b/tests/openlp_plugins/presentations/lib/test_serializers.py new file mode 100644 index 000000000..aa99e6dc1 --- /dev/null +++ b/tests/openlp_plugins/presentations/lib/test_serializers.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +########################################################################## +# OpenLP - Open Source Lyrics Projection # +# ---------------------------------------------------------------------- # +# Copyright (c) 2008-2020 OpenLP Developers # +# ---------------------------------------------------------------------- # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +########################################################################## +""" +Package to test the openlp.core.lib package. +""" +from pathlib import Path +from unittest.mock import patch + +try: + import Pyro4 # noqa + from openlp.plugins.presentations.lib.serializers import path_class_to_dict, path_dict_to_class, register_classes +except ImportError: + import pytest + pytestmark = pytest.mark.skip('Pyro4 not installed') + + +def test_path_class_to_dict(): + """Test that the path_class_to_dict() method returns a correctly formatted dictionary""" + # GIVEN: A Path object + path = Path('openlp/core/ui/aboutform.py') + + # WHEN: path_class_to_dict() is called with the given Path object + result = path_class_to_dict(path) + + # THEN: The dictionary should be formatted correctly + assert result == {'__class__': 'Path', 'parts': ('openlp', 'core', 'ui', 'aboutform.py')} + + +def test_path_dict_to_class(): + """Test that the path_dict_to_class() method returns the right object""" + # GIVEN: A dictionary that was created from a Path object + path_dict = {'__class__': 'Path', 'parts': ('openlp', 'core', 'app.py')} + + # WHEN: path_dict_to_class() is called with the given dictionary + result = path_dict_to_class('Path', path_dict) + + # THEN: The correct Path object should have been created + assert isinstance(result, Path) + assert result == Path('openlp/core/app.py') + + +@patch('openlp.plugins.presentations.lib.serializers.SerializerBase') +def test_register_classes(MockSerializerBase): + """Test that the register_classes() method registers the two functions""" + # GIVEN: A mocked out SerializerBase + + # WHEN: register_classes() is run + register_classes() + + # THEN: The functions should have been registered + MockSerializerBase.register_class_to_dict.assert_called_once_with(Path, path_class_to_dict) + MockSerializerBase.register_dict_to_class.assert_called_once_with('Path', path_dict_to_class) From 06e0d9d5224b49a8fa5694a86ee07d80a01bd63d Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 17 Nov 2020 03:59:38 +0000 Subject: [PATCH 2/2] Add a delay to closing vlc This gives time for the web engine to appear, preventing any flicker to the desktop --- openlp/core/display/html/display.js | 3 - openlp/core/ui/media/mediacontroller.py | 279 ++++++++++++------ openlp/core/ui/media/vlcplayer.py | 34 +-- openlp/core/ui/slidecontroller.py | 41 ++- openlp/core/ui/starttimeform.py | 11 +- .../ui/media/test_mediacontroller.py | 120 +++++++- .../openlp_core/ui/media/test_vlcplayer.py | 15 +- .../openlp_core/ui/test_slidecontroller.py | 128 +++++++- .../openlp_core/ui/test_starttimedialog.py | 10 +- 9 files changed, 473 insertions(+), 168 deletions(-) diff --git a/openlp/core/display/html/display.js b/openlp/core/display/html/display.js index 639308c67..0a80a93e0 100644 --- a/openlp/core/display/html/display.js +++ b/openlp/core/display/html/display.js @@ -1016,9 +1016,6 @@ var Display = { targetElement.style.cssText = ""; targetElement.setAttribute("data-background", backgroundContent); targetElement.setAttribute("data-background-size", "cover"); - if (!!backgroundHtml) { - background.innerHTML = backgroundHtml; - } // set up the main area if (!is_text) { diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 245b2d752..ca04ac55f 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -41,8 +41,7 @@ from openlp.core.common.registry import Registry, RegistryBase from openlp.core.lib.serviceitem import ItemCapabilities from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui import DisplayControllerType, HideMode -from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, parse_stream_path, \ - VIDEO_EXT, AUDIO_EXT +from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, parse_stream_path from openlp.core.ui.media.remote import register_views from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc @@ -50,6 +49,7 @@ from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc log = logging.getLogger(__name__) TICK_TIME = 200 +HIDE_DELAY_TIME = 2500 class MediaController(RegistryBase, LogMixin, RegistryProperties): @@ -68,10 +68,16 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): # Timer for video state self.live_timer = QtCore.QTimer() self.live_timer.setInterval(TICK_TIME) + self.live_hide_timer = QtCore.QTimer() + self.live_hide_timer.setSingleShot(True) + self.live_kill_timer = QtCore.QTimer() + self.live_kill_timer.setSingleShot(True) self.preview_timer = QtCore.QTimer() self.preview_timer.setInterval(TICK_TIME) # Signals self.live_timer.timeout.connect(self._media_state_live) + self.live_hide_timer.timeout.connect(self._on_media_hide_live) + self.live_kill_timer.timeout.connect(self._on_media_kill_live) self.preview_timer.timeout.connect(self._media_state_preview) Registry().register_function('playbackPlay', self.media_play_msg) Registry().register_function('playbackPause', self.media_pause_msg) @@ -154,31 +160,29 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): """ Check if there is a running Live media Player and do updating stuff (e.g. update the UI) """ - display = self._define_display(self._display_controllers(DisplayControllerType.Live)) if DisplayControllerType.Live in self.current_media_players: - self.current_media_players[DisplayControllerType.Live].resize(self.live_controller) - self.current_media_players[DisplayControllerType.Live].update_ui(self.live_controller, display) - self.tick(self._display_controllers(DisplayControllerType.Live)) - if self.current_media_players[DisplayControllerType.Live].get_live_state() is not MediaState.Playing: + media_player = self.current_media_players[DisplayControllerType.Live] + media_player.resize(self.live_controller) + media_player.update_ui(self.live_controller, self._define_display(self.live_controller)) + if not self.tick(self.live_controller): self.live_timer.stop() else: self.live_timer.stop() - self.media_stop(self._display_controllers(DisplayControllerType.Live)) + self.media_stop(self.live_controller) def _media_state_preview(self): """ Check if there is a running Preview media Player and do updating stuff (e.g. update the UI) """ - display = self._define_display(self._display_controllers(DisplayControllerType.Preview)) if DisplayControllerType.Preview in self.current_media_players: - self.current_media_players[DisplayControllerType.Preview].resize(self.live_controller) - self.current_media_players[DisplayControllerType.Preview].update_ui(self.preview_controller, display) - self.tick(self._display_controllers(DisplayControllerType.Preview)) - if self.current_media_players[DisplayControllerType.Preview].get_preview_state() is not MediaState.Playing: + media_player = self.current_media_players[DisplayControllerType.Preview] + media_player.resize(self.preview_controller) + media_player.update_ui(self.preview_controller, self._define_display(self.preview_controller)) + if not self.tick(self.preview_controller): self.preview_timer.stop() else: self.preview_timer.stop() - self.media_stop(self._display_controllers(DisplayControllerType.Preview)) + self.media_stop(self.preview_controller) def setup_display(self, controller, preview): """ @@ -224,9 +228,11 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): """ is_valid = True controller = self._display_controllers(source) + log.debug(f'load_video is_live:{controller.is_live}') # stop running videos self.media_reset(controller) controller.media_info = ItemMediaInfo() + controller.media_info.media_type = MediaType.Video if controller.is_live: controller.media_info.volume = self.settings.value('media/live volume') else: @@ -234,6 +240,9 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): # background will always loop video. if service_item.is_capable(ItemCapabilities.HasBackgroundAudio): controller.media_info.file_info = service_item.background_audio + controller.media_info.media_type = MediaType.Audio + # is_background indicates we shouldn't override the normal display + controller.media_info.is_background = True else: if service_item.is_capable(ItemCapabilities.HasBackgroundStream): (name, mrl, options) = parse_stream_path(service_item.stream_mrl) @@ -268,6 +277,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): controller.media_info.length = service_item.media_length is_valid = self._check_file_type(controller, display) controller.media_info.start_time = service_item.start_time + controller.media_info.timer = service_item.start_time controller.media_info.end_time = service_item.end_time elif controller.preview_display: if service_item.is_capable(ItemCapabilities.IsOptical): @@ -292,11 +302,21 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): translate('MediaPlugin.MediaItem', 'Unsupported File')) return False self.log_debug('video media type: {tpe} '.format(tpe=str(controller.media_info.media_type))) + # If both the preview and live view have a stream, make sure only the live view continues streaming + if controller.media_info.media_type == MediaType.Stream: + if controller.is_live: + if self.preview_controller.media_info.media_type == MediaType.Stream: + self.log_warning('stream can only be displayed in one instance, killing preview stream') + self.preview_controller.on_media_close() + else: + if self.live_controller.media_info.media_type == MediaType.Stream: + self.log_warning('stream cannot be previewed while also streaming live') + return autoplay = False - if service_item.requires_media(): + if service_item.requires_media() and hidden == HideMode.Theme: autoplay = True # Preview requested - if not controller.is_live: + elif not controller.is_live: autoplay = True # Visible or background requested or Service Item wants to autostart elif not hidden or service_item.will_auto_start: @@ -355,6 +375,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): else: controller.media_info.media_type = MediaType.DVD controller.media_info.start_time = start + controller.media_info.timer = start controller.media_info.end_time = end controller.media_info.length = (end - start) controller.media_info.title_track = title @@ -386,38 +407,23 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): return True return False for file in controller.media_info.file_info: - if file.is_file: - suffix = '*%s' % file.suffix.lower() - file = str(file) - if suffix in VIDEO_EXT: - self.resize(controller, self.vlc_player) - if self.vlc_player.load(controller, display, file): - self.current_media_players[controller.controller_type] = self.vlc_player - controller.media_info.media_type = MediaType.Video - return True - if suffix in AUDIO_EXT: - if self.vlc_player.load(controller, display, file): - self.current_media_players[controller.controller_type] = self.vlc_player - controller.media_info.media_type = MediaType.Audio - return True - else: - file = str(file) - if self.vlc_player.can_folder: - self.resize(controller, self.vlc_player) - if self.vlc_player.load(controller, display, file): - self.current_media_players[controller.controller_type] = self.vlc_player - controller.media_info.media_type = MediaType.Video - return True + if not file.is_file and not self.vlc_player.can_folder: + return False + file = str(file) + self.resize(controller, self.vlc_player) + if self.vlc_player.load(controller, display, file): + self.current_media_players[controller.controller_type] = self.vlc_player + return True return False - def media_play_msg(self, msg, status=True): + def media_play_msg(self, msg): """ Responds to the request to play a loaded video :param msg: First element is the controller which should be used :param status: """ - return self.media_play(msg[0], status) + return self.media_play(msg[0]) def on_media_play(self): """ @@ -425,14 +431,13 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): """ return self.media_play(self.live_controller) - def media_play(self, controller, first_time=True): + def media_play(self, controller): """ Responds to the request to play a loaded video :param controller: The controller to be played - :param first_time: """ - self.log_debug(f"media_play with {first_time}") + self.log_debug(f'media_play is_live:{controller.is_live}') controller.seek_slider.blockSignals(True) controller.volume_slider.blockSignals(True) display = self._define_display(controller) @@ -441,25 +446,29 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): controller.volume_slider.blockSignals(False) return False self.media_volume(controller, controller.media_info.volume) - if first_time: - self.current_media_players[controller.controller_type].set_visible(controller, True) - controller.mediabar.actions['playbackPlay'].setVisible(False) - controller.mediabar.actions['playbackPause'].setVisible(True) - controller.mediabar.actions['playbackStop'].setDisabled(False) + self._media_set_visibility(controller, True) + controller.mediabar.actions['playbackPlay'].setVisible(False) + controller.mediabar.actions['playbackPause'].setVisible(True) + controller.mediabar.actions['playbackStop'].setDisabled(False) + controller.mediabar.actions['playbackLoop'].setChecked(controller.media_info.is_looping_playback) + controller.mediabar.actions['playbackStop'].setVisible(not controller.media_info.is_background or + controller.media_info.media_type is MediaType.Audio) + controller.mediabar.actions['playbackLoop'].setVisible((not controller.media_info.is_background and + controller.media_info.media_type is not MediaType.Stream) + or controller.media_info.media_type is MediaType.Audio) + # Start Timer for ui updates if controller.is_live: - if controller.hide_menu.defaultAction().isChecked(): - controller.hide_menu.defaultAction().trigger() - # Start Timer for ui updates if not self.live_timer.isActive(): self.live_timer.start() else: - # Start Timer for ui updates if not self.preview_timer.isActive(): self.preview_timer.start() controller.seek_slider.blockSignals(False) controller.volume_slider.blockSignals(False) controller.media_info.is_playing = True if not controller.media_info.is_background: + if controller.is_live: + controller.set_hide_mode(None) display = self._define_display(controller) display.hide_display(HideMode.Screen) controller._set_theme(controller.service_item) @@ -472,30 +481,35 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): Add a tick while the media is playing but only count if not paused :param controller: The Controller to be processed + :return: Is the video still running? """ start_again = False + stopped = False if controller.media_info.is_playing and controller.media_info.length > 0: - if controller.media_info.timer > controller.media_info.length: + if controller.media_info.timer + TICK_TIME >= controller.media_info.length: if controller.media_info.is_looping_playback: start_again = True else: self.media_stop(controller) - elif controller.media_info.timer > controller.media_info.length - TICK_TIME * 4: - if not controller.media_info.is_looping_playback: - display = self._define_display(controller) - display.show_display() + stopped = True controller.media_info.timer += TICK_TIME - seconds = controller.media_info.timer // 1000 - minutes = seconds // 60 - seconds %= 60 - total_seconds = controller.media_info.length // 1000 - total_minutes = total_seconds // 60 - total_seconds %= 60 - controller.position_label.setText(' %02d:%02d / %02d:%02d' % - (minutes, seconds, total_minutes, total_seconds)) + self._update_seek_ui(controller) + else: + stopped = True + if start_again: controller.seek_slider.setSliderPosition(0) - self.media_play(controller, False) + return not stopped + + def _update_seek_ui(self, controller): + seconds = controller.media_info.timer // 1000 + minutes = seconds // 60 + seconds %= 60 + total_seconds = controller.media_info.length // 1000 + total_minutes = total_seconds // 60 + total_seconds %= 60 + controller.position_label.setText(' %02d:%02d / %02d:%02d' % + (minutes, seconds, total_minutes, total_seconds)) def media_pause_msg(self, msg): """ @@ -517,12 +531,16 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): :param controller: The Controller to be paused """ + self.log_debug(f'media_stop is_live:{controller.is_live}') if controller.controller_type in self.current_media_players: self.current_media_players[controller.controller_type].pause(controller) controller.mediabar.actions['playbackPlay'].setVisible(True) controller.mediabar.actions['playbackStop'].setDisabled(False) controller.mediabar.actions['playbackPause'].setVisible(False) controller.media_info.is_playing = False + # Add a tick to the timer to prevent it finishing the video before it can loop back or stop + # If the clip finishes, we hit a bug where we cannot start the video + controller.media_info.timer += TICK_TIME controller.output_has_changed() return True return False @@ -565,9 +583,19 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): :param controller: The controller that needs to be stopped """ + self.log_debug(f'media_stop is_live:{controller.is_live}') if controller.controller_type in self.current_media_players: self.current_media_players[controller.controller_type].stop(controller) - self.current_media_players[controller.controller_type].set_visible(controller, False) + if controller.is_live: + self.live_hide_timer.start(HIDE_DELAY_TIME) + if not controller.media_info.is_background: + display = self._define_display(controller) + if display.hide_mode == HideMode.Screen: + controller.set_hide_mode(HideMode.Blank) + else: + controller.set_hide_mode(display.hide_mode or HideMode.Blank) + else: + self._media_set_visibility(controller, False) controller.seek_slider.setSliderPosition(0) total_seconds = controller.media_info.length // 1000 total_minutes = total_seconds // 60 @@ -577,10 +605,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): controller.mediabar.actions['playbackStop'].setDisabled(True) controller.mediabar.actions['playbackPause'].setVisible(False) controller.media_info.is_playing = False - controller.media_info.timer = 1000 - controller.media_timer = 0 - display = self._define_display(controller) - display.show_display() + controller.media_info.timer = 0 controller.output_has_changed() return True return False @@ -631,33 +656,76 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): """ self.current_media_players[controller.controller_type].seek(controller, seek_value) controller.media_info.timer = seek_value + self._update_seek_ui(controller) - def media_reset(self, controller): + def media_reset(self, controller, delayed=False): """ Responds to the request to reset a loaded video :param controller: The controller to use. + :param delayed: Should the controller briefly remain visible. """ + self.log_debug('media_reset') self.set_controls_visible(controller, False) if controller.controller_type in self.current_media_players: display = self._define_display(controller) - display.show_display() + hide_mode = controller.get_hide_mode() + if hide_mode is None: + display.show_display() + else: + display.hide_display(hide_mode) self.current_media_players[controller.controller_type].reset(controller) - self.current_media_players[controller.controller_type].set_visible(controller, False) - del self.current_media_players[controller.controller_type] + if controller.is_live and delayed: + self.live_kill_timer.start(HIDE_DELAY_TIME) + else: + if controller.is_live: + self.live_kill_timer.stop() + else: + self._media_set_visibility(controller, False) + del self.current_media_players[controller.controller_type] - def media_hide(self, msg): + def media_hide_msg(self, msg): """ Hide the related video Widget :param msg: First element is the boolean for Live indication """ is_live = msg[1] - if not is_live: + self.media_hide(is_live) + + def media_hide(self, is_live, delayed=False): + """ + Pause and hide the related video Widget if is_live + + :param is_live: Live indication + :param delayed: Should the controller briefly remain visible. + """ + self.log_debug(f'media_hide is_live:{is_live}') + if not is_live or self.live_kill_timer.isActive(): return if self.live_controller.controller_type in self.current_media_players and \ self.current_media_players[self.live_controller.controller_type].get_live_state() == MediaState.Playing: - self.media_pause(self.live_controller) - self.current_media_players[self.live_controller.controller_type].set_visible(self.live_controller, False) + if delayed: + self.live_hide_timer.start(HIDE_DELAY_TIME) + else: + self.media_pause(self.live_controller) + self._media_set_visibility(self.live_controller, False) + + def _on_media_hide_live(self): + self.media_pause(self.live_controller) + self._media_set_visibility(self.live_controller, False) + + def _on_media_kill_live(self): + self._media_set_visibility(self.live_controller, False) + del self.current_media_players[self.live_controller.controller_type] + + def _media_set_visibility(self, controller, visible): + """ + Set the live video Widget visibility + """ + if controller.is_live: + self.live_hide_timer.stop() + visible = visible and controller.media_info.media_type is not MediaType.Audio + self.current_media_players[controller.controller_type].set_visible(controller, visible) def media_blank(self, msg): """ @@ -668,13 +736,30 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): """ is_live = msg[1] hide_mode = msg[2] - if not is_live: + self.log_debug(f'media_blank is_live:{is_live}') + if not is_live or self.live_controller.controller_type not in self.current_media_players: return - Registry().execute('live_display_hide', hide_mode) - if self.live_controller.controller_type in self.current_media_players and \ - self.current_media_players[self.live_controller.controller_type].get_live_state() == MediaState.Playing: - self.media_pause(self.live_controller) - self.current_media_players[self.live_controller.controller_type].set_visible(self.live_controller, False) + if self.live_kill_timer.isActive(): + # If pressing blank when the video is being removed, remove it instantly + self._media_set_visibility(self.live_controller, False) + self.media_reset(self.live_controller) + return + if not self.live_controller.media_info.is_background: + Registry().execute('live_display_hide', hide_mode) + controller_type = self.live_controller.controller_type + playing = self.current_media_players[controller_type].get_live_state() == MediaState.Playing + if hide_mode == HideMode.Theme: + if not playing: + self.media_play(self.live_controller) + else: + self.live_hide_timer.stop() + else: + if hide_mode == HideMode.Screen: + if playing: + self.media_pause(self.live_controller) + self._media_set_visibility(self.live_controller, False) + else: + self.live_hide_timer.start(HIDE_DELAY_TIME) def media_unblank(self, msg): """ @@ -683,24 +768,28 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): :param msg: First element is not relevant in this context Second element is the boolean for Live indication """ - Registry().execute('live_display_show') is_live = msg[1] - if not is_live: + self.log_debug(f'media_blank is_live:{is_live}') + if not is_live or self.live_kill_timer.isActive(): return - if self.live_controller.controller_type in self.current_media_players and \ - self.current_media_players[self.live_controller.controller_type].get_live_state() != \ - MediaState.Playing: - if self.media_play(self.live_controller): - self.current_media_players[self.live_controller.controller_type].set_visible(self.live_controller, True) - # Start Timer for ui updates - if not self.live_timer.isActive(): - self.live_timer.start() + Registry().execute('live_display_show') + if self.live_controller.controller_type in self.current_media_players: + if self.current_media_players[self.live_controller.controller_type].get_live_state() != \ + MediaState.Playing: + self.media_play(self.live_controller) + else: + self._media_set_visibility(self.live_controller, True) + if not self.live_controller.media_info.is_background: + display = self._define_display(self.live_controller) + display.hide_display(HideMode.Screen) def finalise(self): """ Reset all the media controllers when OpenLP shuts down """ self.live_timer.stop() + self.live_hide_timer.stop() + self.live_kill_timer.stop() self.preview_timer.stop() self.media_reset(self._display_controllers(DisplayControllerType.Live)) self.media_reset(self._display_controllers(DisplayControllerType.Preview)) diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 7f47f2dd7..ce419947a 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -134,7 +134,6 @@ class VlcPlayer(MediaPlayer): # creating an empty vlc media player controller.vlc_media_player = controller.vlc_instance.media_player_new() controller.vlc_widget.resize(controller.size()) - controller.vlc_widget.raise_() controller.vlc_widget.hide() # The media player has to be 'connected' to the QFrame. # (otherwise a video would be displayed in it's own window) @@ -245,22 +244,22 @@ class VlcPlayer(MediaPlayer): start_time = 0 log.debug('vlc play') if controller.is_live: - if self.get_live_state() != MediaState.Paused and controller.media_info.start_time > 0: - start_time = controller.media_info.start_time + if self.get_live_state() != MediaState.Paused and controller.media_info.timer > 0: + start_time = controller.media_info.timer else: - if self.get_preview_state() != MediaState.Paused and controller.media_info.start_time > 0: - start_time = controller.media_info.start_time + if self.get_preview_state() != MediaState.Paused and controller.media_info.timer > 0: + start_time = controller.media_info.timer threading.Thread(target=controller.vlc_media_player.play).start() if not self.media_state_wait(controller, vlc.State.Playing): return False if controller.is_live: - if self.get_live_state() != MediaState.Paused and controller.media_info.start_time > 0: + if self.get_live_state() != MediaState.Paused and controller.media_info.timer > 0: log.debug('vlc play, start time set') - start_time = controller.media_info.start_time + start_time = controller.media_info.timer else: - if self.get_preview_state() != MediaState.Paused and controller.media_info.start_time > 0: + if self.get_preview_state() != MediaState.Paused and controller.media_info.timer > 0: log.debug('vlc play, start time set') - start_time = controller.media_info.start_time + start_time = controller.media_info.timer log.debug('mediatype: ' + str(controller.media_info.media_type)) # Set tracks for the optical device if controller.media_info.media_type == MediaType.DVD and \ @@ -278,10 +277,10 @@ class VlcPlayer(MediaPlayer): if controller.media_info.subtitle_track > 0: controller.vlc_media_player.video_set_spu(controller.media_info.subtitle_track) log.debug('vlc play, subtitle_track set: ' + str(controller.media_info.subtitle_track)) - if controller.media_info.start_time > 0: - log.debug('vlc play, starttime set: ' + str(controller.media_info.start_time)) - start_time = controller.media_info.start_time - controller.media_info.length = controller.media_info.end_time - controller.media_info.start_time + if controller.media_info.timer > 0: + log.debug('vlc play, starttime set: ' + str(controller.media_info.timer)) + start_time = controller.media_info.timer + controller.media_info.length = controller.media_info.end_time - controller.media_info.timer self.volume(controller, controller.media_info.volume) if start_time > 0 and controller.vlc_media_player.is_seekable(): controller.vlc_media_player.set_time(int(start_time)) @@ -343,7 +342,6 @@ class VlcPlayer(MediaPlayer): :param controller: The controller where the media is """ controller.vlc_media_player.stop() - controller.vlc_widget.setVisible(False) self.set_state(MediaState.Off, controller) def set_visible(self, controller, status): @@ -362,14 +360,6 @@ class VlcPlayer(MediaPlayer): :param controller: Which Controller is running the show. :param output_display: The display where the media is """ - vlc = get_vlc() - # Stop video if playback is finished. - if controller.vlc_media.get_state() == vlc.State.Ended: - self.stop(controller) - if controller.media_info.end_time > 0: - if controller.vlc_media_player.get_time() > controller.media_info.end_time: - self.stop(controller) - self.set_visible(controller, False) if not controller.seek_slider.isSliderDown(): controller.seek_slider.blockSignals(True) if controller.media_info.media_type == MediaType.CD \ diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 93a239222..6fdced0b1 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -525,6 +525,10 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): self.song_menu.setPopupMode(QtWidgets.QToolButton.InstantPopup) self.song_menu.setMenu(QtWidgets.QMenu(translate('OpenLP.SlideController', 'Go To'), self.toolbar)) + def _raise_displays(self): + for display in self.displays: + display.raise_() + def _slide_shortcut_activated(self): """ Called, when a shortcut has been activated to jump to a chorus, verse, etc. @@ -946,8 +950,12 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): self._reset_blank(self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay)) self.info_label.setText(self.service_item.title) self.slide_list = {} - if old_item and old_item.requires_media(): - self.on_media_close() + if old_item: + # Close the old item if it's not to be used by the new sevice item + if not self.service_item.is_media() and not self.service_item.requires_media(): + self.on_media_close() + if old_item.is_command() and not old_item.is_media(): + Registry().execute('{name}_stop'.format(name=old_item.name.lower()), [old_item, self.is_live]) row = 0 width = self.main_window.control_splitter.sizes()[self.split] if self.service_item.is_text(): @@ -989,26 +997,15 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): if self.service_item.is_command(): self.preview_display.load_verses(media_empty_song, True) self.on_media_start(self.service_item) - # Let media window init, then put webengine back on top - self.application.process_events() - for display in self.displays: - display.raise_() + # Try to get display back on top of media window asap. If the media window + # is not loaded by the time _raise_displays is run, lyrics (web display) + # will be under the media window (not good). + QtCore.QTimer.singleShot(100, self._raise_displays) + QtCore.QTimer.singleShot(500, self._raise_displays) + QtCore.QTimer.singleShot(1000, self._raise_displays) self.slide_selected(True) if self.service_item.from_service: self.preview_widget.setFocus() - if old_item: - # Close the old item after the new one is opened - # This avoids the service theme/desktop flashing on screen - # However opening a new item of the same type will automatically - # close the previous, so make sure we don't close the new one. - if old_item.is_command() and not self.service_item.is_command() or \ - old_item.is_command() and not old_item.is_media() and self.service_item.is_media(): - if old_item.is_media(): - self.on_media_close() - else: - Registry().execute('{name}_stop'.format(name=old_item.name.lower()), [old_item, self.is_live]) - if old_item.is_media() and not self.service_item.is_media(): - self.on_media_close() if self.is_live: Registry().execute('slidecontroller_{item}_started'.format(item=self.type_prefix), [self.service_item]) # Need to process events four times to get correct controller width @@ -1528,7 +1525,9 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): :param item: The service item to be processed """ if State().check_preconditions('media'): - if self.is_live and self.get_hide_mode() == HideMode.Theme: + if self.is_live and not item.is_media() and item.requires_media(): + self.media_controller.load_video(self.controller_type, item, self.get_hide_mode()) + elif self.is_live and self.get_hide_mode() == HideMode.Theme: self.media_controller.load_video(self.controller_type, item, HideMode.Blank) self.set_hide_mode(HideMode.Blank) elif self.is_live or item.is_media(): @@ -1542,7 +1541,7 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): Respond to a request to close the Video if we have media """ if State().check_preconditions('media'): - self.media_controller.media_reset(self) + self.media_controller.media_reset(self, delayed=True) def _reset_blank(self, no_theme): """ diff --git a/openlp/core/ui/starttimeform.py b/openlp/core/ui/starttimeform.py index ec9157e94..f1ee883c9 100644 --- a/openlp/core/ui/starttimeform.py +++ b/openlp/core/ui/starttimeform.py @@ -68,7 +68,7 @@ class StartTimeForm(QtWidgets.QDialog, Ui_StartTimeDialog, RegistryProperties): start = self.hour_spin_box.value() * 3600 + self.minute_spin_box.value() * 60 + self.second_spin_box.value() end = self.hour_finish_spin_box.value() * 3600 + \ self.minute_finish_spin_box.value() * 60 + self.second_finish_spin_box.value() - if end > self.item['service_item'].media_length: + if end > self.item['service_item'].media_length // 1000: critical_error_message_box(title=translate('OpenLP.StartTime_form', 'Time Validation Error'), message=translate('OpenLP.StartTime_form', 'Finish time is set after the end of the media item')) @@ -78,14 +78,15 @@ class StartTimeForm(QtWidgets.QDialog, Ui_StartTimeDialog, RegistryProperties): message=translate('OpenLP.StartTime_form', 'Start time is after the finish time of the media item')) return - self.item['service_item'].start_time = start - self.item['service_item'].end_time = end + self.item['service_item'].start_time = start * 1000 + self.item['service_item'].end_time = end * 1000 return QtWidgets.QDialog.accept(self) - def _time_split(self, seconds): + def _time_split(self, milliseconds): """ - Split time up into hours minutes and seconds from seconds + Split time up into hours minutes and seconds from milliseconds """ + seconds = milliseconds // 1000 hours = seconds // 3600 seconds -= 3600 * hours minutes = seconds // 60 diff --git a/tests/functional/openlp_core/ui/media/test_mediacontroller.py b/tests/functional/openlp_core/ui/media/test_mediacontroller.py index 6955bf8a2..0a24ffbef 100644 --- a/tests/functional/openlp_core/ui/media/test_mediacontroller.py +++ b/tests/functional/openlp_core/ui/media/test_mediacontroller.py @@ -26,9 +26,9 @@ import pytest from unittest.mock import MagicMock, patch from openlp.core.common.registry import Registry -from openlp.core.ui import DisplayControllerType +from openlp.core.ui import DisplayControllerType, HideMode from openlp.core.ui.media.mediacontroller import MediaController -from openlp.core.ui.media import ItemMediaInfo +from openlp.core.ui.media import ItemMediaInfo, MediaState from tests.utils.constants import RESOURCE_PATH @@ -60,6 +60,36 @@ def test_resize(media_env): mocked_player.resize.assert_called_with(mocked_display) +def test_load_video(media_env, settings): + """ + Test that the load_video runs correctly + """ + # GIVEN: A media controller and a service item + mocked_slide_controller = MagicMock() + mocked_service_item = MagicMock() + mocked_service_item.is_capable.return_value = False + settings.setValue('media/live volume', 1) + media_env.media_controller.current_media_players = MagicMock() + media_env.media_controller._check_file_type = MagicMock(return_value=True) + media_env.media_controller._display_controllers = MagicMock(return_value=mocked_slide_controller) + media_env.media_controller._define_display = MagicMock() + media_env.media_controller.media_reset = MagicMock() + media_env.media_controller.media_play = MagicMock() + media_env.media_controller.set_controls_visible = MagicMock() + + # WHEN: load_video() is called + media_env.media_controller.load_video(DisplayControllerType.Live, mocked_service_item) + + # THEN: The current controller's media should be reset + # The volume should be set from the settings + # The video should have autoplayed + # The controls should have been made visible + media_env.media_controller.media_reset.assert_called_once_with(mocked_slide_controller) + assert mocked_slide_controller.media_info.volume == 1 + media_env.media_controller.media_play.assert_called_once_with(mocked_slide_controller) + media_env.media_controller.set_controls_visible.assert_called_once_with(mocked_slide_controller, True) + + def test_check_file_type_null(media_env): """ Test that we don't try to play media when no players available @@ -125,10 +155,10 @@ def test_media_play_msg(media_env): # WHEN: media_play_msg() is called with patch.object(media_env.media_controller, u'media_play') as mocked_media_play: - media_env.media_controller.media_play_msg(message, False) + media_env.media_controller.media_play_msg(message) # THEN: The underlying method is called - mocked_media_play.assert_called_with(1, False) + mocked_media_play.assert_called_with(1) def test_media_pause_msg(media_env): @@ -161,6 +191,33 @@ def test_media_stop_msg(media_env): mocked_media_stop.assert_called_with(1) +def test_media_stop(media_env): + """ + Test that the media controller takes the correct actions when stopping media + """ + # GIVEN: A live media controller and a message with two elements + mocked_slide_controller = MagicMock() + mocked_media_player = MagicMock() + mocked_display = MagicMock(hide_mode=None) + mocked_slide_controller.controller_type = 'media player' + mocked_slide_controller.media_info = MagicMock(is_background=False) + mocked_slide_controller.set_hide_mode = MagicMock() + mocked_slide_controller.is_live = True + media_env.media_controller.current_media_players = {'media player': mocked_media_player} + media_env.media_controller.live_hide_timer = MagicMock() + media_env.media_controller._define_display = MagicMock(return_value=mocked_display) + + # WHEN: media_stop() is called + result = media_env.media_controller.media_stop(mocked_slide_controller) + + # THEN: Result should be successful, media player should be stopped and the hide timer should have started + # The controller's hide mode should be set to Blank + assert result is True + mocked_media_player.stop.assert_called_once_with(mocked_slide_controller) + media_env.media_controller.live_hide_timer.start.assert_called_once() + mocked_slide_controller.set_hide_mode.assert_called_once_with(HideMode.Blank) + + def test_media_volume_msg(media_env): """ Test that the media controller responds to the request to change the volume @@ -191,6 +248,59 @@ def test_media_seek_msg(media_env): mocked_media_seek.assert_called_with(1, 800) +def test_media_reset(media_env): + """ + Test that the media controller conducts the correct actions when resetting + """ + # GIVEN: A media controller, mocked slide controller, mocked media player and mocked display + mocked_slide_controller = MagicMock() + mocked_media_player = MagicMock() + mocked_display = MagicMock(hide_mode=None) + mocked_slide_controller.controller_type = 'media player' + mocked_slide_controller.media_info = MagicMock(is_background=False) + mocked_slide_controller.get_hide_mode = MagicMock(return_value=None) + mocked_slide_controller.is_live = False + media_env.media_controller.current_media_players = {'media player': mocked_media_player} + media_env.media_controller.live_hide_timer = MagicMock() + media_env.media_controller._define_display = MagicMock(return_value=mocked_display) + media_env.media_controller._media_set_visibility = MagicMock() + + # WHEN: media_reset() is called + media_env.media_controller.media_reset(mocked_slide_controller) + + # THEN: The display should be shown, media should be hidden and removed + mocked_display.show_display.assert_called_once_with() + media_env.media_controller._media_set_visibility.assert_called_once_with(mocked_slide_controller, False) + assert 'media player' not in media_env.media_controller.current_media_players + + +def test_media_hide(media_env, registry): + """ + Test that the media controller conducts the correct actions when hiding + """ + # GIVEN: A media controller, mocked slide controller, mocked media player and mocked display + mocked_slide_controller = MagicMock() + mocked_media_player = MagicMock() + mocked_media_player.get_live_state.return_value = MediaState.Playing + mocked_slide_controller.controller_type = 'media player' + mocked_slide_controller.media_info = MagicMock(is_background=False) + mocked_slide_controller.get_hide_mode = MagicMock(return_value=None) + mocked_slide_controller.is_live = False + Registry().register('live_controller', mocked_slide_controller) + media_env.media_controller.current_media_players = {'media player': mocked_media_player} + media_env.media_controller.live_kill_timer = MagicMock(isActive=MagicMock(return_value=False)) + media_env.media_controller._media_set_visibility = MagicMock() + media_env.media_controller.media_pause = MagicMock() + + # WHEN: media_hide() is called + media_env.media_controller.media_hide(is_live=True) + + # THEN: media should be paused and hidden, but the player should still exist + media_env.media_controller.media_pause.assert_called_once_with(mocked_slide_controller) + media_env.media_controller._media_set_visibility.assert_called_once_with(mocked_slide_controller, False) + assert 'media player' in media_env.media_controller.current_media_players + + def test_media_length(media_env): """ Test the Media Info basic functionality @@ -334,6 +444,7 @@ def test_media_play(media_env): media_env.current_media_players = MagicMock() Registry().register('settings', MagicMock()) media_env.live_timer = MagicMock() + media_env.live_hide_timer = MagicMock() mocked_controller = MagicMock() mocked_controller.media_info.is_background = False @@ -343,4 +454,5 @@ def test_media_play(media_env): # THEN: The web display should become transparent (only tests that the theme is reset here) # And the function should return true to indicate success assert result is True + media_env.live_hide_timer.stop.assert_called_once_with() mocked_controller._set_theme.assert_called_once() diff --git a/tests/functional/openlp_core/ui/media/test_vlcplayer.py b/tests/functional/openlp_core/ui/media/test_vlcplayer.py index f11c67d47..03392c78d 100644 --- a/tests/functional/openlp_core/ui/media/test_vlcplayer.py +++ b/tests/functional/openlp_core/ui/media/test_vlcplayer.py @@ -122,7 +122,6 @@ def test_setup(MockedQtWidgets, mocked_get_vlc, mocked_is_macosx, mocked_is_win, assert mocked_output_display.vlc_media_player == mocked_media_player_new mocked_output_display.size.assert_called_with() mocked_qframe.resize.assert_called_with((10, 10)) - mocked_qframe.raise_.assert_called_with() mocked_qframe.hide.assert_called_with() mocked_media_player_new.set_xwindow.assert_called_with(2) assert vlc_player.has_own_widget is True @@ -848,7 +847,7 @@ def test_reset(): # THEN: The media should be stopped and invisible mocked_display.vlc_media_player.stop.assert_called_with() - mocked_display.vlc_widget.setVisible.assert_called_with(False) + mocked_display.vlc_widget.setVisible.assert_not_called() assert MediaState.Off == vlc_player.get_live_state() @@ -886,15 +885,10 @@ def test_update_ui(mocked_get_vlc): vlc_player = VlcPlayer(None) # WHEN: update_ui() is called - with patch.object(vlc_player, 'stop') as mocked_stop, \ - patch.object(vlc_player, 'set_visible') as mocked_set_visible: - vlc_player.update_ui(mocked_controller, mocked_display) + vlc_player.update_ui(mocked_controller, mocked_display) # THEN: Certain methods should be called - mocked_stop.assert_called_with(mocked_controller) - assert 2 == mocked_stop.call_count mocked_controller.vlc_media_player.get_time.assert_called_with() - mocked_set_visible.assert_called_with(mocked_controller, False) mocked_controller.seek_slider.setSliderPosition.assert_called_with(400000) expected_calls = [call(True), call(False)] assert expected_calls == mocked_controller.seek_slider.blockSignals.call_args_list @@ -921,12 +915,9 @@ def test_update_ui_dvd(mocked_get_vlc): vlc_player = VlcPlayer(None) # WHEN: update_ui() is called - with patch.object(vlc_player, 'stop') as mocked_stop: - vlc_player.update_ui(mocked_controller, mocked_display) + vlc_player.update_ui(mocked_controller, mocked_display) # THEN: Certain methods should be called - mocked_stop.assert_called_with(mocked_controller) - assert 1 == mocked_stop.call_count mocked_controller.vlc_media_player.get_time.assert_called_with() mocked_controller.seek_slider.setSliderPosition.assert_called_with(200) expected_calls = [call(True), call(False)] diff --git a/tests/functional/openlp_core/ui/test_slidecontroller.py b/tests/functional/openlp_core/ui/test_slidecontroller.py index 7a35ee61c..22dbdbf8c 100644 --- a/tests/functional/openlp_core/ui/test_slidecontroller.py +++ b/tests/functional/openlp_core/ui/test_slidecontroller.py @@ -899,11 +899,137 @@ def test_process_item(mocked_execute, registry, state_media): slide_controller._process_item(mocked_media_item, 0) # THEN: Registry.execute should have been called to stop the presentation - assert 1 == mocked_execute.call_count, 'Execute should have been called 2 times' + assert 1 == mocked_execute.call_count, 'Execute should have been called once' assert 'mocked_presentation_item_stop' == mocked_execute.call_args_list[0][0][0], \ 'The presentation should have been stopped.' +@patch.object(Registry, 'execute') +def test_process_item_transition(mocked_execute, registry, state_media): + """ + Test that the correct actions are taken when a media service-item is closed followed by a image service-item + """ + # GIVEN: A mocked presentation service item, a mocked media service item, a mocked Registry.execute + # and a slide controller with many mocks. + # and the setting 'themes/item transitions' = True + mocked_pres_item = MagicMock() + mocked_pres_item.name = 'mocked_image_item' + mocked_pres_item.is_command.return_value = True + mocked_pres_item.is_media.return_value = True + mocked_pres_item.is_image.return_value = False + mocked_pres_item.from_service = False + mocked_pres_item.get_frames.return_value = [] + mocked_media_item = MagicMock() + mocked_media_item.name = 'mocked_media_item' + mocked_media_item.get_transition_delay.return_value = 0 + mocked_media_item.is_text.return_value = False + mocked_media_item.is_command.return_value = False + mocked_media_item.is_media.return_value = False + mocked_media_item.requires_media.return_value = False + mocked_media_item.is_image.return_value = True + mocked_media_item.from_service = False + mocked_media_item.get_frames.return_value = [] + mocked_settings = MagicMock() + mocked_settings.value.return_value = True + mocked_main_window = MagicMock() + Registry().register('main_window', mocked_main_window) + Registry().register('media_controller', MagicMock()) + Registry().register('application', MagicMock()) + Registry().register('settings', mocked_settings) + slide_controller = SlideController(None) + slide_controller.service_item = mocked_pres_item + slide_controller.is_live = True + slide_controller._reset_blank = MagicMock() + slide_controller.preview_widget = MagicMock() + slide_controller.preview_display = MagicMock() + slide_controller.enable_tool_bar = MagicMock() + slide_controller.on_controller_size_changed = MagicMock() + slide_controller.on_media_start = MagicMock() + slide_controller.on_media_close = MagicMock() + slide_controller.slide_selected = MagicMock() + slide_controller.new_song_menu = MagicMock() + slide_controller.on_stop_loop = MagicMock() + slide_controller.info_label = MagicMock() + slide_controller.song_menu = MagicMock() + slide_controller.displays = [MagicMock()] + slide_controller.toolbar = MagicMock() + slide_controller.split = 0 + slide_controller.type_prefix = 'test' + slide_controller.screen_capture = 'old_capture' + + # WHEN: _process_item is called + slide_controller._process_item(mocked_media_item, 0) + + # THEN: Registry.execute should have been called to start the live item + # Media should be closed + # Controller size change should be called (because it's a live item and the interface might have changed) + # The screen capture should have been reset to none + assert 1 == mocked_execute.call_count, 'Execute should have been called once' + slide_controller.on_media_close.assert_called_once_with() + slide_controller.on_controller_size_changed.assert_called_once() + assert slide_controller.screen_capture is None + + +@patch.object(Registry, 'execute') +def test_process_item_text(mocked_execute, registry, state_media): + """ + Test that the correct actions are taken a text item is processed + """ + # GIVEN: A mocked presentation service item, a mocked media service item, a mocked Registry.execute + # and a slide controller with many mocks. + # and the setting 'themes/item transitions' = True + mocked_media_item = MagicMock() + mocked_media_item.name = 'mocked_media_item' + mocked_media_item.get_transition_delay.return_value = 0 + mocked_media_item.is_text.return_value = True + mocked_media_item.is_command.return_value = False + mocked_media_item.is_media.return_value = False + mocked_media_item.requires_media.return_value = False + mocked_media_item.is_image.return_value = False + mocked_media_item.from_service = False + mocked_media_item.get_frames.return_value = [] + mocked_media_item.display_slides = [{'verse': 'Verse name'}] + mocked_settings = MagicMock() + mocked_settings.value.return_value = True + mocked_main_window = MagicMock() + Registry().register('main_window', mocked_main_window) + Registry().register('media_controller', MagicMock()) + Registry().register('application', MagicMock()) + Registry().register('settings', mocked_settings) + slide_controller = SlideController(None) + slide_controller.service_item = None + slide_controller.is_live = True + slide_controller._reset_blank = MagicMock() + slide_controller.preview_widget = MagicMock() + slide_controller.preview_display = MagicMock() + slide_controller.enable_tool_bar = MagicMock() + slide_controller.on_controller_size_changed = MagicMock() + slide_controller.on_media_start = MagicMock() + slide_controller.on_media_close = MagicMock() + slide_controller.slide_selected = MagicMock() + slide_controller.new_song_menu = MagicMock() + slide_controller.on_stop_loop = MagicMock() + slide_controller.info_label = MagicMock() + slide_controller.song_menu = MagicMock() + slide_controller.displays = [MagicMock()] + slide_controller.toolbar = MagicMock() + slide_controller.split = 0 + slide_controller.type_prefix = 'test' + slide_controller.screen_capture = 'old_capture' + + # WHEN: _process_item is called + slide_controller._process_item(mocked_media_item, 0) + + # THEN: Registry.execute should have been called to start the live item + # Controller size change should be called (because it's a live item and the interface might have changed) + # The screen capture should have been reset to none + # The slide should have been added to the slide list with the correct index + assert 1 == mocked_execute.call_count, 'Execute should have been called once' + slide_controller.on_controller_size_changed.assert_called_once() + assert slide_controller.screen_capture is None + assert slide_controller.slide_list['Verse name'] == 0 + + @patch.object(Registry, 'execute') def test_process_item_song_vlc(mocked_execute, registry, state_media): """ diff --git a/tests/interfaces/openlp_core/ui/test_starttimedialog.py b/tests/interfaces/openlp_core/ui/test_starttimedialog.py index 0653ac8c5..20954a06e 100644 --- a/tests/interfaces/openlp_core/ui/test_starttimedialog.py +++ b/tests/interfaces/openlp_core/ui/test_starttimedialog.py @@ -65,9 +65,9 @@ def test_time_display(form): """ # GIVEN: A service item with with time mocked_serviceitem = MagicMock() - mocked_serviceitem.start_time = 61 - mocked_serviceitem.end_time = 3701 - mocked_serviceitem.media_length = 3701 + mocked_serviceitem.start_time = 61000 + mocked_serviceitem.end_time = 3701000 + mocked_serviceitem.media_length = 3701000 # WHEN displaying the UI and pressing enter form.item = {'service_item': mocked_serviceitem} @@ -80,7 +80,7 @@ def test_time_display(form): assert form.hour_spin_box.value() == 0 assert form.minute_spin_box.value() == 1 assert form.second_spin_box.value() == 1 - assert form.item['service_item'].start_time == 61, 'The start time should stay the same' + assert form.item['service_item'].start_time == 61000, 'The start time should stay the same' # WHEN displaying the UI, changing the time to 2min 3secs and pressing enter form.item = {'service_item': mocked_serviceitem} @@ -95,4 +95,4 @@ def test_time_display(form): assert form.hour_spin_box.value() == 0 assert form.minute_spin_box.value() == 2 assert form.second_spin_box.value() == 3 - assert form.item['service_item'].start_time == 123, 'The start time should have changed' + assert form.item['service_item'].start_time == 123000, 'The start time should have changed'