comparison sat/test/test_plugin_misc_radiocol.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/test/test_plugin_misc_radiocol.py@2daf7b4c6756
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT: a jabber client
5 # Copyright (C) 2009, 2010, 2011, 2012, 2013 Jérôme Poisson (goffi@goffi.org)
6 # Copyright (C) 2013 Adrien Cossa (souliane@mailoo.org)
7
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
17
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21 """ Tests for the plugin radiocol """
22
23 from sat.core import exceptions
24 from sat.test import helpers, helpers_plugins
25 from sat.plugins import plugin_misc_radiocol as plugin
26 from sat.plugins import plugin_misc_room_game as plugin_room_game
27 from constants import Const
28
29 from twisted.words.protocols.jabber.jid import JID
30 from twisted.words.xish import domish
31 from twisted.internet import reactor
32 from twisted.internet import defer
33 from twisted.python.failure import Failure
34 from twisted.trial.unittest import SkipTest
35
36 try:
37 from mutagen.oggvorbis import OggVorbis
38 from mutagen.mp3 import MP3
39 from mutagen.easyid3 import EasyID3
40 from mutagen.id3 import ID3NoHeaderError
41 except ImportError:
42 raise exceptions.MissingModule(u"Missing module Mutagen, please download/install from https://bitbucket.org/lazka/mutagen")
43
44 import uuid
45 import os
46 import copy
47 import shutil
48
49
50 ROOM_JID = JID(Const.MUC_STR[0])
51 PROFILE = Const.PROFILE[0]
52 REFEREE_FULL = JID(ROOM_JID.userhost() + '/' + Const.JID[0].user)
53 PLAYERS_INDICES = [0, 1, 3] # referee included
54 OTHER_PROFILES = [Const.PROFILE[1], Const.PROFILE[3]]
55 OTHER_PLAYERS = [Const.JID[1], Const.JID[3]]
56
57
58 class RadiocolTest(helpers.SatTestCase):
59
60 def setUp(self):
61 self.host = helpers.FakeSAT()
62
63 def reinit(self):
64 self.host.reinit()
65 self.host.plugins['ROOM-GAME'] = plugin_room_game.RoomGame(self.host)
66 self.plugin = plugin.Radiocol(self.host) # must be init after ROOM-GAME
67 self.plugin.testing = True
68 self.plugin_0045 = self.host.plugins['XEP-0045'] = helpers_plugins.FakeXEP_0045(self.host)
69 self.plugin_0249 = self.host.plugins['XEP-0249'] = helpers_plugins.FakeXEP_0249(self.host)
70 for profile in Const.PROFILE:
71 self.host.getClient(profile) # init self.host.profiles[profile]
72 self.songs = []
73 self.playlist = []
74 self.sound_dir = self.host.memory.getConfig('', 'media_dir') + '/test/sound/'
75 try:
76 for filename in os.listdir(self.sound_dir):
77 if filename.endswith('.ogg') or filename.endswith('.mp3'):
78 self.songs.append(filename)
79 except OSError:
80 raise SkipTest('The sound samples in sat_media/test/sound were not found')
81
82 def _buildPlayers(self, players=[]):
83 """@return: the "started" content built with the given players"""
84 content = "<started"
85 if not players:
86 content += "/>"
87 else:
88 content += ">"
89 for i in xrange(0, len(players)):
90 content += "<player index='%s'>%s</player>" % (i, players[i])
91 content += "</started>"
92 return content
93
94 def _expectedMessage(self, to_jid, type_, content):
95 """
96 @param to_jid: recipient full jid
97 @param type_: message type ('normal' or 'groupchat')
98 @param content: content as unicode or list of domish elements
99 @return: the message XML built from the given recipient, message type and content
100 """
101 if isinstance(content, list):
102 new_content = copy.deepcopy(content)
103 for element in new_content:
104 if not element.hasAttribute('xmlns'):
105 element['xmlns'] = ''
106 content = "".join([element.toXml() for element in new_content])
107 return "<message to='%s' type='%s'><%s xmlns='%s'>%s</%s></message>" % (to_jid.full(), type_, plugin.RADIOC_TAG, plugin.NC_RADIOCOL, content, plugin.RADIOC_TAG)
108
109 def _rejectSongCb(self, profile_index):
110 """Check if the message "song_rejected" has been sent by the referee
111 and process the command with the profile of the uploader
112 @param profile_index: uploader's profile"""
113 sent = self.host.getSentMessage(0)
114 content = "<song_rejected xmlns='' reason='Too many songs in queue'/>"
115 self.assertEqualXML(sent.toXml(), self._expectedMessage(JID(ROOM_JID.userhost() + '/' + self.plugin_0045.getNick(0, profile_index), 'normal', content)))
116 self._roomGameCmd(sent, ['radiocolSongRejected', ROOM_JID.full(), 'Too many songs in queue'])
117
118 def _noUploadCb(self):
119 """Check if the message "no_upload" has been sent by the referee
120 and process the command with the profiles of each room users"""
121 sent = self.host.getSentMessage(0)
122 content = "<no_upload xmlns=''/>"
123 self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID, 'groupchat', content))
124 self._roomGameCmd(sent, ['radiocolNoUpload', ROOM_JID.full()])
125
126 def _uploadOkCb(self):
127 """Check if the message "upload_ok" has been sent by the referee
128 and process the command with the profiles of each room users"""
129 sent = self.host.getSentMessage(0)
130 content = "<upload_ok xmlns=''/>"
131 self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID, 'groupchat', content))
132 self._roomGameCmd(sent, ['radiocolUploadOk', ROOM_JID.full()])
133
134 def _preloadCb(self, attrs, profile_index):
135 """Check if the message "preload" has been sent by the referee
136 and process the command with the profiles of each room users
137 @param attrs: information dict about the song
138 @param profile_index: profile index of the uploader
139 """
140 sent = self.host.getSentMessage(0)
141 attrs['sender'] = self.plugin_0045.getNick(0, profile_index)
142 radiocol_elt = domish.generateElementsNamed(sent.elements(), 'radiocol').next()
143 preload_elt = domish.generateElementsNamed(radiocol_elt.elements(), 'preload').next()
144 attrs['timestamp'] = preload_elt['timestamp'] # we could not guess it...
145 content = "<preload xmlns='' %s/>" % " ".join(["%s='%s'" % (attr, attrs[attr]) for attr in attrs])
146 if sent.hasAttribute('from'):
147 del sent['from']
148 self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID, 'groupchat', content))
149 self._roomGameCmd(sent, ['radiocolPreload', ROOM_JID.full(), attrs['timestamp'], attrs['filename'], attrs['title'], attrs['artist'], attrs['album'], attrs['sender']])
150
151 def _playNextSongCb(self):
152 """Check if the message "play" has been sent by the referee
153 and process the command with the profiles of each room users"""
154 sent = self.host.getSentMessage(0)
155 filename = self.playlist.pop(0)
156 content = "<play xmlns='' filename='%s' />" % filename
157 self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID, 'groupchat', content))
158 self._roomGameCmd(sent, ['radiocolPlay', ROOM_JID.full(), filename])
159
160 game_data = self.plugin.games[ROOM_JID]
161 if len(game_data['queue']) == plugin.QUEUE_LIMIT - 1:
162 self._uploadOkCb()
163
164 def _addSongCb(self, d, filepath, profile_index):
165 """Check if the message "song_added" has been sent by the uploader
166 and process the command with the profile of the referee
167 @param d: deferred value or failure got from self.plugin.radiocolSongAdded
168 @param filepath: full path to the sound file
169 @param profile_index: the profile index of the uploader
170 """
171 if isinstance(d, Failure):
172 self.fail("OGG or MP3 song could not be added!")
173
174 game_data = self.plugin.games[ROOM_JID]
175
176 # this is copied from the plugin
177 if filepath.lower().endswith('.mp3'):
178 actual_song = MP3(filepath)
179 try:
180 song = EasyID3(filepath)
181
182 class Info(object):
183 def __init__(self, length):
184 self.length = length
185 song.info = Info(actual_song.info.length)
186 except ID3NoHeaderError:
187 song = actual_song
188 else:
189 song = OggVorbis(filepath)
190
191 attrs = {'filename': os.path.basename(filepath),
192 'title': song.get("title", ["Unknown"])[0],
193 'artist': song.get("artist", ["Unknown"])[0],
194 'album': song.get("album", ["Unknown"])[0],
195 'length': str(song.info.length)
196 }
197 self.assertEqual(game_data['to_delete'][attrs['filename']], filepath)
198
199 content = "<song_added xmlns='' %s/>" % " ".join(["%s='%s'" % (attr, attrs[attr]) for attr in attrs])
200 sent = self.host.getSentMessage(profile_index)
201 self.assertEqualXML(sent.toXml(), self._expectedMessage(REFEREE_FULL, 'normal', content))
202
203 reject_song = len(game_data['queue']) >= plugin.QUEUE_LIMIT
204 no_upload = len(game_data['queue']) + 1 >= plugin.QUEUE_LIMIT
205 play_next = not game_data['playing'] and len(game_data['queue']) + 1 == plugin.QUEUE_TO_START
206
207 self._roomGameCmd(sent, profile_index) # queue unchanged or +1
208 if reject_song:
209 self._rejectSongCb(profile_index)
210 return
211 if no_upload:
212 self._noUploadCb()
213 self._preloadCb(attrs, profile_index)
214 self.playlist.append(attrs['filename'])
215 if play_next:
216 self._playNextSongCb() # queue -1
217
218 def _roomGameCmd(self, sent, from_index=0, call=[]):
219 """Process a command. It is also possible to call this method as
220 _roomGameCmd(sent, call) instead of _roomGameCmd(sent, from_index, call).
221 If from index is a list, it is assumed that it is containing the value
222 for call and from_index will take its default value.
223 @param sent: the sent message that we need to process
224 @param from_index: index of the message sender
225 @param call: list containing the name of the expected bridge call
226 followed by its arguments, or empty list if no call is expected
227 """
228 if isinstance(from_index, list):
229 call = from_index
230 from_index = 0
231
232 sent['from'] = ROOM_JID.full() + '/' + self.plugin_0045.getNick(0, from_index)
233 recipient = JID(sent['to']).resource
234
235 # The message could have been sent to a room user (room_jid + '/' + nick),
236 # but when it is received, the 'to' attribute of the message has been
237 # changed to the recipient own JID. We need to simulate that here.
238 if recipient:
239 room = self.plugin_0045.getRoom(0, 0)
240 sent['to'] = Const.JID_STR[0] if recipient == room.nick else room.roster[recipient].entity.full()
241
242 for index in xrange(0, len(Const.PROFILE)):
243 nick = self.plugin_0045.getNick(0, index)
244 if nick:
245 if not recipient or nick == recipient:
246 if call and (self.plugin.isPlayer(ROOM_JID, nick) or call[0] == 'radiocolStarted'):
247 args = copy.deepcopy(call)
248 args.append(Const.PROFILE[index])
249 self.host.bridge.expectCall(*args)
250 self.plugin.room_game_cmd(sent, Const.PROFILE[index])
251
252 def _syncCb(self, sync_data, profile_index):
253 """Synchronize one player when he joins a running game.
254 @param sync_data: result from self.plugin.getSyncData
255 @param profile_index: index of the profile to be synchronized
256 """
257 for nick in sync_data:
258 expected = self._expectedMessage(JID(ROOM_JID.userhost() + '/' + nick), 'normal', sync_data[nick])
259 sent = self.host.getSentMessage(0)
260 self.assertEqualXML(sent.toXml(), expected)
261 for elt in sync_data[nick]:
262 if elt.name == 'preload':
263 self.host.bridge.expectCall('radiocolPreload', ROOM_JID.full(), elt['timestamp'], elt['filename'], elt['title'], elt['artist'], elt['album'], elt['sender'], Const.PROFILE[profile_index])
264 elif elt.name == 'play':
265 self.host.bridge.expectCall('radiocolPlay', ROOM_JID.full(), elt['filename'], Const.PROFILE[profile_index])
266 elif elt.name == 'no_upload':
267 self.host.bridge.expectCall('radiocolNoUpload', ROOM_JID.full(), Const.PROFILE[profile_index])
268 sync_data[nick]
269 self._roomGameCmd(sent, [])
270
271 def _joinRoom(self, room, nicks, player_index, sync=True):
272 """Make a player join a room and update the list of nicks
273 @param room: wokkel.muc.Room instance from the referee perspective
274 @param nicks: list of the players which will be updated
275 @param player_index: profile index of the new player
276 @param sync: set to True to synchronize data
277 """
278 user_nick = self.plugin_0045.joinRoom(0, player_index)
279 self.plugin.userJoinedTrigger(room, room.roster[user_nick], PROFILE)
280 if player_index not in PLAYERS_INDICES:
281 # this user is actually not a player
282 self.assertFalse(self.plugin.isPlayer(ROOM_JID, user_nick))
283 to_jid, type_ = (JID(ROOM_JID.userhost() + '/' + user_nick), 'normal')
284 else:
285 # this user is a player
286 self.assertTrue(self.plugin.isPlayer(ROOM_JID, user_nick))
287 nicks.append(user_nick)
288 to_jid, type_ = (ROOM_JID, 'groupchat')
289
290 # Check that the message "players" has been sent by the referee
291 expected = self._expectedMessage(to_jid, type_, self._buildPlayers(nicks))
292 sent = self.host.getSentMessage(0)
293 self.assertEqualXML(sent.toXml(), expected)
294
295 # Process the command with the profiles of each room users
296 self._roomGameCmd(sent, ['radiocolStarted', ROOM_JID.full(), REFEREE_FULL.full(), nicks, [plugin.QUEUE_TO_START, plugin.QUEUE_LIMIT]])
297
298 if sync:
299 self._syncCb(self.plugin._getSyncData(ROOM_JID, [user_nick]), player_index)
300
301 def _leaveRoom(self, room, nicks, player_index):
302 """Make a player leave a room and update the list of nicks
303 @param room: wokkel.muc.Room instance from the referee perspective
304 @param nicks: list of the players which will be updated
305 @param player_index: profile index of the new player
306 """
307 user_nick = self.plugin_0045.getNick(0, player_index)
308 user = room.roster[user_nick]
309 self.plugin_0045.leaveRoom(0, player_index)
310 self.plugin.userLeftTrigger(room, user, PROFILE)
311 nicks.remove(user_nick)
312
313 def _uploadSong(self, song_index, profile_index):
314 """Upload the song of index song_index (modulo self.songs size) from the profile of index profile_index.
315
316 @param song_index: index of the song or None to test with non existing file
317 @param profile_index: index of the uploader's profile
318 """
319 if song_index is None:
320 dst_filepath = unicode(uuid.uuid1())
321 expect_io_error = True
322 else:
323 song_index = song_index % len(self.songs)
324 src_filename = self.songs[song_index]
325 dst_filepath = '/tmp/%s%s' % (uuid.uuid1(), os.path.splitext(src_filename)[1])
326 shutil.copy(self.sound_dir + src_filename, dst_filepath)
327 expect_io_error = False
328
329 try:
330 d = self.plugin.radiocolSongAdded(REFEREE_FULL, dst_filepath, Const.PROFILE[profile_index])
331 except IOError:
332 self.assertTrue(expect_io_error)
333 return
334
335 self.assertFalse(expect_io_error)
336 cb = lambda defer: self._addSongCb(defer, dst_filepath, profile_index)
337
338 def eb(failure):
339 if not isinstance(failure, Failure):
340 self.fail("Adding a song which is not OGG nor MP3 should fail!")
341 self.assertEqual(failure.value.__class__, exceptions.DataError)
342
343 if src_filename.endswith('.ogg') or src_filename.endswith('.mp3'):
344 d.addCallbacks(cb, cb)
345 else:
346 d.addCallbacks(eb, eb)
347
348 def test_init(self):
349 self.reinit()
350 self.assertEqual(self.plugin.invite_mode, self.plugin.FROM_PLAYERS)
351 self.assertEqual(self.plugin.wait_mode, self.plugin.FOR_NONE)
352 self.assertEqual(self.plugin.join_mode, self.plugin.INVITED)
353 self.assertEqual(self.plugin.ready_mode, self.plugin.FORCE)
354
355 def test_game(self):
356 self.reinit()
357
358 # create game
359 self.plugin.prepareRoom(OTHER_PLAYERS, ROOM_JID, PROFILE)
360 self.assertTrue(self.plugin._gameExists(ROOM_JID, True))
361 room = self.plugin_0045.getRoom(0, 0)
362 nicks = [self.plugin_0045.getNick(0, 0)]
363
364 sent = self.host.getSentMessage(0)
365 self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID, 'groupchat', self._buildPlayers(nicks)))
366 self._roomGameCmd(sent, ['radiocolStarted', ROOM_JID.full(), REFEREE_FULL.full(), nicks, [plugin.QUEUE_TO_START, plugin.QUEUE_LIMIT]])
367
368 self._joinRoom(room, nicks, 1) # player joins
369 self._joinRoom(room, nicks, 4) # user not playing joins
370
371 song_index = 0
372 self._uploadSong(song_index, 0) # ogg or mp3 file should exist in sat_media/test/song
373 self._uploadSong(None, 0) # non existing file
374
375 # another songs are added by Const.JID[1] until the radio starts + 1 to fill the queue
376 # when the first song starts + 1 to be rejected because the queue is full
377 for song_index in xrange(1, plugin.QUEUE_TO_START + 1):
378 self._uploadSong(song_index, 1)
379
380 self.plugin.playNext(Const.MUC[0], PROFILE) # simulate the end of the first song
381 self._playNextSongCb()
382 self._uploadSong(song_index, 1) # now the song is accepted and the queue is full again
383
384 self._joinRoom(room, nicks, 3) # new player joins
385
386 self.plugin.playNext(Const.MUC[0], PROFILE) # the second song finishes
387 self._playNextSongCb()
388 self._uploadSong(0, 3) # the player who recently joined re-upload the first file
389
390 self._leaveRoom(room, nicks, 1) # one player leaves
391 self._joinRoom(room, nicks, 1) # and join again
392
393 self.plugin.playNext(Const.MUC[0], PROFILE) # empty the queue
394 self._playNextSongCb()
395 self.plugin.playNext(Const.MUC[0], PROFILE)
396 self._playNextSongCb()
397
398 for filename in self.playlist:
399 self.plugin.deleteFile('/tmp/' + filename)
400
401 return defer.succeed(None)
402
403 def tearDown(self, *args, **kwargs):
404 """Clean the reactor"""
405 helpers.SatTestCase.tearDown(self, *args, **kwargs)
406 for delayed_call in reactor.getDelayedCalls():
407 delayed_call.cancel()