1# This program is free software; you can redistribute it and/or modify 2# it under the terms of the GNU General Public License as published by 3# the Free Software Foundation; either version 2 of the License, or 4# (at your option) any later version. 5 6"""TODO: Share better with, i.e. test MenuItemPlugin directly""" 7 8import os 9import shutil 10 11from gi.repository import Gtk 12from quodlibet.browsers import Browser 13 14from quodlibet.library import SongLibrary 15from quodlibet.plugins.playlist import PlaylistPlugin, PlaylistPluginHandler 16from quodlibet.util.collection import Playlist 17from tests import TestCase, mkstemp, mkdtemp 18from quodlibet.plugins import PluginManager, Plugin 19from tests.helper import capture_output 20 21MAX_PLAYLISTS = 50 22TEST_PLAYLIST = Playlist("foo") 23 24 25def generate_playlists(n): 26 return [Playlist("Playlist %d" % x) for x in range(n)] 27 28 29class TPlaylistPlugins(TestCase): 30 31 class MockBrowser(Browser): 32 def __init__(self): 33 super(TPlaylistPlugins.MockBrowser, self).__init__() 34 self.activated = False 35 36 def activate(self): 37 self.activated = True 38 39 def get_toplevel(self): 40 return self 41 42 def is_toplevel(self): 43 return True 44 45 def _confirmer(self, *args): 46 self.confirmed = True 47 48 def setUp(self): 49 self.tempdir = mkdtemp() 50 self.pm = PluginManager(folders=[self.tempdir]) 51 self.confirmed = False 52 self.mock_browser = self.MockBrowser() 53 self.handler = PlaylistPluginHandler(self._confirmer) 54 self.pm.register_handler(self.handler) 55 self.pm.rescan() 56 self.assertEquals(self.pm.plugins, []) 57 self.library = SongLibrary('foo') 58 59 def tearDown(self): 60 self.library.destroy() 61 self.pm.quit() 62 shutil.rmtree(self.tempdir) 63 64 def create_plugin(self, id='', name='', desc='', icon='', 65 funcs=None, mod=False): 66 fd, fn = mkstemp(suffix='.py', text=True, dir=self.tempdir) 67 file = os.fdopen(fd, 'w') 68 69 if mod: 70 indent = '' 71 else: 72 file.write( 73 "from quodlibet.plugins.playlist import PlaylistPlugin\n") 74 file.write("class %s(PlaylistPlugin):\n" % name) 75 indent = ' ' 76 file.write("%spass\n" % indent) 77 78 if name: 79 file.write("%sPLUGIN_ID = %r\n" % (indent, name)) 80 if name: 81 file.write("%sPLUGIN_NAME = %r\n" % (indent, name)) 82 if desc: 83 file.write("%sPLUGIN_DESC = %r\n" % (indent, desc)) 84 if icon: 85 file.write("%sPLUGIN_ICON = %r\n" % (indent, icon)) 86 for f in (funcs or []): 87 if f in ["__init__"]: 88 file.write("%sdef %s(self, *args): super(%s, self).__init__(" 89 "*args); raise Exception(\"as expected.\")\n" 90 % (indent, f, name)) 91 else: 92 file.write("%sdef %s(*args): return args\n" % (indent, f)) 93 file.flush() 94 file.close() 95 96 def test_empty_has_no_plugins(self): 97 self.pm.rescan() 98 self.assertEquals(self.pm.plugins, []) 99 100 def test_name_and_desc_plus_func_is_one(self): 101 self.create_plugin(name='Name', desc='Desc', funcs=['plugin_playlist']) 102 self.pm.rescan() 103 self.assertEquals(len(self.pm.plugins), 1) 104 105 def test_additional_functions_still_only_one(self): 106 self.create_plugin(name='Name', desc='Desc', 107 funcs=['plugin_playlist', 'plugin_playlists']) 108 self.pm.rescan() 109 self.assertEquals(len(self.pm.plugins), 1) 110 111 def test_two_plugins_are_two(self): 112 self.create_plugin(name='Name', desc='Desc', funcs=['plugin_playlist']) 113 self.create_plugin(name='Name2', desc='Desc2', 114 funcs=['plugin_albums']) 115 self.pm.rescan() 116 self.assertEquals(len(self.pm.plugins), 2) 117 118 def test_disables_plugin(self): 119 self.create_plugin(name='Name', desc='Desc', funcs=['plugin_playlist']) 120 self.pm.rescan() 121 self.failIf(self.pm.enabled(self.pm.plugins[0])) 122 123 def test_enabledisable_plugin(self): 124 self.create_plugin(name='Name', desc='Desc', funcs=['plugin_playlist']) 125 self.pm.rescan() 126 plug = self.pm.plugins[0] 127 self.pm.enable(plug, True) 128 self.failUnless(self.pm.enabled(plug)) 129 self.pm.enable(plug, False) 130 self.failIf(self.pm.enabled(plug)) 131 132 def test_ignores_broken_plugin(self): 133 self.create_plugin(name="Broken", desc="Desc", 134 funcs=["__init__", "plugin_playlist"]) 135 136 self.pm.rescan() 137 plug = self.pm.plugins[0] 138 self.pm.enable(plug, True) 139 menu = Gtk.Menu() 140 with capture_output(): 141 self.handler.populate_menu(menu, None, self.mock_browser, 142 [TEST_PLAYLIST]) 143 self.failUnlessEqual(len(menu.get_children()), 0, 144 msg="Shouldn't have enabled a broken plugin") 145 146 def test_populate_menu(self): 147 plugin = Plugin(FakePlaylistPlugin) 148 self.handler.plugin_enable(plugin) 149 menu = Gtk.Menu() 150 self.handler.populate_menu(menu, None, self.mock_browser, 151 [TEST_PLAYLIST]) 152 # Don't forget the separator 153 num = len(menu.get_children()) - 1 154 self.failUnlessEqual(num, 1, msg="Need 1 plugin not %d" % num) 155 156 def test_handling_playlists_without_confirmation(self): 157 plugin = Plugin(FakePlaylistPlugin) 158 self.handler.plugin_enable(plugin) 159 playlists = generate_playlists(MAX_PLAYLISTS) 160 self.handler.handle(plugin.id, self.library, self.mock_browser, 161 playlists) 162 self.failUnless("Didn't execute plugin", 163 FakePlaylistPlugin.total > 0) 164 self.failIf(self.confirmed, ("Wasn't expecting a confirmation for %d" 165 " invocations" % len(playlists))) 166 167 def test_handling_lots_of_songs_with_confirmation(self): 168 plugin = Plugin(FakePlaylistPlugin) 169 self.handler.plugin_enable(plugin) 170 playlists = generate_playlists(MAX_PLAYLISTS + 1) 171 self.handler.handle(plugin.id, self.library, self.mock_browser, 172 playlists) 173 self.failUnless(self.confirmed, 174 ("Should have confirmed %d invocations (Max=%d)." 175 % (len(playlists), MAX_PLAYLISTS))) 176 177 178class FakePlaylistPlugin(PlaylistPlugin): 179 PLUGIN_NAME = "Fake Playlist Plugin" 180 PLUGIN_ID = "PlaylistMunger" 181 MAX_INVOCATIONS = MAX_PLAYLISTS 182 total = 0 183 184 def __init__(self, playlists, library): 185 super(FakePlaylistPlugin, self).__init__(playlists, library) 186 self.total = 0 187 188 def plugin_playlist(self, _): 189 self.total += 1 190 if self.total > self.MAX_INVOCATIONS: 191 raise ValueError("Shouldn't have called me on this many songs" 192 " (%d > %d)" % (self.total, self.MAX_INVOCATIONS)) 193