3 # This file is part of DisOrder.
4 # Copyright (C) 2007-2012 Richard Kettlewell
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 """Utility module used by tests"""
22 import os
,os
.path
,subprocess
,sys
,re
,time
,unicodedata
,random
,socket
,traceback
23 import atexit
,base64
,errno
28 """Write an error message and exit"""
29 sys
.stderr
.write("ERROR: %s\n" % s
)
34 if homelink
is not None:
37 # Identify the top build directory
39 if os
.path
.exists("config.h"):
41 elif os
.path
.exists("../config.h"):
42 top_builddir
= os
.path
.dirname(cwd
)
44 fatal("cannot identify build directory")
46 # Make sure the Python build directory is on the module search path
47 sys
.path
.insert(0, os
.path
.join(top_builddir
, "python"))
50 # Make sure the build directories are on the executable search path
51 ospath
= os
.environ
["PATH"].split(os
.pathsep
)
52 ospath
.insert(0, os
.path
.join(top_builddir
, "server"))
53 ospath
.insert(0, os
.path
.join(top_builddir
, "clients"))
54 ospath
.insert(0, os
.path
.join(top_builddir
, "tests"))
55 os
.environ
["PATH"] = os
.pathsep
.join(ospath
)
57 # Some of our track names contain non-ASCII characters, and the server will
58 # be sad if it can't convert them according to the current locale. Make sure
59 # we have something plausible.
60 locale
= os
.environ
.get("LANG")
61 if locale
is None or locale
== "C" or locale
== "POSIX":
62 os
.environ
["LANG"] = "C.UTF-8"
64 # Parse the makefile in the current directory to identify the source directory
66 for l
in file("Makefile"):
67 r
= re
.match("top_srcdir *= *(.*)", l
)
69 top_srcdir
= r
.group(1)
72 fatal("cannot identify source directory")
74 # The tests source directory must be on the module search path already since
77 # -----------------------------------------------------------------------------
82 open(b
,"w").write(open(a
).read())
85 """Convert UTF-8 to unicode. A no-op if already unicode."""
86 if type(s
) == unicode:
89 return unicode(s
, "UTF-8")
92 """Convert UTF-8 string or unicode to NFC unicode."""
93 return unicodedata
.normalize("NFC", to_unicode(s
))
98 Make track with relative path S exist"""
99 trackpath
= "%s/%s" %
(tracks
, s
)
100 trackdir
= os
.path
.dirname(trackpath
)
101 if not os
.path
.exists(trackdir
):
102 os
.makedirs(trackdir
)
103 copyfile("%s/sounds/long.ogg" % top_srcdir
, trackpath
)
104 # We record the tracks we created so they can be tested against
105 # server responses. We put them into NFC since that's what the server
107 bits
= nfc(s
).split('/')
109 for d
in bits
[0:-1]:
110 dd
= "%s/%s" %
(dp
, d
)
111 if dp
not in dirs_by_dir
:
113 if dd
not in dirs_by_dir
[dp
]:
114 dirs_by_dir
[dp
].append(dd
)
115 dp
= "%s/%s" %
(dp
, d
)
116 if dp
not in files_by_dir
:
117 files_by_dir
[dp
] = []
118 files_by_dir
[dp
].append("%s/%s" %
(dp
, bits
[-1]))
121 # We create some tracks with non-ASCII characters in the name and
122 # we (currently) force UTF-8.
124 # On a traditional UNIX filesystem, that treats filenames as byte strings
125 # with special significant for '/', this should just work, though the
126 # names will look wrong to ls(1) in a non UTF-8 locale.
128 # On Apple HFS+ filenames normalized to a decomposed form that isn't quite
129 # NFD, so our attempts to have both normalized and denormalized filenames
130 # is frustrated. Provided we test on traditional filesytsems too this
131 # shouldn't be a problem.
132 # (See http://developer.apple.com/qa/qa2001/qa1173.html)
134 global dirs_by_dir
, files_by_dir
138 # C3 8C = 00CC LATIN CAPITAL LETTER I WITH GRAVE
140 maketrack("Joe Bloggs/First Album/01:F\xC3\x8Crst track.ogg")
142 maketrack("Joe Bloggs/First Album/02:Second track.ogg")
144 # CC 81 = 0301 COMBINING ACUTE ACCENT
145 # (giving an NFD i-acute)
146 maketrack("Joe Bloggs/First Album/03:ThI\xCC\x81rd track.ogg")
147 # ...hopefuly giving C3 8D = 00CD LATIN CAPITAL LETTER I WITH ACUTE
148 maketrack("Joe Bloggs/First Album/04:Fourth track.ogg")
149 maketrack("Joe Bloggs/First Album/05:Fifth track.ogg")
150 maketrack("Joe Bloggs/Second Album/01:First track.ogg")
151 maketrack("Joe Bloggs/Second Album/02:Second track.ogg")
152 maketrack("Joe Bloggs/Second Album/03:Third track.ogg")
153 maketrack("Joe Bloggs/Second Album/04:Fourth track.ogg")
154 maketrack("Joe Bloggs/Second Album/05:Fifth track.ogg")
155 maketrack("Joe Bloggs/Third Album/01:First_track.ogg")
156 maketrack("Joe Bloggs/Third Album/02:Second_track.ogg")
157 maketrack("Joe Bloggs/Third Album/03:Third_track.ogg")
158 maketrack("Joe Bloggs/Third Album/04:Fourth_track.ogg")
159 maketrack("Joe Bloggs/Third Album/05:Fifth_track.ogg")
160 maketrack("Fred Smith/Boring/01:Dull.ogg")
161 maketrack("Fred Smith/Boring/02:Tedious.ogg")
162 maketrack("Fred Smith/Boring/03:Drum Solo.ogg")
163 maketrack("Fred Smith/Boring/04:Yawn.ogg")
164 maketrack("misc/blahblahblah.ogg")
165 maketrack("Various/Greatest Hits/01:Jim Whatever - Spong.ogg")
166 maketrack("Various/Greatest Hits/02:Joe Bloggs - Yadda.ogg")
171 Return True iff UDP port P is bindable, else False"""
172 s
= socket
.socket(socket
.AF_INET
,
176 s
.bind(("127.0.0.1", p
))
182 def default_config(encoding
="UTF-8"):
183 """Write the default config"""
184 open("%s/config" % testroot
, "w").write(
186 collection fs %s %s/tracks
187 scratch %s/scratch.ogg
189 stopword 01 02 03 04 05 06 07 08 09 10
190 stopword 1 2 3 4 5 6 7 8 9
191 stopword 11 12 13 14 15 16 17 18 19 20
192 stopword 21 22 23 24 25 26 27 28 29 30
193 stopword the a an and to too in on of we i am as im for is
198 plugins %s/plugins/.libs
199 player *.mp3 execraw disorder-decode
200 player *.ogg execraw disorder-decode
201 player *.wav execraw disorder-decode
202 player *.flac execraw disorder-decode
203 tracklength *.mp3 disorder-tracklength
204 tracklength *.ogg disorder-tracklength
205 tracklength *.wav disorder-tracklength
206 tracklength *.flac disorder-tracklength
208 broadcast 127.0.0.1 %d
209 broadcast_from 127.0.0.1 %d
210 mail_sender no.such.user.sorry@greenend.org.uk
211 """ %
(homelink
, encoding
, testroot
, testroot
, top_builddir
, top_builddir
,
217 os
.makedirs(testroot
)
218 os
.makedirs("%s/home" % testroot
)
219 # Establish a symlink to the home directory, to keep the socket pathnames
222 for v
in ["TMPDIR", "TMP"]:
223 try: tmpdir
= os
.environ
[v
]
224 except KeyError: pass
226 for i
in xrange(1024):
227 r
= base64
.b64encode(os
.urandom(9)).replace("/", "_")
228 f
= "%s/disorder-home.%s" %
(tmpdir
, r
)
230 os
.symlink("%s/home" % testroot
, f
)
232 if e
.errno
!= errno
.EEXIST
: raise
237 fatal("failed to make home link")
240 port
= random
.randint(49152, 65530)
241 while not bindable(port
+ 1):
242 print "port %d is not bindable, trying another" %
(port
+ 1)
243 port
= random
.randint(49152, 65530)
244 # Log anything sent to that port
245 packetlog
= "%s/packetlog" % testroot
246 subprocess
.Popen(["disorder-udplog",
247 "--output", packetlog
,
248 "127.0.0.1", "%d" % port
])
249 # disorder-udplog will quit when its parent process terminates
250 copyfile("%s/sounds/scratch.ogg" % top_srcdir
,
251 "%s/scratch.ogg" % testroot
)
258 global daemon
, errs
, port
259 assert daemon
== None, "no daemon running"
260 if not bindable(port
+ 1):
261 print "waiting for port %d to become bindable again..." %
(port
+ 1)
263 while not bindable(port
+ 1):
265 print " starting daemon"
266 # remove the socket if it exists
267 socket
= "%s/socket" % homelink
268 if os
.path
.exists(socket
):
270 daemon
= subprocess
.Popen(["disorderd",
272 "--config", "%s/config" % testroot
],
274 # Wait for the socket to be created
276 sleep_resolution
= 0.125
277 while not os
.path
.exists(socket
):
280 print "FATAL: daemon failed to start up"
282 waited
+= sleep_resolution
283 if sleep_resolution
< 1:
284 sleep_resolution
*= 2
286 print " waiting for socket..."
288 print "FATAL: took too long for socket to appear"
290 time
.sleep(sleep_resolution
)
292 print " took about %ss for socket to appear" % waited
293 # Wait for root user to be created
294 command(["disorderd",
295 "--config", disorder
._configfile
,
298 def create_user(username
="fred", password
="fredpass"):
299 """create_user(USERNAME, PASSWORD)
301 Create a user, abusing direct database access to do so. Gives the
302 user rights 'all', allowing them to do anything."""
303 print " creating user %s" % username
305 "--config", disorder
._configfile
, "--no-per-user-config",
306 "--user", "root", "adduser", username
, password
])
308 "--config", disorder
._configfile
, "--no-per-user-config",
309 "--user", "root", "edituser", username
, "rights", "all"])
312 print " initiating rescan"
314 c
= disorder
.client()
316 print " rescan completed"
321 Stop the daemon if it has not stopped already"""
324 print " (daemon not running)"
328 print " stopping daemon"
329 os
.kill(daemon
.pid
, 15)
330 print " waiting for daemon"
332 print " daemon has stopped (rc=%d)" % rc
334 print " daemon already stopped"
337 def run(module
=None, report
=True):
340 Run the test in MODULE. This can be a string (in which case the module
341 will be imported) or a module object."""
342 global tests
, failures
344 # Locate the test module
346 # We're running a test stand-alone
349 name
= os
.path
.splitext(os
.path
.basename(sys
.argv
[0]))[0]
351 # We've been passed a module or a module name
352 if type(module
) == str:
353 module
= __import__(module
)
354 name
= module
.__name__
355 print "--- %s ---" % name
358 logfile
= "%s.log" % name
363 errs
= open(logfile
, "a")
364 # Ensure that disorder.py uses the test installation
365 disorder
._configfile
= "%s/config" % testroot
366 disorder
._userconf
= False
367 # Make config file etc
369 # Create some standard tracks
374 traceback
.print_exc(None, sys
.stderr
)
378 os
.system("ps -ef | grep disorderd")
389 Recursively delete directory D"""
390 if os
.path
.lexists(d
):
392 for dd
in os
.listdir(d
):
393 remove_dir("%s/%s" %
(d
, dd
))
398 def lists_have_same_contents(l1
, l2
):
399 """lists_have_same_contents(L1, L2)
401 Return True if L1 and L2 have equal members, in any order; else False."""
408 return map(nfc
, s1
) == map(nfc
, s2
)
410 def check_files(chatty
=True):
411 c
= disorder
.client()
413 for d
in dirs_by_dir
:
414 xdirs
= dirs_by_dir
[d
]
415 dirs
= c
.directories(d
)
416 if not lists_have_same_contents(xdirs
, dirs
):
419 print "directory: %s" % d
420 print "expected: %s" % xdirs
421 print "got: %s" % dirs
423 for d
in files_by_dir
:
424 xfiles
= files_by_dir
[d
]
426 if not lists_have_same_contents(xfiles
, files
):
429 print "directory: %s" % d
430 print "expected: %s" % xfiles
431 print "got: %s" % files
436 """Execute a command given as a list and return its stdout"""
437 p
= subprocess
.Popen(args
, stdout
=subprocess
.PIPE
)
438 lines
= p
.stdout
.readlines()
440 assert rc
== 0, ("%s returned status %s" %
(args
, rc
))
443 # -----------------------------------------------------------------------------
449 testroot
= "%s/tests/testroot/%s" % \
450 (top_builddir
, os
.path
.basename(sys
.argv
[0]))
451 tracks
= "%s/tracks" % testroot