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 # Parse the makefile in the current directory to identify the source directory
59 for l
in file("Makefile"):
60 r
= re
.match("top_srcdir *= *(.*)", l
)
62 top_srcdir
= r
.group(1)
65 fatal("cannot identify source directory")
67 # The tests source directory must be on the module search path already since
70 # -----------------------------------------------------------------------------
75 open(b
,"w").write(open(a
).read())
78 """Convert UTF-8 to unicode. A no-op if already unicode."""
79 if type(s
) == unicode:
82 return unicode(s
, "UTF-8")
85 """Convert UTF-8 string or unicode to NFC unicode."""
86 return unicodedata
.normalize("NFC", to_unicode(s
))
91 Make track with relative path S exist"""
92 trackpath
= "%s/%s" %
(tracks
, s
)
93 trackdir
= os
.path
.dirname(trackpath
)
94 if not os
.path
.exists(trackdir
):
96 copyfile("%s/sounds/long.ogg" % top_srcdir
, trackpath
)
97 # We record the tracks we created so they can be tested against
98 # server responses. We put them into NFC since that's what the server
100 bits
= nfc(s
).split('/')
102 for d
in bits
[0:-1]:
103 dd
= "%s/%s" %
(dp
, d
)
104 if dp
not in dirs_by_dir
:
106 if dd
not in dirs_by_dir
[dp
]:
107 dirs_by_dir
[dp
].append(dd
)
108 dp
= "%s/%s" %
(dp
, d
)
109 if dp
not in files_by_dir
:
110 files_by_dir
[dp
] = []
111 files_by_dir
[dp
].append("%s/%s" %
(dp
, bits
[-1]))
114 # We create some tracks with non-ASCII characters in the name and
115 # we (currently) force UTF-8.
117 # On a traditional UNIX filesystem, that treats filenames as byte strings
118 # with special significant for '/', this should just work, though the
119 # names will look wrong to ls(1) in a non UTF-8 locale.
121 # On Apple HFS+ filenames normalized to a decomposed form that isn't quite
122 # NFD, so our attempts to have both normalized and denormalized filenames
123 # is frustrated. Provided we test on traditional filesytsems too this
124 # shouldn't be a problem.
125 # (See http://developer.apple.com/qa/qa2001/qa1173.html)
127 global dirs_by_dir
, files_by_dir
131 # C3 8C = 00CC LATIN CAPITAL LETTER I WITH GRAVE
133 maketrack("Joe Bloggs/First Album/01:F\xC3\x8Crst track.ogg")
135 maketrack("Joe Bloggs/First Album/02:Second track.ogg")
137 # CC 81 = 0301 COMBINING ACUTE ACCENT
138 # (giving an NFD i-acute)
139 maketrack("Joe Bloggs/First Album/03:ThI\xCC\x81rd track.ogg")
140 # ...hopefuly giving C3 8D = 00CD LATIN CAPITAL LETTER I WITH ACUTE
141 maketrack("Joe Bloggs/First Album/04:Fourth track.ogg")
142 maketrack("Joe Bloggs/First Album/05:Fifth track.ogg")
143 maketrack("Joe Bloggs/Second Album/01:First track.ogg")
144 maketrack("Joe Bloggs/Second Album/02:Second track.ogg")
145 maketrack("Joe Bloggs/Second Album/03:Third track.ogg")
146 maketrack("Joe Bloggs/Second Album/04:Fourth track.ogg")
147 maketrack("Joe Bloggs/Second Album/05:Fifth track.ogg")
148 maketrack("Joe Bloggs/Third Album/01:First_track.ogg")
149 maketrack("Joe Bloggs/Third Album/02:Second_track.ogg")
150 maketrack("Joe Bloggs/Third Album/03:Third_track.ogg")
151 maketrack("Joe Bloggs/Third Album/04:Fourth_track.ogg")
152 maketrack("Joe Bloggs/Third Album/05:Fifth_track.ogg")
153 maketrack("Fred Smith/Boring/01:Dull.ogg")
154 maketrack("Fred Smith/Boring/02:Tedious.ogg")
155 maketrack("Fred Smith/Boring/03:Drum Solo.ogg")
156 maketrack("Fred Smith/Boring/04:Yawn.ogg")
157 maketrack("misc/blahblahblah.ogg")
158 maketrack("Various/Greatest Hits/01:Jim Whatever - Spong.ogg")
159 maketrack("Various/Greatest Hits/02:Joe Bloggs - Yadda.ogg")
164 Return True iff UDP port P is bindable, else False"""
165 s
= socket
.socket(socket
.AF_INET
,
169 s
.bind(("127.0.0.1", p
))
175 def default_config(encoding
="UTF-8"):
176 """Write the default config"""
177 open("%s/config" % testroot
, "w").write(
179 collection fs %s %s/tracks
180 scratch %s/scratch.ogg
182 stopword 01 02 03 04 05 06 07 08 09 10
183 stopword 1 2 3 4 5 6 7 8 9
184 stopword 11 12 13 14 15 16 17 18 19 20
185 stopword 21 22 23 24 25 26 27 28 29 30
186 stopword the a an and to too in on of we i am as im for is
191 plugins %s/plugins/.libs
192 player *.mp3 execraw disorder-decode
193 player *.ogg execraw disorder-decode
194 player *.wav execraw disorder-decode
195 player *.flac execraw disorder-decode
196 tracklength *.mp3 disorder-tracklength
197 tracklength *.ogg disorder-tracklength
198 tracklength *.wav disorder-tracklength
199 tracklength *.flac disorder-tracklength
201 broadcast 127.0.0.1 %d
202 broadcast_from 127.0.0.1 %d
203 mail_sender no.such.user.sorry@greenend.org.uk
204 """ %
(homelink
, encoding
, testroot
, testroot
, top_builddir
, top_builddir
,
210 os
.makedirs(testroot
)
211 os
.makedirs("%s/home" % testroot
)
212 # Establish a symlink to the home directory, to keep the socket pathnames
215 for v
in ["TMPDIR", "TMP"]:
216 try: tmpdir
= os
.environ
[v
]
217 except KeyError: pass
219 for i
in xrange(1024):
220 r
= base64
.b64encode(os
.urandom(9)).replace("/", "_")
221 f
= "%s/disorder-home.%s" %
(tmpdir
, r
)
223 os
.symlink("%s/home" % testroot
, f
)
225 if e
.errno
!= errno
.EEXIST
: raise
230 fatal("failed to make home link")
233 port
= random
.randint(49152, 65530)
234 while not bindable(port
+ 1):
235 print "port %d is not bindable, trying another" %
(port
+ 1)
236 port
= random
.randint(49152, 65530)
237 # Log anything sent to that port
238 packetlog
= "%s/packetlog" % testroot
239 subprocess
.Popen(["disorder-udplog",
240 "--output", packetlog
,
241 "127.0.0.1", "%d" % port
])
242 # disorder-udplog will quit when its parent process terminates
243 copyfile("%s/sounds/scratch.ogg" % top_srcdir
,
244 "%s/scratch.ogg" % testroot
)
251 global daemon
, errs
, port
252 assert daemon
== None, "no daemon running"
253 if not bindable(port
+ 1):
254 print "waiting for port %d to become bindable again..." %
(port
+ 1)
256 while not bindable(port
+ 1):
258 print " starting daemon"
259 # remove the socket if it exists
260 socket
= "%s/socket" % homelink
261 if os
.path
.exists(socket
):
263 daemon
= subprocess
.Popen(["disorderd",
265 "--config", "%s/config" % testroot
],
267 # Wait for the socket to be created
269 sleep_resolution
= 0.125
270 while not os
.path
.exists(socket
):
273 print "FATAL: daemon failed to start up"
275 waited
+= sleep_resolution
276 if sleep_resolution
< 1:
277 sleep_resolution
*= 2
279 print " waiting for socket..."
281 print "FATAL: took too long for socket to appear"
283 time
.sleep(sleep_resolution
)
285 print " took about %ss for socket to appear" % waited
286 # Wait for root user to be created
287 command(["disorderd",
288 "--config", disorder
._configfile
,
291 def create_user(username
="fred", password
="fredpass"):
292 """create_user(USERNAME, PASSWORD)
294 Create a user, abusing direct database access to do so. Gives the
295 user rights 'all', allowing them to do anything."""
296 print " creating user %s" % username
298 "--config", disorder
._configfile
, "--no-per-user-config",
299 "--user", "root", "adduser", username
, password
])
301 "--config", disorder
._configfile
, "--no-per-user-config",
302 "--user", "root", "edituser", username
, "rights", "all"])
305 print " initiating rescan"
307 c
= disorder
.client()
309 print " rescan completed"
314 Stop the daemon if it has not stopped already"""
317 print " (daemon not running)"
321 print " stopping daemon"
322 os
.kill(daemon
.pid
, 15)
323 print " waiting for daemon"
325 print " daemon has stopped (rc=%d)" % rc
327 print " daemon already stopped"
330 def run(module
=None, report
=True):
333 Run the test in MODULE. This can be a string (in which case the module
334 will be imported) or a module object."""
335 global tests
, failures
337 # Locate the test module
339 # We're running a test stand-alone
342 name
= os
.path
.splitext(os
.path
.basename(sys
.argv
[0]))[0]
344 # We've been passed a module or a module name
345 if type(module
) == str:
346 module
= __import__(module
)
347 name
= module
.__name__
348 print "--- %s ---" % name
351 logfile
= "%s.log" % name
356 errs
= open(logfile
, "a")
357 # Ensure that disorder.py uses the test installation
358 disorder
._configfile
= "%s/config" % testroot
359 disorder
._userconf
= False
360 # Make config file etc
362 # Create some standard tracks
367 traceback
.print_exc(None, sys
.stderr
)
371 os
.system("ps -ef | grep disorderd")
382 Recursively delete directory D"""
383 if os
.path
.lexists(d
):
385 for dd
in os
.listdir(d
):
386 remove_dir("%s/%s" %
(d
, dd
))
391 def lists_have_same_contents(l1
, l2
):
392 """lists_have_same_contents(L1, L2)
394 Return True if L1 and L2 have equal members, in any order; else False."""
401 return map(nfc
, s1
) == map(nfc
, s2
)
403 def check_files(chatty
=True):
404 c
= disorder
.client()
406 for d
in dirs_by_dir
:
407 xdirs
= dirs_by_dir
[d
]
408 dirs
= c
.directories(d
)
409 if not lists_have_same_contents(xdirs
, dirs
):
412 print "directory: %s" % d
413 print "expected: %s" % xdirs
414 print "got: %s" % dirs
416 for d
in files_by_dir
:
417 xfiles
= files_by_dir
[d
]
419 if not lists_have_same_contents(xfiles
, files
):
422 print "directory: %s" % d
423 print "expected: %s" % xfiles
424 print "got: %s" % files
429 """Execute a command given as a list and return its stdout"""
430 p
= subprocess
.Popen(args
, stdout
=subprocess
.PIPE
)
431 lines
= p
.stdout
.readlines()
433 assert rc
== 0, ("%s returned status %s" %
(args
, rc
))
436 # -----------------------------------------------------------------------------
442 testroot
= "%s/tests/testroot/%s" % \
443 (top_builddir
, os
.path
.basename(sys
.argv
[0]))
444 tracks
= "%s/tracks" % testroot