comparison src/test/test_plugin_misc_radiocol.py @ 830:d6bdf6022180

test radiocol: added a full scenario test
author souliane <souliane@mailoo.org>
date Wed, 15 Jan 2014 23:24:22 +0100
parents
children c5a8f602662b
comparison
equal deleted inserted replaced
829:187d2443c82d 830:d6bdf6022180
1 #!/usr/bin/python
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.i18n import _
24 from sat.core import exceptions
25 from sat.test import helpers, helpers_plugins
26 from sat.plugins import plugin_misc_radiocol as plugin
27 from sat.plugins import plugin_misc_room_game as plugin_room_game
28 from constants import Const
29
30 from twisted.words.protocols.jabber.jid import JID
31 from twisted.words.xish import domish
32 from twisted.internet import reactor
33 from twisted.python.failure import Failure
34
35 from mutagen.oggvorbis import OggVorbis
36
37 import uuid
38 import logging
39 import os
40 import copy
41 import shutil
42
43 ROOM_JID_S = Const.MUC_STR[0]
44 PROFILE = Const.PROFILE[0]
45 REFEREE_FULL = ROOM_JID_S + '/' + Const.JID[0].user
46 PLAYERS_INDICES = [0, 1, 3] # referee included
47 OTHER_PROFILES = [Const.PROFILE[1], Const.PROFILE[3]]
48 OTHER_PLAYERS = [Const.JID_STR[1], Const.JID_STR[3]]
49
50
51 class RadiocolTest(helpers.SatTestCase):
52
53 def setUp(self):
54 self.host = helpers.FakeSAT()
55
56 def init(self):
57 self.host.init()
58 self.host.plugins['ROOM-GAME'] = plugin_room_game.RoomGame(self.host)
59 self.plugin = plugin.Radiocol(self.host) # must be init after ROOM-GAME
60 self.plugin.testing = True
61 self.plugin_0045 = self.host.plugins['XEP-0045'] = helpers_plugins.FakeXEP_0045(self.host)
62 self.plugin_0249 = self.host.plugins['XEP-0249'] = helpers_plugins.FakeXEP_0249(self.host)
63 logger = logging.getLogger()
64 level = logger.getEffectiveLevel()
65 logger.setLevel(logging.WARNING) # remove info pollution
66 for profile in Const.PROFILE:
67 self.host.getClient(profile) # init self.host.profiles[profile]
68 logger.setLevel(level)
69 self.songs = []
70 self.playlist = []
71 self.sound_dir = self.host.memory.getConfig('', 'media_dir') + '/test/sound/'
72 for filename in os.listdir(self.sound_dir):
73 if filename.endswith('.ogg'):
74 self.songs.append(filename)
75
76 def _buildPlayers(self, players=[]):
77 """@return: the "started" content built with the given players"""
78 content = "<started"
79 if not players:
80 content += "/>"
81 else:
82 content += ">"
83 for i in xrange(0, len(players)):
84 content += "<player index='%s'>%s</player>" % (i, players[i])
85 content += "</started>"
86 return content
87
88 def _expectedMessage(self, to_s, type_, content):
89 """
90 @param to_s: recipient full jid as unicode
91 @param type_: message type ('normal' or 'groupchat')
92 @param content: content as unicode or list of domish elements
93 @return: the message XML built from the given recipient, message type and content
94 """
95 if isinstance(content, list):
96 new_content = copy.deepcopy(content)
97 for element in new_content:
98 if not element.hasAttribute('xmlns'):
99 element['xmlns'] = ''
100 content = "".join([element.toXml() for element in new_content])
101 return "<message to='%s' type='%s'><%s xmlns='%s'>%s</%s></message>" % (to_s, type_, plugin.RADIOC_TAG, plugin.NC_RADIOCOL, content, plugin.RADIOC_TAG)
102
103 def _rejectSongCb(self, profile_index):
104 """Check if the message "song_rejected" has been sent by the referee
105 and process the command with the profile of the uploader
106 @param profile_index: uploader's profile"""
107 sent = self.host.getSentMessageRaw(0)
108 content = "<song_rejected xmlns='' reason='Too many songs in queue'/>"
109 self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID_S + '/' + self.plugin_0045.getNick(0, profile_index), 'normal', content))
110 self._roomGameCmd(sent, ['radiocolSongRejected', ROOM_JID_S, 'Too many songs in queue'])
111
112 def _noUploadCb(self):
113 """Check if the message "no_upload" has been sent by the referee
114 and process the command with the profiles of each room users"""
115 sent = self.host.getSentMessageRaw(0)
116 content = "<no_upload xmlns=''/>"
117 self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID_S, 'groupchat', content))
118 self._roomGameCmd(sent, ['radiocolNoUpload', ROOM_JID_S])
119
120 def _uploadOkCb(self):
121 """Check if the message "upload_ok" has been sent by the referee
122 and process the command with the profiles of each room users"""
123 sent = self.host.getSentMessageRaw(0)
124 content = "<upload_ok xmlns=''/>"
125 self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID_S, 'groupchat', content))
126 self._roomGameCmd(sent, ['radiocolUploadOk', ROOM_JID_S])
127
128 def _preloadCb(self, attrs, profile_index):
129 """Check if the message "preload" has been sent by the referee
130 and process the command with the profiles of each room users
131 @param attrs: information dict about the song
132 @param profile_index: profile index of the uploader
133 """
134 sent = self.host.getSentMessageRaw(0)
135 attrs['sender'] = self.plugin_0045.getNick(0, profile_index)
136 radiocol_elt = domish.generateElementsNamed(sent.elements(), 'radiocol').next()
137 preload_elt = domish.generateElementsNamed(radiocol_elt.elements(), 'preload').next()
138 attrs['timestamp'] = preload_elt['timestamp'] # we could not guess it...
139 content = "<preload xmlns='' %s/>" % " ".join(["%s='%s'" % (attr, attrs[attr]) for attr in attrs])
140 if sent.hasAttribute('from'):
141 del sent['from']
142 self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID_S, 'groupchat', content))
143 self._roomGameCmd(sent, ['radiocolPreload', ROOM_JID_S, attrs['timestamp'], attrs['filename'], attrs['title'], attrs['artist'], attrs['album']])
144
145 def _playNextSongCb(self):
146 """Check if the message "play" has been sent by the referee
147 and process the command with the profiles of each room users"""
148 sent = self.host.getSentMessageRaw(0)
149 filename = self.playlist.pop(0)
150 content = "<play xmlns='' filename='%s' />" % filename
151 self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID_S, 'groupchat', content))
152 self._roomGameCmd(sent, ['radiocolPlay', ROOM_JID_S, filename])
153
154 game_data = self.plugin.games[ROOM_JID_S]
155 if len(game_data['queue']) == plugin.QUEUE_LIMIT - 1:
156 self._uploadOkCb()
157
158 def _addSongCb(self, d, filepath, profile_index):
159 """Check if the message "song_added" has been sent by the uploader
160 and process the command with the profile of the referee
161 @param d: deferred value or failure got from self.plugin.radiocolSongAdded
162 @param filepath: full path to the sound file
163 @param profile_index: the profile index of the uploader
164 """
165 if isinstance(d, Failure):
166 self.fail("OGG song could not be added!")
167
168 game_data = self.plugin.games[ROOM_JID_S]
169 song = OggVorbis(filepath)
170 attrs = {'filename': os.path.basename(filepath),
171 'title': song.get("title", ["Unknown"])[0],
172 'artist': song.get("artist", ["Unknown"])[0],
173 'album': song.get("album", ["Unknown"])[0],
174 'length': str(song.info.length)
175 }
176 self.assertEqual(game_data['to_delete'][attrs['filename']], filepath)
177
178 content = "<song_added xmlns='' %s/>" % " ".join(["%s='%s'" % (attr, attrs[attr]) for attr in attrs])
179 sent = self.host.getSentMessageRaw(profile_index)
180 self.assertEqualXML(sent.toXml(), self._expectedMessage(REFEREE_FULL, 'normal', content))
181
182 reject_song = len(game_data['queue']) >= plugin.QUEUE_LIMIT
183 no_upload = len(game_data['queue']) + 1 >= plugin.QUEUE_LIMIT
184 play_next = not game_data['playing'] and len(game_data['queue']) + 1 == plugin.QUEUE_TO_START
185
186 self._roomGameCmd(sent, profile_index) # queue unchanged or +1
187 if reject_song:
188 self._rejectSongCb(profile_index)
189 return
190 if no_upload:
191 self._noUploadCb()
192 self._preloadCb(attrs, profile_index)
193 self.playlist.append(attrs['filename'])
194 if play_next:
195 self._playNextSongCb() # queue -1
196
197 def _roomGameCmd(self, sent, from_index=0, call=[]):
198 """Process a command. It is also possible to call this method as
199 _roomGameCmd(sent, call) instead of _roomGameCmd(sent, from_index, call).
200 If from index is a list, it is assumed that it is containing the value
201 for call and from_index will take its default value.
202 @param sent: the sent message that we need to process
203 @param from_index: index of the message sender
204 @param call: list containing the name of the expected bridge call
205 followed by its arguments, or empty list if no call is expected
206 """
207 if isinstance(from_index, list):
208 call = from_index
209 from_index = 0
210
211 sent['from'] = ROOM_JID_S + '/' + self.plugin_0045.getNick(0, from_index)
212 recipient = JID(sent['to']).resource
213
214 # The message could have been sent to a room user (room_jid + '/' + nick),
215 # but when it is received, the 'to' attribute of the message has been
216 # changed to the recipient own JID. We need to simulate that here.
217 if recipient:
218 room = self.plugin_0045.getRoom(0, 0)
219 sent['to'] = Const.JID_STR[0] if recipient == room.nick else room.roster[recipient].entity.full()
220
221 for index in xrange(0, len(Const.PROFILE)):
222 nick = self.plugin_0045.getNick(0, index)
223 if nick:
224 if not recipient or nick == recipient:
225 if call and (self.plugin.isPlayer(ROOM_JID_S, nick) or call[0] == 'radiocolStarted'):
226 args = copy.deepcopy(call)
227 args.append(Const.PROFILE[index])
228 self.host.bridge.expectCall(*args)
229 self.plugin.room_game_cmd(sent, Const.PROFILE[index])
230
231 def _syncCb(self, sync_data, profile_index):
232 """Synchronize one player when he joins a running game.
233 @param sync_data: result from self.plugin.getSyncData
234 @param profile_index: index of the profile to be synchronized
235 """
236 for nick in sync_data:
237 expected = self._expectedMessage(ROOM_JID_S + '/' + nick, 'normal', sync_data[nick])
238 sent = self.host.getSentMessageRaw(0)
239 self.assertEqualXML(sent.toXml(), expected)
240 for elt in sync_data[nick]:
241 if elt.name == 'preload':
242 self.host.bridge.expectCall('radiocolPreload', ROOM_JID_S, elt['timestamp'], elt['filename'], elt['title'], elt['artist'], elt['album'], Const.PROFILE[profile_index])
243 elif elt.name == 'play':
244 self.host.bridge.expectCall('radiocolPlay', ROOM_JID_S, elt['filename'], Const.PROFILE[profile_index])
245 elif elt.name == 'no_upload':
246 self.host.bridge.expectCall('radiocolNoUpload', ROOM_JID_S, Const.PROFILE[profile_index])
247 sync_data[nick]
248 self._roomGameCmd(sent, [])
249
250 def _joinRoom(self, room, nicks, player_index, sync=True):
251 """Make a player join a room and update the list of nicks
252 @param room: wokkel.muc.Room instance from the referee perspective
253 @param nicks: list of the players which will be updated
254 @param player_index: profile index of the new player
255 @param sync: set to True to synchronize data
256 """
257 user_nick = self.plugin_0045.joinRoom(0, player_index)
258 self.plugin.userJoinedTrigger(room, room.roster[user_nick], PROFILE)
259 if player_index not in PLAYERS_INDICES:
260 # this user is actually not a player
261 self.assertFalse(self.plugin.isPlayer(ROOM_JID_S, user_nick))
262 to_jid, type_ = (ROOM_JID_S + '/' + user_nick, 'normal')
263 else:
264 # this user is a player
265 self.assertTrue(self.plugin.isPlayer(ROOM_JID_S, user_nick))
266 nicks.append(user_nick)
267 to_jid, type_ = (ROOM_JID_S, 'groupchat')
268
269 # Check that the message "players" has been sent by the referee
270 expected = self._expectedMessage(to_jid, type_, self._buildPlayers(nicks))
271 sent = self.host.getSentMessageRaw(0)
272 self.assertEqualXML(sent.toXml(), expected)
273
274 # Process the command with the profiles of each room users
275 self._roomGameCmd(sent, ['radiocolStarted', ROOM_JID_S, REFEREE_FULL, nicks, [plugin.QUEUE_TO_START, plugin.QUEUE_LIMIT]])
276
277 if sync:
278 self._syncCb(self.plugin._getSyncData(ROOM_JID_S, [user_nick]), player_index)
279
280 def _leaveRoom(self, room, nicks, player_index):
281 """Make a player leave a room and update the list of nicks
282 @param room: wokkel.muc.Room instance from the referee perspective
283 @param nicks: list of the players which will be updated
284 @param player_index: profile index of the new player
285 """
286 user_nick = self.plugin_0045.getNick(0, player_index)
287 user = room.roster[user_nick]
288 self.plugin_0045.leaveRoom(0, player_index)
289 self.plugin.userLeftTrigger(room, user, PROFILE)
290 nicks.remove(user_nick)
291
292 def _uploadSong(self, song_index, profile_index, ext='ogg'):
293 """Upload the song of index song_index (modulo self.songs size)
294 from the profile of index profile_index. All the songs in self.songs
295 are OGG, but you can set ext to a value different than 'ogg':
296 - 'mp3' to test unsupported file format (MP3 file should exist)
297 - 'xxx' to test non existent files
298 @param song_index: index of the song
299 @param profile_index: index of the uploader's profile
300 @param new_ext: change the extension from "ogg" to this value
301 """
302 song_index = song_index % len(self.songs)
303 src_filename = self.songs[song_index]
304 if ext != 'ogg':
305 src_filename.replace('ogg', ext)
306 dst_filepath = '/tmp/%s.%s' % (uuid.uuid1(), ext)
307 expect_io_error = ext == 'xxx'
308 if not expect_io_error:
309 shutil.copy(self.sound_dir + src_filename, dst_filepath)
310
311 try:
312 d = self.plugin.radiocolSongAdded(REFEREE_FULL, dst_filepath, Const.PROFILE[profile_index])
313 except IOError:
314 self.assertTrue(expect_io_error)
315 return
316
317 self.assertFalse(expect_io_error)
318 cb = lambda defer: self._addSongCb(defer, dst_filepath, profile_index)
319
320 def eb(failure):
321 if not isinstance(failure, Failure):
322 self.fail("Adding a song which is not OGG should fail!")
323 self.assertEqual(failure.value.__class__, exceptions.DataError)
324
325 if self.songs[song_index].endswith('.ogg'):
326 d.addCallbacks(cb, cb)
327 else:
328 d.addCallbacks(eb, eb)
329
330 def test_init(self):
331 self.init()
332 self.assertEqual(self.plugin.invite_mode, self.plugin.FROM_PLAYERS)
333 self.assertEqual(self.plugin.wait_mode, self.plugin.FOR_NONE)
334 self.assertEqual(self.plugin.join_mode, self.plugin.INVITED)
335 self.assertEqual(self.plugin.ready_mode, self.plugin.FORCE)
336
337 def test_game(self):
338 self.init()
339
340 # create game
341 self.plugin.prepareRoom(OTHER_PLAYERS, ROOM_JID_S, PROFILE)
342 self.assertTrue(self.plugin._gameExists(ROOM_JID_S, True))
343 room = self.plugin_0045.getRoom(0, 0)
344 nicks = [self.plugin_0045.getNick(0, 0)]
345
346 sent = self.host.getSentMessageRaw(0)
347 self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID_S, 'groupchat', self._buildPlayers(nicks)))
348 self._roomGameCmd(sent, ['radiocolStarted', ROOM_JID_S, REFEREE_FULL, nicks, [plugin.QUEUE_TO_START, plugin.QUEUE_LIMIT]])
349
350 self._joinRoom(room, nicks, 1) # player joins
351 self._joinRoom(room, nicks, 4) # user not playing joins
352
353 song_index = 0
354 self._uploadSong(song_index, 0) # ogg file should exist in sat_media/test/song
355 self._uploadSong(song_index, 0, 'mp3') # mp3 file should exist in sat_media/test/song
356 self._uploadSong(song_index, 0, 'xxx') # file should not exist
357
358 # another songs are added by Const.JID[1] until the radio starts + 1 to fill the queue
359 # when the first song starts + 1 to be rejected because the queue is full
360 for song_index in xrange(1, plugin.QUEUE_TO_START + 1):
361 self._uploadSong(song_index, 1)
362
363 self.plugin.playNext(Const.MUC[0], PROFILE) # simulate the end of the first song
364 self._playNextSongCb()
365 self._uploadSong(song_index, 1) # now the song is accepted and the queue is full again
366
367 self._joinRoom(room, nicks, 3) # new player joins
368
369 self.plugin.playNext(Const.MUC[0], PROFILE) # the second song finishes
370 self._playNextSongCb()
371 self._uploadSong(0, 3) # the player who recently joined re-upload the first file
372
373 self._leaveRoom(room, nicks, 1) # one player leaves
374 self._joinRoom(room, nicks, 1) # and join again
375
376 self.plugin.playNext(Const.MUC[0], PROFILE) # empty the queue
377 self._playNextSongCb()
378 self.plugin.playNext(Const.MUC[0], PROFILE)
379 self._playNextSongCb()
380
381 for filename in self.playlist:
382 self.plugin.deleteFile('/tmp/' + filename)
383
384 def tearDown(self, *args, **kwargs):
385 """Clean the reactor"""
386 helpers.SatTestCase.tearDown(self, *args, **kwargs)
387 for delayed_call in reactor.getDelayedCalls():
388 delayed_call.cancel()