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