Merge lp:~yolanda.robla/nut/dep-8-tests into lp:ubuntu/saucy/nut

Proposed by Yolanda Robla
Status: Work in progress
Proposed branch: lp:~yolanda.robla/nut/dep-8-tests
Merge into: lp:ubuntu/saucy/nut
Diff against target: 1621 lines (+1582/-0)
6 files modified
debian/changelog (+6/-0)
debian/control (+1/-0)
debian/tests/control (+3/-0)
debian/tests/nut (+6/-0)
debian/tests/test-nut.py (+422/-0)
debian/tests/testlib.py (+1144/-0)
To merge this branch: bzr merge lp:~yolanda.robla/nut/dep-8-tests
Reviewer Review Type Date Requested Status
Martin Pitt Needs Fixing
James Hunt (community) Approve
Daniel Holbach (community) Needs Fixing
Jean-Baptiste Lallement (community) Approve
Review via email: mp+161414@code.launchpad.net
To post a comment you must log in.
lp:~yolanda.robla/nut/dep-8-tests updated
42. By Yolanda Robla

fixing test, no need to install packages

Revision history for this message
Jean-Baptiste Lallement (jibel) wrote :

Thanks for your work. Reviewed and tested on the QA infrastructure and it works fine.

On a side note, if several tests from the QRT are going to be ported to DEP8 it would make sense to package testlib separately to avoid code redundancy and make maintenance easier.

review: Approve
Revision history for this message
Daniel Holbach (dholbach) wrote :

Could you add a changelog entry in debian/changelog for the upload? It might also be a good idea to forward the change to Debian.

review: Needs Fixing
lp:~yolanda.robla/nut/dep-8-tests updated
43. By Yolanda Robla

d/tests: added dep-8-tests

Revision history for this message
Yolanda Robla (yolanda.robla) wrote :

recheck

Revision history for this message
James Hunt (jamesodhunt) wrote :

LGTM. Has this been submitted to Debian yet?

Revision history for this message
Yolanda Robla (yolanda.robla) wrote :

No, nothing submitted, i was waiting for the MP to be approved.

Revision history for this message
James Hunt (jamesodhunt) wrote :

Ok :)

review: Approve
Revision history for this message
Jamie Strandboge (jdstrand) wrote :

FYI, this has now diverged from QRT. I fixed it in a different way when /etc/init.d/nut moved to /etc/init.d/nut-server (and client).

Revision history for this message
Yolanda Robla (yolanda.robla) wrote :

There are problems with nut branch, it's giving an error:http://package-import.ubuntu.com/status/nut.html#2012-07-04%2020:31:15.037950

Changes have been submitted to debian manually, so they can be picked up when they are accepted.

Revision history for this message
Martin Pitt (pitti) wrote :

I ran this on current saucy (run-adt-test -sS lp:~yolanda.robla/nut/dep-8-tests nut) and got a failure:

adt-run: trace: & ubtree0t-nut: - - - - - - - - - - results - - - - - - - - - -
ubtree0t-nut FAIL non-zero exit status 1
adt-run: trace: & ubtree0t-nut: - - - - - - - - - - stdout - - - - - - - - - -
test_CVE_2012_2944 (__main__.BasicTest)
Test CVE-2012-2944 ... FAIL
test_daemons_pid (__main__.BasicTest)
Test daemons using PID files ... ok
test_daemons_service (__main__.BasicTest)
Test daemons using "service status" ... ok
test_upsc_device_list (__main__.BasicTest)
Test NUT client interface (upsc): device(s) listing ... ok
test_upsd_IPv4 (__main__.BasicTest)
Test upsd IPv4 reachability ... ok
test_upsd_IPv6 (__main__.BasicTest)
Test upsd IPv6 reachability ... ok
test_upsmon_notif (__main__.BasicTest)
Test upsmon notifications ... ok
test_upsrw (__main__.BasicTest)
Test upsrw ... ok

======================================================================
FAIL: test_CVE_2012_2944 (__main__.BasicTest)
Test CVE-2012-2944
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/tmp.K0RNk6YJAb/ubtree0-ubtree/debian/tests/test-nut.py", line 396, in test_CVE_2012_2944
    self.assertFalse(os.path.exists(pidfile), "Found %s" % pidfile)
AssertionError: Found /var/run/nut/upsd.pid

----------------------------------------------------------------------
Ran 8 tests in 62.702s

May this be related to what Jamie said in above comment?

Please set back to "needs review" once you are done, setting to WIP now to make it disappear from the sponsoring queue.

Thanks!

review: Needs Fixing

Unmerged revisions

43. By Yolanda Robla

d/tests: added dep-8-tests

42. By Yolanda Robla

fixing test, no need to install packages

41. By Yolanda Robla

adding dep-8 tests

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/changelog'
2--- debian/changelog 2009-12-18 14:04:47 +0000
3+++ debian/changelog 2013-05-07 21:46:24 +0000
4@@ -1,3 +1,9 @@
5+nut (2.4.1-3.2ubuntu2) saucy; urgency=low
6+
7+ * d/tests: added dep-8-tests
8+
9+ -- Yolanda <yolanda.robla@canonical.com> Tue, 07 May 2013 23:42:31 +0200
10+
11 nut (2.4.1-3.2ubuntu1) lucid; urgency=low
12
13 * Resynchronize with debian, remaining changes:
14
15=== modified file 'debian/control'
16--- debian/control 2009-11-06 01:34:44 +0000
17+++ debian/control 2013-05-07 21:46:24 +0000
18@@ -9,6 +9,7 @@
19 Homepage: http://www.networkupstools.org
20 Vcs-Browser: http://svn.debian.org/wsvn/nut
21 Vcs-Svn: svn://svn.debian.org/nut/trunk
22+XS-Testsuite: autopkgtest
23
24 Package: nut
25 Architecture: any
26
27=== added directory 'debian/tests'
28=== added file 'debian/tests/control'
29--- debian/tests/control 1970-01-01 00:00:00 +0000
30+++ debian/tests/control 2013-05-07 21:46:24 +0000
31@@ -0,0 +1,3 @@
32+Tests: nut
33+Depends: python-unit, nut-server, nut-client
34+Restrictions: needs-root
35
36=== added file 'debian/tests/nut'
37--- debian/tests/nut 1970-01-01 00:00:00 +0000
38+++ debian/tests/nut 2013-05-07 21:46:24 +0000
39@@ -0,0 +1,6 @@
40+#!/bin/bash
41+#------------
42+# Testing nut
43+#------------
44+set -e
45+python `dirname $0`/test-nut.py 2>&1
46
47=== added file 'debian/tests/test-nut.py'
48--- debian/tests/test-nut.py 1970-01-01 00:00:00 +0000
49+++ debian/tests/test-nut.py 2013-05-07 21:46:24 +0000
50@@ -0,0 +1,422 @@
51+#!/usr/bin/python
52+#
53+# test-nut.py quality assurance test script
54+# Copyright (C) 2008-2011 Arnaud Quette <aquette@debian.org>
55+# Copyright (C) 2012 Jamie Strandboge <jamie@canonical.com>
56+#
57+# This program is free software: you can redistribute it and/or modify
58+# it under the terms of the GNU General Public License version 3,
59+# as published by the Free Software Foundation.
60+#
61+# This program is distributed in the hope that it will be useful,
62+# but WITHOUT ANY WARRANTY; without even the implied warranty of
63+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
64+# GNU General Public License for more details.
65+#
66+# You should have received a copy of the GNU General Public License
67+# along with this program. If not, see <http://www.gnu.org/licenses/>.
68+#
69+
70+'''
71+ *** IMPORTANT ***
72+ DO NOT RUN ON A PRODUCTION SERVER.
73+ *** IMPORTANT ***
74+
75+ How to run (up to natty):
76+ $ sudo apt-get -y install python-unit nut
77+ $ sudo ./test-nut.py -v
78+
79+ How to run (oneiric+):
80+ $ sudo apt-get -y install python-unit nut-server nut-client
81+ $ sudo ./test-nut.py -v
82+
83+ NOTE:
84+ - NUT architecture (helps understanding):
85+ http://www.networkupstools.org/docs/developer-guide.chunked/ar01s02.html#_the_layering
86+
87+ - These tests only validate the NUT software framework itself (communication
88+ between the drivers, server and client layers ; events propagation and
89+ detection). The critical part of NUT, Ie the driver layer which
90+ communicate with actual devices, can only be tested with real hardware!
91+
92+ - These tests use the NUT simulation driver (dummy-ups) to emulate real
93+ hardware behavior, and generate events (power failure, low battery, ...).
94+
95+ TODO:
96+ - improve test duration, by reworking NutTestCommon._setUp() and the way
97+ daemons are started (ie, always)
98+ - more events testing (upsmon / upssched)
99+ - test syslog and wall output
100+ - test UPS redundancy
101+ - test Powerchain (once available!)
102+ - test AppArmor (once available!)
103+ - add hardware testing as Private tests?
104+ - load a .dev file, and test a full output
105+
106+ QA INFORMATION:
107+ - NUT provides "make check" and "make distcheck" in its source distribution
108+ - NUT provides Quality Assurance information, to track all efforts:
109+ http://www.networkupstools.org/nut-qa.html
110+'''
111+
112+# QRT-Packages: python-unit netcat-openbsd
113+# QRT-Alternates: nut-server nut
114+# QRT-Alternates: nut-client nut
115+# nut-dev is needed for the dummy driver on hardy
116+# QRT-Alternates: nut-dev
117+# QRT-Privilege: root
118+# QRT-Depends:
119+
120+
121+import unittest, subprocess, sys, os, time
122+import tempfile
123+import testlib
124+
125+use_private = True
126+try:
127+ from private.qrt.nut import PrivateNutTest
128+except ImportError:
129+ class PrivateNutTest(object):
130+ '''Empty class'''
131+ print >>sys.stdout, "Skipping private tests"
132+
133+
134+class NutTestCommon(testlib.TestlibCase):
135+ '''Common functions'''
136+
137+ # FIXME: initscript will be splitted into nut-server and nut-client
138+ # (Debian bug #634858)
139+ initscript = "/etc/init.d/nut-server"
140+ hosts_file = "/etc/hosts"
141+ powerdownflag = "/etc/killpower"
142+ shutdowncmd = "/tmp/shutdowncmd"
143+ notifyscript = "/tmp/nutifyme"
144+ notifylog = "/tmp/notify.log"
145+
146+ def _setUp(self):
147+ '''Set up prior to each test_* function'''
148+ '''We generate a NUT config using the dummmy-ups driver
149+ and standard settings for local monitoring
150+ '''
151+ self.tmpdir = ""
152+ self.rundir = "/var/run/nut"
153+ testlib.cmd(['/bin/rm -f' + self.powerdownflag])
154+
155+ testlib.config_replace('/etc/nut/ups.conf', '''
156+[dummy-dev1]
157+ driver = dummy-ups
158+ port = dummy.dev
159+ desc = "simulation device"
160+ ''')
161+
162+ if self.lsb_release['Release'] <= 8.04:
163+ testlib.config_replace('/etc/nut/upsd.conf', '''
164+ACL dummy-net 127.0.0.1/8
165+ACL dummy-net2 ::1/64
166+ACL all 0.0.0.0/0
167+ACCEPT dummy-net dummy-net2
168+REJECT all
169+ ''')
170+ else:
171+ testlib.config_replace('/etc/nut/upsd.conf', '''# just to touch the file''')
172+
173+ extra_cfgs = ''
174+ if self.lsb_release['Release'] <= 8.04:
175+ extra_cfgs = ''' allowfrom = dummy-net dummy-net2
176+'''
177+ testlib.config_replace('/etc/nut/upsd.users', '''
178+[admin]
179+ password = dummypass
180+ actions = SET
181+ instcmds = ALL
182+%s
183+[monuser]
184+ password = dummypass
185+ upsmon master
186+%s ''' %(extra_cfgs, extra_cfgs))
187+
188+ testlib.config_replace('/etc/nut/upsmon.conf', '''
189+MONITOR dummy-dev1@localhost 1 monuser dummy-pass master
190+MINSUPPLIES 1
191+SHUTDOWNCMD "/usr/bin/touch ''' + self.shutdowncmd + '"\n'
192+'''POWERDOWNFLAG ''' + self.powerdownflag + '\n'
193+'''
194+NOTIFYCMD ''' + self.notifyscript + '\n'
195+'''
196+NOTIFYFLAG ONLINE SYSLOG+EXEC
197+NOTIFYFLAG ONBATT SYSLOG+EXEC
198+NOTIFYFLAG LOWBATT SYSLOG+EXEC
199+NOTIFYFLAG FSD SYSLOG+EXEC
200+# NOTIFYFLAG COMMOK SYSLOG+EXEC
201+# NOTIFYFLAG COMMBAD SYSLOG+EXEC
202+NOTIFYFLAG SHUTDOWN SYSLOG+EXEC
203+# NOTIFYFLAG REPLBATT SYSLOG+EXEC
204+# NOTIFYFLAG NOCOMM SYSLOG+EXEC
205+# NOTIFYFLAG NOPARENT SYSLOG+EXEC
206+
207+# Shorten test duration by:
208+# Speeding up polling frequency
209+POLLFREQ 2
210+# And final wait delay
211+FINALDELAY 0
212+'''
213+)
214+
215+ testlib.create_fill(self.notifyscript, '''
216+#! /bin/bash
217+echo "$*" > ''' + self.notifylog + '\n', mode=0755)
218+
219+ # dummy-ups absolutely needs a data file, even if empty
220+ testlib.config_replace('/etc/nut/dummy.dev', '''
221+ups.mfr: Dummy Manufacturer
222+ups.model: Dummy UPS
223+ups.status: OL
224+# Set a big enough timer to avoid value reset, due to reading loop
225+TIMER 600
226+ ''')
227+
228+ testlib.config_replace('/etc/nut/nut.conf', '''MODE=standalone''')
229+
230+ # Add known friendly IP names for localhost v4 and v6
231+ # FIXME: find a way to determine if v4 / v6 are enabled, and a way to
232+ # get v4 / v6 names
233+ testlib.config_replace(self.hosts_file, '''#
234+127.0.0.1 localv4
235+::1 localv6
236+''', append=True)
237+
238+ if self.lsb_release['Release'] <= 8.04:
239+ testlib.config_replace('/etc/default/nut', '''#
240+START_UPSD=yes
241+UPSD_OPTIONS=""
242+START_UPSMON=yes
243+UPSMON_OPTIONS=""
244+''', append=False)
245+
246+ # Start the framework
247+ self._restart()
248+
249+ def _tearDown(self):
250+ '''Clean up after each test_* function'''
251+ self._stop()
252+ time.sleep(2)
253+ os.unlink('/etc/nut/ups.conf')
254+ os.unlink('/etc/nut/upsd.conf')
255+ os.unlink('/etc/nut/upsd.users')
256+ os.unlink('/etc/nut/upsmon.conf')
257+ os.unlink('/etc/nut/dummy.dev')
258+ os.unlink('/etc/nut/nut.conf')
259+ testlib.config_restore('/etc/nut/ups.conf')
260+ testlib.config_restore('/etc/nut/upsd.conf')
261+ testlib.config_restore('/etc/nut/upsd.users')
262+ testlib.config_restore('/etc/nut/upsmon.conf')
263+ testlib.config_restore('/etc/nut/dummy.dev')
264+ testlib.config_restore('/etc/nut/nut.conf')
265+ if os.path.exists(self.notifyscript):
266+ os.unlink(self.notifyscript)
267+ if os.path.exists(self.shutdowncmd):
268+ os.unlink(self.shutdowncmd)
269+ testlib.config_restore(self.hosts_file)
270+ if self.lsb_release['Release'] <= 8.04:
271+ testlib.config_restore('/etc/default/nut')
272+
273+ if os.path.exists(self.tmpdir):
274+ testlib.recursive_rm(self.tmpdir)
275+
276+ # this is needed because of the potentially hung upsd process in the
277+ # CVE-2012-2944 test
278+ testlib.cmd(['killall', 'upsd'])
279+ testlib.cmd(['killall', '-9', 'upsd'])
280+
281+ def _start(self):
282+ '''Start NUT'''
283+ rc, report = testlib.cmd([self.initscript, 'start'])
284+ expected = 0
285+ result = 'Got exit code %d, expected %d\n' % (rc, expected)
286+ self.assertEquals(expected, rc, result + report)
287+ time.sleep(2)
288+
289+ def _stop(self):
290+ '''Stop NUT'''
291+ rc, report = testlib.cmd([self.initscript, 'stop'])
292+ expected = 0
293+ result = 'Got exit code %d, expected %d\n' % (rc, expected)
294+ self.assertEquals(expected, rc, result + report)
295+
296+ def _reload(self):
297+ '''Reload NUT'''
298+ rc, report = testlib.cmd([self.initscript, 'force-reload'])
299+ expected = 0
300+ result = 'Got exit code %d, expected %d\n' % (rc, expected)
301+ self.assertEquals(expected, rc, result + report)
302+
303+ def _restart(self):
304+ '''Restart NUT'''
305+ self._stop()
306+ time.sleep(2)
307+ self._start()
308+
309+ def _status(self):
310+ '''NUT Status'''
311+ rc, report = testlib.cmd([self.initscript, 'status'])
312+ expected = 0
313+ if self.lsb_release['Release'] <= 8.04:
314+ self._skipped("init script does not support status command")
315+ expected = 1
316+ result = 'Got exit code %d, expected %d\n' % (rc, expected)
317+ self.assertEquals(expected, rc, result + report)
318+
319+ def _testDaemons(self, daemons):
320+ '''Daemons running'''
321+ for d in daemons:
322+ # A note on the driver pid file: its name is
323+ # <ups.conf section name>-<driver name>.pid
324+ # ex: dummy-dev1-dummy-ups.pid
325+ if d == 'dummy-ups' :
326+ pidfile = os.path.join(self.rundir, 'dummy-ups-dummy-dev1.pid')
327+ else :
328+ pidfile = os.path.join(self.rundir, d + '.pid')
329+ warning = "Could not find pidfile '" + pidfile + "'"
330+ self.assertTrue(os.path.exists(pidfile), warning)
331+ self.assertTrue(testlib.check_pidfile(d, pidfile), d + ' is not running')
332+
333+ def _nut_setvar(self, var, value):
334+ '''Test upsrw'''
335+ rc, report = testlib.cmd(['/bin/upsrw', '-s', var + '=' + value,
336+ '-u', 'admin' , '-p', 'dummypass', 'dummy-dev1@localhost'])
337+ self.assertTrue(rc == 0, 'upsrw: ' + report)
338+ return rc,report
339+
340+
341+class BasicTest(NutTestCommon, PrivateNutTest):
342+ '''Test basic NUT functionalities'''
343+
344+ def setUp(self):
345+ '''Setup mechanisms'''
346+ NutTestCommon._setUp(self)
347+
348+ def tearDown(self):
349+ '''Shutdown methods'''
350+ NutTestCommon._tearDown(self)
351+
352+ def test_daemons_service(self):
353+ '''Test daemons using "service status"'''
354+ self._status()
355+
356+ def test_daemons_pid(self):
357+ '''Test daemons using PID files'''
358+ # upsmon does not work because ups-client is still missing
359+ daemons = [ 'dummy-ups', 'upsd']
360+ self._testDaemons(daemons)
361+
362+ def test_upsd_IPv4(self):
363+ '''Test upsd IPv4 reachability'''
364+ rc, report = testlib.cmd(['/bin/upsc', '-l', 'localv4'])
365+ self.assertTrue('dummy-dev1' in report, 'dummy-dev1 should be present in device(s) listing: ' + report)
366+
367+ def test_upsd_IPv6(self):
368+ '''Test upsd IPv6 reachability'''
369+ rc, report = testlib.cmd(['/bin/upsc', '-l', 'localv6'])
370+ self.assertTrue('dummy-dev1' in report, 'dummy-dev1 should be present in device(s) listing: ' + report)
371+
372+ def test_upsc_device_list(self):
373+ '''Test NUT client interface (upsc): device(s) listing'''
374+ rc, report = testlib.cmd(['/bin/upsc', '-L'])
375+ self.assertTrue('dummy-dev1: simulation device' in report, 'dummy-dev1 should be present in device(s) listing: ' + report)
376+
377+ def _test_upsc_status(self):
378+ '''Test NUT client interface (upsc): data access'''
379+ rc, report = testlib.cmd(['/bin/upsc', 'dummy-dev1', 'ups.status'])
380+ self.assertTrue('OL' in report, 'UPS Status: ' + report + 'should be OL')
381+
382+ #def test_upsc_powerchain(self):
383+ # '''Test NUT client interface (upsc): Powerchain(s) listing'''
384+ # rc, report = testlib.cmd(['/bin/upsc', '-p'])
385+ # Result == Main ; dummy-dev1 ; $hostname
386+ # self.assertTrue('dummy-dev1' in report, 'dummy-dev1 should be present in device(s) listing: ' + report)
387+
388+ def test_upsrw(self):
389+ '''Test upsrw'''
390+ # Set ups.status to OB (On Battery)...
391+ self._nut_setvar('ups.model', 'Test')
392+ time.sleep(2)
393+ # and check the result on the client side
394+ rc, report = testlib.cmd(['/bin/upsc', 'dummy-dev1@localhost', 'ups.model'])
395+ self.assertTrue('Test' in report, 'UPS Model: ' + report + 'should be Test')
396+
397+ # FIXME: need a simulation counterpart, not yet implemented
398+ #def test_upscmd(self):
399+ # '''Test upscmd'''
400+
401+ def test_upsmon_notif(self):
402+ '''Test upsmon notifications'''
403+ # Set ups.status to OB (On Battery)...
404+ self._nut_setvar('ups.status', 'OB')
405+ time.sleep(1)
406+ # and check the result on the client side
407+ rc, report = testlib.cmd(['/bin/upsc', 'dummy-dev1@localhost', 'ups.status'])
408+ self.assertTrue('OB' in report, 'UPS Status: ' + report + 'should be OB')
409+
410+ #def test_upsmon_shutdown(self):
411+ # '''Test upsmon basic shutdown (single UPS, low battery status)'''
412+ # self._nut_setvar('ups.status', 'OB LB')
413+ # time.sleep(2)
414+ # # and check the result on the client side
415+ # rc, report = testlib.cmd(['/bin/upsc', 'dummy-dev1@localhost', 'ups.status'])
416+ # self.assertTrue('OB LB' in report, 'UPS Status: ' + report + 'should be OB LB')
417+ # # FIXME: improve with a 2 sec loop * 5 tries
418+ # time.sleep(3)
419+ # # Check for powerdownflag and shutdowncmd (needed for halt!)
420+ # # FIXME: replace by a call to 'upsmon -K'
421+ # self.assertTrue(os.path.exists(self.powerdownflag), 'POWERDOWNFLAG has not been set!')
422+ # self.assertTrue(os.path.exists(self.shutdowncmd), 'SHUTDOWNCMD has not been executed!')
423+
424+ def test_CVE_2012_2944(self):
425+ '''Test CVE-2012-2944'''
426+ self.tmpdir = tempfile.mkdtemp(dir='/tmp', prefix="testlib-")
427+ # First send bad input. We need to do this in a script because python
428+ # functions don't like our embedded NULs
429+ script = os.path.join(self.tmpdir, 'script.sh')
430+ contents = '''#!/bin/sh
431+printf '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\n' | nc -q 1 127.0.0.1 3493
432+sleep 1
433+dd if=/dev/urandom count=64 | nc -q 1 127.0.0.1 3493
434+'''
435+ testlib.create_fill(script, contents, mode=0755)
436+ rc, report = testlib.cmd([script])
437+
438+ # It should not have crashed. Let's see if it did
439+ self._testDaemons(['upsd'])
440+ self.assertTrue('ERR UNKNOWN-COMMAND' in report, "Could not find 'ERR UNKNOWN-COMMAND' in:\n%s" % report)
441+
442+ # This CVE may also result in a hung upsd. Try to kill it, if it is
443+ # still around, it is hung
444+ testlib.cmd(['killall', 'upsd'])
445+ pidfile = os.path.join(self.rundir, 'upsd.pid')
446+ self.assertFalse(os.path.exists(pidfile), "Found %s" % pidfile)
447+ self.assertFalse(testlib.check_pidfile('upsd', pidfile), 'upsd is hung')
448+ #subprocess.call(['bash'])
449+
450+# FIXME
451+#class AdvancedTest(NutTestCommon, PrivateNutTest):
452+# '''Test advanced NUT functionalities'''
453+
454+if __name__ == '__main__':
455+
456+ suite = unittest.TestSuite()
457+ # more configurable
458+ if (len(sys.argv) == 1 or sys.argv[1] == '-v'):
459+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(BasicTest))
460+
461+ # Pull in private tests
462+ #if use_private:
463+ # suite.addTest(unittest.TestLoader().loadTestsFromTestCase(MyPrivateTest))
464+
465+ else:
466+ print '''Usage:
467+ test-nut.py [-v] basic tests
468+'''
469+ sys.exit(1)
470+ rc = unittest.TextTestRunner(verbosity=2).run(suite)
471+ if not rc.wasSuccessful():
472+ sys.exit(1)
473
474=== added file 'debian/tests/testlib.py'
475--- debian/tests/testlib.py 1970-01-01 00:00:00 +0000
476+++ debian/tests/testlib.py 2013-05-07 21:46:24 +0000
477@@ -0,0 +1,1144 @@
478+#
479+# testlib.py quality assurance test script
480+# Copyright (C) 2008-2011 Canonical Ltd.
481+#
482+# This library is free software; you can redistribute it and/or
483+# modify it under the terms of the GNU Library General Public
484+# License as published by the Free Software Foundation; either
485+# version 2 of the License.
486+#
487+# This library is distributed in the hope that it will be useful,
488+# but WITHOUT ANY WARRANTY; without even the implied warranty of
489+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
490+# Library General Public License for more details.
491+#
492+# You should have received a copy of the GNU Library General Public
493+# License along with this program. If not, see
494+# <http://www.gnu.org/licenses/>.
495+#
496+
497+'''Common classes and functions for package tests.'''
498+
499+import string, random, crypt, subprocess, pwd, grp, signal, time, unittest, tempfile, shutil, os, os.path, re, glob
500+import sys, socket, gzip
501+from stat import *
502+from encodings import string_escape
503+
504+import warnings
505+warnings.filterwarnings('ignore', message=r'.*apt_pkg\.TagFile.*', category=DeprecationWarning)
506+try:
507+ import apt_pkg
508+ apt_pkg.InitSystem();
509+except:
510+ # On non-Debian system, fall back to simple comparison without debianisms
511+ class apt_pkg(object):
512+ def VersionCompare(one, two):
513+ list_one = one.split('.')
514+ list_two = two.split('.')
515+ while len(list_one)>0 and len(list_two)>0:
516+ if list_one[0] > list_two[0]:
517+ return 1
518+ if list_one[0] < list_two[0]:
519+ return -1
520+ list_one.pop(0)
521+ list_two.pop(0)
522+ return 0
523+
524+bogus_nxdomain = "208.69.32.132"
525+
526+# http://www.chiark.greenend.org.uk/ucgi/~cjwatson/blosxom/2009-07-02-python-sigpipe.html
527+# This is needed so that the subprocesses that produce endless output
528+# actually quit when the reader goes away.
529+import signal
530+def subprocess_setup():
531+ # Python installs a SIGPIPE handler by default. This is usually not what
532+ # non-Python subprocesses expect.
533+ signal.signal(signal.SIGPIPE, signal.SIG_DFL)
534+
535+class TimedOutException(Exception):
536+ def __init__(self, value = "Timed Out"):
537+ self.value = value
538+ def __str__(self):
539+ return repr(self.value)
540+
541+def _restore_backup(path):
542+ pathbackup = path + '.autotest'
543+ if os.path.exists(pathbackup):
544+ shutil.move(pathbackup, path)
545+
546+def _save_backup(path):
547+ pathbackup = path + '.autotest'
548+ if os.path.exists(path) and not os.path.exists(pathbackup):
549+ shutil.copy2(path, pathbackup)
550+ # copy2 does not copy ownership, so do it here.
551+ # Reference: http://docs.python.org/library/shutil.html
552+ a = os.stat(path)
553+ os.chown(pathbackup, a[4], a[5])
554+
555+def config_copydir(path):
556+ if os.path.exists(path) and not os.path.isdir(path):
557+ raise OSError, "'%s' is not a directory" % (path)
558+ _restore_backup(path)
559+
560+ pathbackup = path + '.autotest'
561+ if os.path.exists(path):
562+ shutil.copytree(path, pathbackup, symlinks=True)
563+
564+def config_replace(path,contents,append=False):
565+ '''Replace (or append) to a config file'''
566+ _restore_backup(path)
567+ if os.path.exists(path):
568+ _save_backup(path)
569+ if append:
570+ contents = file(path).read() + contents
571+ open(path, 'w').write(contents)
572+
573+def config_comment(path, field):
574+ _save_backup(path)
575+ contents = ""
576+ for line in file(path):
577+ if re.search("^\s*%s\s*=" % (field), line):
578+ line = "#" + line
579+ contents += line
580+
581+ open(path+'.new', 'w').write(contents)
582+ os.rename(path+'.new', path)
583+
584+def config_set(path, field, value, spaces=True):
585+ _save_backup(path)
586+ contents = ""
587+ if spaces==True:
588+ setting = '%s = %s\n' % (field, value)
589+ else:
590+ setting = '%s=%s\n' % (field, value)
591+ found = False
592+ for line in file(path):
593+ if re.search("^\s*%s\s*=" % (field), line):
594+ found = True
595+ line = setting
596+ contents += line
597+ if not found:
598+ contents += setting
599+
600+ open(path+'.new', 'w').write(contents)
601+ os.rename(path+'.new', path)
602+
603+def config_patch(path, patch, depth=1):
604+ '''Patch a config file'''
605+ _restore_backup(path)
606+ _save_backup(path)
607+
608+ handle, name = mkstemp_fill(patch)
609+ rc = subprocess.call(['/usr/bin/patch', '-p%s' %(depth), path], stdin=handle, stdout=subprocess.PIPE)
610+ os.unlink(name)
611+ if rc != 0:
612+ raise Exception("Patch failed")
613+
614+def config_restore(path):
615+ '''Rename a replaced config file back to its initial state'''
616+ _restore_backup(path)
617+
618+def timeout(secs, f, *args):
619+ def handler(signum, frame):
620+ raise TimedOutException()
621+
622+ old = signal.signal(signal.SIGALRM, handler)
623+ result = None
624+ signal.alarm(secs)
625+ try:
626+ result = f(*args)
627+ finally:
628+ signal.alarm(0)
629+ signal.signal(signal.SIGALRM, old)
630+
631+ return result
632+
633+def require_nonroot():
634+ if os.geteuid() == 0:
635+ print >>sys.stderr, "This series of tests should be run as a regular user with sudo access, not as root."
636+ sys.exit(1)
637+
638+def require_root():
639+ if os.geteuid() != 0:
640+ print >>sys.stderr, "This series of tests should be run with root privileges (e.g. via sudo)."
641+ sys.exit(1)
642+
643+def require_sudo():
644+ if os.geteuid() != 0 or os.environ.get('SUDO_USER', None) == None:
645+ print >>sys.stderr, "This series of tests must be run under sudo."
646+ sys.exit(1)
647+ if os.environ['SUDO_USER'] == 'root':
648+ print >>sys.stderr, 'Please run this test using sudo from a regular user. (You ran sudo from root.)'
649+ sys.exit(1)
650+
651+def random_string(length,lower=False):
652+ '''Return a random string, consisting of ASCII letters, with given
653+ length.'''
654+
655+ s = ''
656+ selection = string.letters
657+ if lower:
658+ selection = string.lowercase
659+ maxind = len(selection)-1
660+ for l in range(length):
661+ s += selection[random.randint(0, maxind)]
662+ return s
663+
664+def mkstemp_fill(contents,suffix='',prefix='testlib-',dir=None):
665+ '''As tempfile.mkstemp does, return a (file, name) pair, but with
666+ prefilled contents.'''
667+
668+ handle, name = tempfile.mkstemp(suffix=suffix,prefix=prefix,dir=dir)
669+ os.close(handle)
670+ handle = file(name,"w+")
671+ handle.write(contents)
672+ handle.flush()
673+ handle.seek(0)
674+
675+ return handle, name
676+
677+def create_fill(path, contents, mode=0644):
678+ '''Safely create a page'''
679+ # make the temp file in the same dir as the destination file so we
680+ # don't get invalid cross-device link errors when we rename
681+ handle, name = mkstemp_fill(contents, dir=os.path.dirname(path))
682+ handle.close()
683+ os.rename(name, path)
684+ os.chmod(path, mode)
685+
686+def login_exists(login):
687+ '''Checks whether the given login exists on the system.'''
688+
689+ try:
690+ pwd.getpwnam(login)
691+ return True
692+ except KeyError:
693+ return False
694+
695+def group_exists(group):
696+ '''Checks whether the given login exists on the system.'''
697+
698+ try:
699+ grp.getgrnam(group)
700+ return True
701+ except KeyError:
702+ return False
703+
704+def recursive_rm(dirPath, contents_only=False):
705+ '''recursively remove directory'''
706+ names = os.listdir(dirPath)
707+ for name in names:
708+ path = os.path.join(dirPath, name)
709+ if os.path.islink(path) or not os.path.isdir(path):
710+ os.unlink(path)
711+ else:
712+ recursive_rm(path)
713+ if contents_only == False:
714+ os.rmdir(dirPath)
715+
716+def check_pidfile(exe, pidfile):
717+ '''Checks if pid in pidfile is running'''
718+ if not os.path.exists(pidfile):
719+ return False
720+
721+ # get the pid
722+ try:
723+ fd = open(pidfile, 'r')
724+ pid = fd.readline().rstrip('\n')
725+ fd.close()
726+ except:
727+ return False
728+
729+ return check_pid(exe, pid)
730+
731+def check_pid(exe, pid):
732+ '''Checks if pid is running'''
733+ cmdline = "/proc/%s/cmdline" % (str(pid))
734+ if not os.path.exists(cmdline):
735+ return False
736+
737+ # get the command line
738+ try:
739+ fd = open(cmdline, 'r')
740+ tmp = fd.readline().split('\0')
741+ fd.close()
742+ except:
743+ return False
744+
745+ # this allows us to match absolute paths or just the executable name
746+ if re.match('^' + exe + '$', tmp[0]) or \
747+ re.match('.*/' + exe + '$', tmp[0]) or \
748+ re.match('^' + exe + ': ', tmp[0]) or \
749+ re.match('^\(' + exe + '\)', tmp[0]):
750+ return True
751+
752+ return False
753+
754+def check_port(port, proto, ver=4):
755+ '''Check if something is listening on the specified port.
756+ WARNING: for some reason this does not work with a bind mounted /proc
757+ '''
758+ assert (port >= 1)
759+ assert (port <= 65535)
760+ assert (proto.lower() == "tcp" or proto.lower() == "udp")
761+ assert (ver == 4 or ver == 6)
762+
763+ fn = "/proc/net/%s" % (proto)
764+ if ver == 6:
765+ fn += str(ver)
766+
767+ rc, report = cmd(['cat', fn])
768+ assert (rc == 0)
769+
770+ hport = "%0.4x" % port
771+
772+ if re.search(': [0-9a-f]{8}:%s [0-9a-f]' % str(hport).lower(), report.lower()):
773+ return True
774+ return False
775+
776+def get_arch():
777+ '''Get the current architecture'''
778+ rc, report = cmd(['uname', '-m'])
779+ assert (rc == 0)
780+ return report.strip()
781+
782+def get_memory():
783+ '''Gets total ram and swap'''
784+ meminfo = "/proc/meminfo"
785+ memtotal = 0
786+ swaptotal = 0
787+ if not os.path.exists(meminfo):
788+ return (False, False)
789+
790+ try:
791+ fd = open(meminfo, 'r')
792+ for line in fd.readlines():
793+ splitline = line.split()
794+ if splitline[0] == 'MemTotal:':
795+ memtotal = int(splitline[1])
796+ elif splitline[0] == 'SwapTotal:':
797+ swaptotal = int(splitline[1])
798+ fd.close()
799+ except:
800+ return (False, False)
801+
802+ return (memtotal,swaptotal)
803+
804+def is_running_in_vm():
805+ '''Check if running under a VM'''
806+ # add other virtualization environments here
807+ for search in ['QEMU Virtual CPU']:
808+ rc, report = cmd_pipe(['dmesg'], ['grep', search])
809+ if rc == 0:
810+ return True
811+ return False
812+
813+def ubuntu_release():
814+ '''Get the Ubuntu release'''
815+ f = "/etc/lsb-release"
816+ try:
817+ size = os.stat(f)[ST_SIZE]
818+ except:
819+ return "UNKNOWN"
820+
821+ if size > 1024*1024:
822+ raise IOError, 'Could not open "%s" (too big)' % f
823+
824+ try:
825+ fh = open("/etc/lsb-release", 'r')
826+ except:
827+ raise
828+
829+ lines = fh.readlines()
830+ fh.close()
831+
832+ pat = re.compile(r'DISTRIB_CODENAME')
833+ for line in lines:
834+ if pat.search(line):
835+ return line.split('=')[1].rstrip('\n').rstrip('\r')
836+
837+ return "UNKNOWN"
838+
839+def cmd(command, input = None, stderr = subprocess.STDOUT, stdout = subprocess.PIPE, stdin = None, timeout = None):
840+ '''Try to execute given command (array) and return its stdout, or return
841+ a textual error if it failed.'''
842+
843+ try:
844+ sp = subprocess.Popen(command, stdin=stdin, stdout=stdout, stderr=stderr, close_fds=True, preexec_fn=subprocess_setup)
845+ except OSError, e:
846+ return [127, str(e)]
847+
848+ out, outerr = sp.communicate(input)
849+ # Handle redirection of stdout
850+ if out == None:
851+ out = ''
852+ # Handle redirection of stderr
853+ if outerr == None:
854+ outerr = ''
855+ return [sp.returncode,out+outerr]
856+
857+def cmd_pipe(command1, command2, input = None, stderr = subprocess.STDOUT, stdin = None):
858+ '''Try to pipe command1 into command2.'''
859+ try:
860+ sp1 = subprocess.Popen(command1, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr, close_fds=True)
861+ sp2 = subprocess.Popen(command2, stdin=sp1.stdout, stdout=subprocess.PIPE, stderr=stderr, close_fds=True)
862+ except OSError, e:
863+ return [127, str(e)]
864+
865+ out = sp2.communicate(input)[0]
866+ return [sp2.returncode,out]
867+
868+def cwd_has_enough_space(cdir, total_bytes):
869+ '''Determine if the partition of the current working directory has 'bytes'
870+ free.'''
871+ rc, df_output = cmd(['df'])
872+ result = 'Got exit code %d, expected %d\n' % (rc, 0)
873+ if rc != 0:
874+ return False
875+
876+ kb = total_bytes / 1024
877+
878+ mounts = dict()
879+ for line in df_output.splitlines():
880+ if '/' not in line:
881+ continue
882+ tmp = line.split()
883+ mounts[tmp[5]] = int(tmp[3])
884+
885+ cdir = os.getcwd()
886+ while cdir != '/':
887+ if not mounts.has_key(cdir):
888+ cdir = os.path.dirname(cdir)
889+ continue
890+ if kb < mounts[cdir]:
891+ return True
892+ else:
893+ return False
894+
895+ if kb < mounts['/']:
896+ return True
897+
898+ return False
899+
900+def get_md5(filename):
901+ '''Gets the md5sum of the file specified'''
902+
903+ (rc, report) = cmd(["/usr/bin/md5sum", "-b", filename])
904+ expected = 0
905+ assert (expected == rc)
906+
907+ return report.split(' ')[0]
908+
909+def dpkg_compare_installed_version(pkg, check, version):
910+ '''Gets the version for the installed package, and compares it to the
911+ specified version.
912+ '''
913+ (rc, report) = cmd(["/usr/bin/dpkg", "-s", pkg])
914+ assert (rc == 0)
915+ assert ("Status: install ok installed" in report)
916+ installed_version = ""
917+ for line in report.splitlines():
918+ if line.startswith("Version: "):
919+ installed_version = line.split()[1]
920+
921+ assert (installed_version != "")
922+
923+ (rc, report) = cmd(["/usr/bin/dpkg", "--compare-versions", installed_version, check, version])
924+ assert (rc == 0 or rc == 1)
925+ if rc == 0:
926+ return True
927+ return False
928+
929+def prepare_source(source, builder, cached_src, build_src, patch_system):
930+ '''Download and unpack source package, installing necessary build depends,
931+ adjusting the permissions for the 'builder' user, and returning the
932+ directory of the unpacked source. Patch system can be one of:
933+ - cdbs
934+ - dpatch
935+ - quilt
936+ - quiltv3
937+ - None (not the string)
938+
939+ This is normally used like this:
940+
941+ def setUp(self):
942+ ...
943+ self.topdir = os.getcwd()
944+ self.cached_src = os.path.join(os.getcwd(), "source")
945+ self.tmpdir = tempfile.mkdtemp(prefix='testlib', dir='/tmp')
946+ self.builder = testlib.TestUser()
947+ testlib.cmd(['chgrp', self.builder.login, self.tmpdir])
948+ os.chmod(self.tmpdir, 0775)
949+
950+ def tearDown(self):
951+ ...
952+ self.builder = None
953+ self.topdir = os.getcwd()
954+ if os.path.exists(self.tmpdir):
955+ testlib.recursive_rm(self.tmpdir)
956+
957+ def test_suite_build(self):
958+ ...
959+ build_dir = testlib.prepare_source('foo', \
960+ self.builder, \
961+ self.cached_src, \
962+ os.path.join(self.tmpdir, \
963+ os.path.basename(self.cached_src)),
964+ "quilt")
965+ os.chdir(build_dir)
966+
967+ # Example for typical build, adjust as necessary
968+ print ""
969+ print " make clean"
970+ rc, report = testlib.cmd(['sudo', '-u', self.builder.login, 'make', 'clean'])
971+
972+ print " configure"
973+ rc, report = testlib.cmd(['sudo', '-u', self.builder.login, './configure', '--prefix=%s' % self.tmpdir, '--enable-debug'])
974+
975+ print " make (will take a while)"
976+ rc, report = testlib.cmd(['sudo', '-u', self.builder.login, 'make'])
977+
978+ print " make check (will take a while)",
979+ rc, report = testlib.cmd(['sudo', '-u', self.builder.login, 'make', 'check'])
980+ expected = 0
981+ result = 'Got exit code %d, expected %d\n' % (rc, expected)
982+ self.assertEquals(expected, rc, result + report)
983+
984+ def test_suite_cleanup(self):
985+ ...
986+ if os.path.exists(self.cached_src):
987+ testlib.recursive_rm(self.cached_src)
988+
989+ It is up to the caller to clean up cached_src and build_src (as in the
990+ above example, often the build_src is in a tmpdir that is cleaned in
991+ tearDown() and the cached_src is cleaned in a one time clean-up
992+ operation (eg 'test_suite_cleanup()) which must be run after the build
993+ suite test (obviously).
994+ '''
995+
996+ # Make sure we have a clean slate
997+ assert (os.path.exists(os.path.dirname(build_src)))
998+ assert (not os.path.exists(build_src))
999+
1000+ cdir = os.getcwd()
1001+ if os.path.exists(cached_src):
1002+ shutil.copytree(cached_src, build_src)
1003+ os.chdir(build_src)
1004+ else:
1005+ # Only install the build dependencies on the initial setup
1006+ rc, report = cmd(['apt-get','-y','--force-yes','build-dep',source])
1007+ assert (rc == 0)
1008+
1009+ os.makedirs(build_src)
1010+ os.chdir(build_src)
1011+
1012+ # These are always needed
1013+ pkgs = ['build-essential', 'dpkg-dev', 'fakeroot']
1014+ rc, report = cmd(['apt-get','-y','--force-yes','install'] + pkgs)
1015+ assert (rc == 0)
1016+
1017+ rc, report = cmd(['apt-get','source',source])
1018+ assert (rc == 0)
1019+ shutil.copytree(build_src, cached_src)
1020+
1021+ unpacked_dir = os.path.join(build_src, glob.glob('%s-*' % source)[0])
1022+
1023+ # Now apply the patches. Do it here so that we don't mess up our cached
1024+ # sources.
1025+ os.chdir(unpacked_dir)
1026+ assert (patch_system in ['cdbs', 'dpatch', 'quilt', 'quiltv3', None])
1027+ if patch_system != None and patch_system != "quiltv3":
1028+ if patch_system == "quilt":
1029+ os.environ.setdefault('QUILT_PATCHES','debian/patches')
1030+ rc, report = cmd(['quilt', 'push', '-a'])
1031+ assert (rc == 0)
1032+ elif patch_system == "cdbs":
1033+ rc, report = cmd(['./debian/rules', 'apply-patches'])
1034+ assert (rc == 0)
1035+ elif patch_system == "dpatch":
1036+ rc, report = cmd(['dpatch', 'apply-all'])
1037+ assert (rc == 0)
1038+
1039+ cmd(['chown', '-R', '%s:%s' % (builder.uid, builder.gid), build_src])
1040+ os.chdir(cdir)
1041+
1042+ return unpacked_dir
1043+
1044+def _aa_status():
1045+ '''Get aa-status output'''
1046+ exe = "/usr/sbin/aa-status"
1047+ assert (os.path.exists(exe))
1048+ if os.geteuid() == 0:
1049+ return cmd([exe])
1050+ return cmd(['sudo', exe])
1051+
1052+def is_apparmor_loaded(path):
1053+ '''Check if profile is loaded'''
1054+ rc, report = _aa_status()
1055+ if rc != 0:
1056+ return False
1057+
1058+ for line in report.splitlines():
1059+ if line.endswith(path):
1060+ return True
1061+ return False
1062+
1063+def is_apparmor_confined(path):
1064+ '''Check if application is confined'''
1065+ rc, report = _aa_status()
1066+ if rc != 0:
1067+ return False
1068+
1069+ for line in report.splitlines():
1070+ if re.search('%s \(' % path, line):
1071+ return True
1072+ return False
1073+
1074+def check_apparmor(path, first_ubuntu_release, is_running=True):
1075+ '''Check if path is loaded and confined for everything higher than the
1076+ first Ubuntu release specified.
1077+
1078+ Usage:
1079+ rc, report = testlib.check_apparmor('/usr/sbin/foo', 8.04, is_running=True)
1080+ if rc < 0:
1081+ return self._skipped(report)
1082+
1083+ expected = 0
1084+ result = 'Got exit code %d, expected %d\n' % (rc, expected)
1085+ self.assertEquals(expected, rc, result + report)
1086+ '''
1087+ global manager
1088+ rc = -1
1089+
1090+ if manager.lsb_release["Release"] < first_ubuntu_release:
1091+ return (rc, "Skipped apparmor check")
1092+
1093+ if not os.path.exists('/sbin/apparmor_parser'):
1094+ return (rc, "Skipped (couldn't find apparmor_parser)")
1095+
1096+ rc = 0
1097+ msg = ""
1098+ if not is_apparmor_loaded(path):
1099+ rc = 1
1100+ msg = "Profile not loaded for '%s'" % path
1101+
1102+ # this check only makes sense it the 'path' is currently executing
1103+ if is_running and rc == 0 and not is_apparmor_confined(path):
1104+ rc = 1
1105+ msg = "'%s' is not running in enforce mode" % path
1106+
1107+ return (rc, msg)
1108+
1109+def get_gcc_version(gcc, full=True):
1110+ gcc_version = 'none'
1111+ if not gcc.startswith('/'):
1112+ gcc = '/usr/bin/%s' % (gcc)
1113+ if os.path.exists(gcc):
1114+ gcc_version = 'unknown'
1115+ lines = cmd([gcc,'-v'])[1].strip().splitlines()
1116+ version_lines = [x for x in lines if x.startswith('gcc version')]
1117+ if len(version_lines) == 1:
1118+ gcc_version = " ".join(version_lines[0].split()[2:])
1119+ if not full:
1120+ return gcc_version.split()[0]
1121+ return gcc_version
1122+
1123+def is_kdeinit_running():
1124+ '''Test if kdeinit is running'''
1125+ # applications that use kdeinit will spawn it if it isn't running in the
1126+ # test. This is a problem because it does not exit. This is a helper to
1127+ # check for it.
1128+ rc, report = cmd(['ps', 'x'])
1129+ if 'kdeinit4 Running' not in report:
1130+ print >>sys.stderr, ("kdeinit not running (you may start/stop any KDE application then run this script again)")
1131+ return False
1132+ return True
1133+
1134+def get_pkgconfig_flags(libs=[]):
1135+ '''Find pkg-config flags for libraries'''
1136+ assert (len(libs) > 0)
1137+ rc, pkg_config = cmd(['pkg-config', '--cflags', '--libs'] + libs)
1138+ expected = 0
1139+ if rc != expected:
1140+ print >>sys.stderr, 'Got exit code %d, expected %d\n' % (rc, expected)
1141+ assert(rc == expected)
1142+ return pkg_config.split()
1143+
1144+class TestDaemon:
1145+ '''Helper class to manage daemons consistently'''
1146+ def __init__(self, init):
1147+ '''Setup daemon attributes'''
1148+ self.initscript = init
1149+
1150+ def start(self):
1151+ '''Start daemon'''
1152+ rc, report = cmd([self.initscript, 'start'])
1153+ expected = 0
1154+ result = 'Got exit code %d, expected %d\n' % (rc, expected)
1155+ time.sleep(2)
1156+ if expected != rc:
1157+ return (False, result + report)
1158+
1159+ if "fail" in report:
1160+ return (False, "Found 'fail' in report\n" + report)
1161+
1162+ return (True, "")
1163+
1164+ def stop(self):
1165+ '''Stop daemon'''
1166+ rc, report = cmd([self.initscript, 'stop'])
1167+ expected = 0
1168+ result = 'Got exit code %d, expected %d\n' % (rc, expected)
1169+ if expected != rc:
1170+ return (False, result + report)
1171+
1172+ if "fail" in report:
1173+ return (False, "Found 'fail' in report\n" + report)
1174+
1175+ return (True, "")
1176+
1177+ def reload(self):
1178+ '''Reload daemon'''
1179+ rc, report = cmd([self.initscript, 'force-reload'])
1180+ expected = 0
1181+ result = 'Got exit code %d, expected %d\n' % (rc, expected)
1182+ if expected != rc:
1183+ return (False, result + report)
1184+
1185+ if "fail" in report:
1186+ return (False, "Found 'fail' in report\n" + report)
1187+
1188+ return (True, "")
1189+
1190+ def restart(self):
1191+ '''Restart daemon'''
1192+ (res, str) = self.stop()
1193+ if not res:
1194+ return (res, str)
1195+
1196+ (res, str) = self.start()
1197+ if not res:
1198+ return (res, str)
1199+
1200+ return (True, "")
1201+
1202+ def status(self):
1203+ '''Check daemon status'''
1204+ rc, report = cmd([self.initscript, 'status'])
1205+ expected = 0
1206+ result = 'Got exit code %d, expected %d\n' % (rc, expected)
1207+ if expected != rc:
1208+ return (False, result + report)
1209+
1210+ if "fail" in report:
1211+ return (False, "Found 'fail' in report\n" + report)
1212+
1213+ return (True, "")
1214+
1215+class TestlibManager(object):
1216+ '''Singleton class used to set up per-test-run information'''
1217+ def __init__(self):
1218+ # Set glibc aborts to dump to stderr instead of the tty so test output
1219+ # is more sane.
1220+ os.environ.setdefault('LIBC_FATAL_STDERR_','1')
1221+
1222+ # check verbosity
1223+ self.verbosity = False
1224+ if (len(sys.argv) > 1 and '-v' in sys.argv[1:]):
1225+ self.verbosity = True
1226+
1227+ # Load LSB release file
1228+ self.lsb_release = dict()
1229+ if not os.path.exists('/usr/bin/lsb_release') and not os.path.exists('/bin/lsb_release'):
1230+ raise OSError, "Please install 'lsb-release'"
1231+ for line in subprocess.Popen(['lsb_release','-a'],stdout=subprocess.PIPE,stderr=subprocess.PIPE).communicate()[0].splitlines():
1232+ field, value = line.split(':',1)
1233+ value=value.strip()
1234+ field=field.strip()
1235+ # Convert numerics
1236+ try:
1237+ value = float(value)
1238+ except:
1239+ pass
1240+ self.lsb_release.setdefault(field,value)
1241+
1242+ # FIXME: hack OEM releases into known-Ubuntu versions
1243+ if self.lsb_release['Distributor ID'] == "HP MIE (Mobile Internet Experience)":
1244+ if self.lsb_release['Release'] == 1.0:
1245+ self.lsb_release['Distributor ID'] = "Ubuntu"
1246+ self.lsb_release['Release'] = 8.04
1247+ else:
1248+ raise OSError, "Unknown version of HP MIE"
1249+
1250+ # FIXME: hack to assume a most-recent release if we're not
1251+ # running under Ubuntu.
1252+ if self.lsb_release['Distributor ID'] not in ["Ubuntu","Linaro"]:
1253+ self.lsb_release['Release'] = 10000
1254+ # Adjust Linaro release to pretend to be Ubuntu
1255+ if self.lsb_release['Distributor ID'] in ["Linaro"]:
1256+ self.lsb_release['Distributor ID'] = "Ubuntu"
1257+ self.lsb_release['Release'] -= 0.01
1258+
1259+ # Load arch
1260+ if not os.path.exists('/usr/bin/dpkg'):
1261+ machine = cmd(['uname','-m'])[1].strip()
1262+ if machine.endswith('86'):
1263+ self.dpkg_arch = 'i386'
1264+ elif machine.endswith('_64'):
1265+ self.dpkg_arch = 'amd64'
1266+ elif machine.startswith('arm'):
1267+ self.dpkg_arch = 'armel'
1268+ else:
1269+ raise ValueError, "Unknown machine type '%s'" % (machine)
1270+ else:
1271+ self.dpkg_arch = cmd(['dpkg','--print-architecture'])[1].strip()
1272+
1273+ # Find kernel version
1274+ self.kernel_is_ubuntu = False
1275+ self.kernel_version_signature = None
1276+ self.kernel_version = cmd(["uname","-r"])[1].strip()
1277+ versig = '/proc/version_signature'
1278+ if os.path.exists(versig):
1279+ self.kernel_is_ubuntu = True
1280+ self.kernel_version_signature = file(versig).read().strip()
1281+ self.kernel_version_ubuntu = self.kernel_version
1282+ elif os.path.exists('/usr/bin/dpkg'):
1283+ # this can easily be inaccurate but is only an issue for Dapper
1284+ rc, out = cmd(['dpkg','-l','linux-image-%s' % (self.kernel_version)])
1285+ if rc == 0:
1286+ self.kernel_version_signature = out.strip().split('\n').pop().split()[2]
1287+ self.kernel_version_ubuntu = self.kernel_version_signature
1288+ if self.kernel_version_signature == None:
1289+ # Attempt to fall back to something for non-Debian-based
1290+ self.kernel_version_signature = self.kernel_version
1291+ self.kernel_version_ubuntu = self.kernel_version
1292+ # Build ubuntu version without hardware suffix
1293+ try:
1294+ self.kernel_version_ubuntu = "-".join([x for x in self.kernel_version_signature.split(' ')[1].split('-') if re.search('^[0-9]', x)])
1295+ except:
1296+ pass
1297+
1298+ # Find gcc version
1299+ self.gcc_version = get_gcc_version('gcc')
1300+
1301+ # Find libc
1302+ self.path_libc = [x.split()[2] for x in cmd(['ldd','/bin/ls'])[1].splitlines() if x.startswith('\tlibc.so.')][0]
1303+
1304+ # Report self
1305+ if self.verbosity:
1306+ kernel = self.kernel_version_ubuntu
1307+ if kernel != self.kernel_version_signature:
1308+ kernel += " (%s)" % (self.kernel_version_signature)
1309+ print >>sys.stdout, "Running test: '%s' distro: '%s %.2f' kernel: '%s' arch: '%s' uid: %d/%d SUDO_USER: '%s')" % ( \
1310+ sys.argv[0],
1311+ self.lsb_release['Distributor ID'],
1312+ self.lsb_release['Release'],
1313+ kernel,
1314+ self.dpkg_arch,
1315+ os.geteuid(), os.getuid(),
1316+ os.environ.get('SUDO_USER', ''))
1317+ sys.stdout.flush()
1318+
1319+ # Additional heuristics
1320+ #if os.environ.get('SUDO_USER', os.environ.get('USER', '')) in ['mdeslaur']:
1321+ # sys.stdout.write("Replying to Marc Deslauriers in http://launchpad.net/bugs/%d: " % random.randint(600000, 980000))
1322+ # sys.stdout.flush()
1323+ # time.sleep(0.5)
1324+ # sys.stdout.write("destroyed\n")
1325+ # time.sleep(0.5)
1326+
1327+ def hello(self, msg):
1328+ print >>sys.stderr, "Hello from %s" % (msg)
1329+# The central instance
1330+manager = TestlibManager()
1331+
1332+class TestlibCase(unittest.TestCase):
1333+ def __init__(self, *args):
1334+ '''This is called for each TestCase test instance, which isn't much better
1335+ than SetUp.'''
1336+
1337+ unittest.TestCase.__init__(self, *args)
1338+
1339+ # Attach to and duplicate dicts from manager singleton
1340+ self.manager = manager
1341+ #self.manager.hello(repr(self) + repr(*args))
1342+ self.my_verbosity = self.manager.verbosity
1343+ self.lsb_release = self.manager.lsb_release
1344+ self.dpkg_arch = self.manager.dpkg_arch
1345+ self.kernel_version = self.manager.kernel_version
1346+ self.kernel_version_signature = self.manager.kernel_version_signature
1347+ self.kernel_version_ubuntu = self.manager.kernel_version_ubuntu
1348+ self.kernel_is_ubuntu = self.manager.kernel_is_ubuntu
1349+ self.gcc_version = self.manager.gcc_version
1350+ self.path_libc = self.manager.path_libc
1351+
1352+ def version_compare(self, one, two):
1353+ return apt_pkg.VersionCompare(one,two)
1354+
1355+ def assertFileType(self, filename, filetype):
1356+ '''Checks the file type of the file specified'''
1357+
1358+ (rc, report, out) = self._testlib_shell_cmd(["/usr/bin/file", "-b", filename])
1359+ out = out.strip()
1360+ expected = 0
1361+ # Absolutely no idea why this happens on Hardy
1362+ if self.lsb_release['Release'] == 8.04 and rc == 255 and len(out) > 0:
1363+ rc = 0
1364+ result = 'Got exit code %d, expected %d:\n%s\n' % (rc, expected, report)
1365+ self.assertEquals(expected, rc, result)
1366+
1367+ filetype = '^%s$' % (filetype)
1368+ result = 'File type reported by file: [%s], expected regex: [%s]\n' % (out, filetype)
1369+ self.assertNotEquals(None, re.search(filetype, out), result)
1370+
1371+ def yank_commonname_from_cert(self, certfile):
1372+ '''Extract the commonName from a given PEM'''
1373+ rc, out = cmd(['openssl','asn1parse','-in',certfile])
1374+ if rc == 0:
1375+ ready = False
1376+ for line in out.splitlines():
1377+ if ready:
1378+ return line.split(':')[-1]
1379+ if ':commonName' in line:
1380+ ready = True
1381+ return socket.getfqdn()
1382+
1383+ def announce(self, text):
1384+ if self.my_verbosity:
1385+ print >>sys.stdout, "(%s) " % (text),
1386+ sys.stdout.flush()
1387+
1388+ def make_clean(self):
1389+ rc, output = self.shell_cmd(['make','clean'])
1390+ self.assertEquals(rc, 0, output)
1391+
1392+ def get_makefile_compiler(self):
1393+ # Find potential compiler name
1394+ compiler = 'gcc'
1395+ if os.path.exists('Makefile'):
1396+ for line in open('Makefile'):
1397+ if line.startswith('CC') and '=' in line:
1398+ items = [x.strip() for x in line.split('=')]
1399+ if items[0] == 'CC':
1400+ compiler = items[1]
1401+ break
1402+ return compiler
1403+
1404+ def make_target(self, target, expected=0):
1405+ '''Compile a target and report output'''
1406+
1407+ compiler = self.get_makefile_compiler()
1408+ rc, output = self.shell_cmd(['make',target])
1409+ self.assertEquals(rc, expected, 'rc(%d)!=%d:\n' % (rc, expected) + output)
1410+ self.assertTrue('%s ' % (compiler) in output, 'Expected "%s":' % (compiler) + output)
1411+ return output
1412+
1413+ # call as return testlib.skipped()
1414+ def _skipped(self, reason=""):
1415+ '''Provide a visible way to indicate that a test was skipped'''
1416+ if reason != "":
1417+ reason = ': %s' % (reason)
1418+ self.announce("skipped%s" % (reason))
1419+ return False
1420+
1421+ def _testlib_shell_cmd(self,args,stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT):
1422+ argstr = "'" + "', '".join(args).strip() + "'"
1423+ rc, out = cmd(args,stdin=stdin,stdout=stdout,stderr=stderr)
1424+ report = 'Command: ' + argstr + '\nOutput:\n' + out
1425+ return rc, report, out
1426+
1427+ def shell_cmd(self, args, stdin=None):
1428+ return cmd(args,stdin=stdin)
1429+
1430+ def assertShellExitEquals(self, expected, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg=""):
1431+ '''Test a shell command matches a specific exit code'''
1432+ rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr)
1433+ result = 'Got exit code %d, expected %d\n' % (rc, expected)
1434+ self.assertEquals(expected, rc, msg + result + report)
1435+
1436+ def assertShellExitNotEquals(self, unwanted, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg=""):
1437+ '''Test a shell command doesn't match a specific exit code'''
1438+ rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr)
1439+ result = 'Got (unwanted) exit code %d\n' % rc
1440+ self.assertNotEquals(unwanted, rc, msg + result + report)
1441+
1442+ def assertShellOutputContains(self, text, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg="", invert=False):
1443+ '''Test a shell command contains a specific output'''
1444+ rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr)
1445+ result = 'Got exit code %d. Looking for text "%s"\n' % (rc, text)
1446+ if not invert:
1447+ self.assertTrue(text in out, msg + result + report)
1448+ else:
1449+ self.assertFalse(text in out, msg + result + report)
1450+
1451+ def assertShellOutputEquals(self, text, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg="", invert=False, expected=None):
1452+ '''Test a shell command matches a specific output'''
1453+ rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr)
1454+ result = 'Got exit code %d. Looking for exact text "%s" (%s)\n' % (rc, text, " ".join(args))
1455+ if not invert:
1456+ self.assertEquals(text, out, msg + result + report)
1457+ else:
1458+ self.assertNotEquals(text, out, msg + result + report)
1459+ if expected != None:
1460+ result = 'Got exit code %d. Expected %d (%s)\n' % (rc, expected, " ".join(args))
1461+ self.assertEquals(rc, expected, msg + result + report)
1462+
1463+ def _word_find(self, report, content, invert=False):
1464+ '''Check for a specific string'''
1465+ if invert:
1466+ warning = 'Found "%s"\n' % content
1467+ self.assertTrue(content not in report, warning + report)
1468+ else:
1469+ warning = 'Could not find "%s"\n' % content
1470+ self.assertTrue(content in report, warning + report)
1471+
1472+ def _test_sysctl_value(self, path, expected, msg=None, exists=True):
1473+ sysctl = '/proc/sys/%s' % (path)
1474+ self.assertEquals(exists, os.path.exists(sysctl), sysctl)
1475+ value = None
1476+ if exists:
1477+ value = int(file(sysctl).read())
1478+ report = "%s is not %d: %d" % (sysctl, expected, value)
1479+ if msg:
1480+ report += " (%s)" % (msg)
1481+ self.assertEquals(value, expected, report)
1482+ return value
1483+
1484+ def set_sysctl_value(self, path, desired):
1485+ sysctl = '/proc/sys/%s' % (path)
1486+ self.assertTrue(os.path.exists(sysctl),"%s does not exist" % (sysctl))
1487+ file(sysctl,'w').write(str(desired))
1488+ self._test_sysctl_value(path, desired)
1489+
1490+ def kernel_at_least(self, introduced):
1491+ return self.version_compare(self.kernel_version_ubuntu,
1492+ introduced) >= 0
1493+
1494+ def kernel_claims_cve_fixed(self, cve):
1495+ changelog = "/usr/share/doc/linux-image-%s/changelog.Debian.gz" % (self.kernel_version)
1496+ if os.path.exists(changelog):
1497+ for line in gzip.open(changelog):
1498+ if cve in line and not "revert" in line and not "Revert" in line:
1499+ return True
1500+ return False
1501+
1502+class TestGroup:
1503+ '''Create a temporary test group and remove it again in the dtor.'''
1504+
1505+ def __init__(self, group=None, lower=False):
1506+ '''Create a new group'''
1507+
1508+ self.group = None
1509+ if group:
1510+ if group_exists(group):
1511+ raise ValueError, 'group name already exists'
1512+ else:
1513+ while(True):
1514+ group = random_string(7,lower=lower)
1515+ if not group_exists(group):
1516+ break
1517+
1518+ assert subprocess.call(['groupadd',group]) == 0
1519+ self.group = group
1520+ g = grp.getgrnam(self.group)
1521+ self.gid = g[2]
1522+
1523+ def __del__(self):
1524+ '''Remove the created group.'''
1525+
1526+ if self.group:
1527+ rc, report = cmd(['groupdel', self.group])
1528+ assert rc == 0
1529+
1530+class TestUser:
1531+ '''Create a temporary test user and remove it again in the dtor.'''
1532+
1533+ def __init__(self, login=None, home=True, group=None, uidmin=None, lower=False, shell=None):
1534+ '''Create a new user account with a random password.
1535+
1536+ By default, the login name is random, too, but can be explicitly
1537+ specified with 'login'. By default, a home directory is created, this
1538+ can be suppressed with 'home=False'.'''
1539+
1540+ self.login = None
1541+
1542+ if os.geteuid() != 0:
1543+ raise ValueError, "You must be root to run this test"
1544+
1545+ if login:
1546+ if login_exists(login):
1547+ raise ValueError, 'login name already exists'
1548+ else:
1549+ while(True):
1550+ login = 't' + random_string(7,lower=lower)
1551+ if not login_exists(login):
1552+ break
1553+
1554+ self.salt = random_string(2)
1555+ self.password = random_string(8,lower=lower)
1556+ self.crypted = crypt.crypt(self.password, self.salt)
1557+
1558+ creation = ['useradd', '-p', self.crypted]
1559+ if home:
1560+ creation += ['-m']
1561+ if group:
1562+ creation += ['-G',group]
1563+ if uidmin:
1564+ creation += ['-K','UID_MIN=%d'%uidmin]
1565+ if shell:
1566+ creation += ['-s',shell]
1567+ creation += [login]
1568+ assert subprocess.call(creation) == 0
1569+ # Set GECOS
1570+ assert subprocess.call(['usermod','-c','Buddy %s' % (login),login]) == 0
1571+
1572+ self.login = login
1573+ p = pwd.getpwnam(self.login)
1574+ self.uid = p[2]
1575+ self.gid = p[3]
1576+ self.gecos = p[4]
1577+ self.home = p[5]
1578+ self.shell = p[6]
1579+
1580+ def __del__(self):
1581+ '''Remove the created user account.'''
1582+
1583+ if self.login:
1584+ # sanity check the login name so we don't accidentally wipe too much
1585+ if len(self.login)>3 and not '/' in self.login:
1586+ subprocess.call(['rm','-rf', '/home/'+self.login, '/var/mail/'+self.login])
1587+ rc, report = cmd(['userdel', '-f', self.login])
1588+ assert rc == 0
1589+
1590+ def add_to_group(self, group):
1591+ '''Add user to the specified group name'''
1592+ rc, report = cmd(['usermod', '-G', group, self.login])
1593+ if rc != 0:
1594+ print report
1595+ assert rc == 0
1596+
1597+# Timeout handler using alarm() from John P. Speno's Pythonic Avocado
1598+class TimeoutFunctionException(Exception):
1599+ """Exception to raise on a timeout"""
1600+ pass
1601+class TimeoutFunction:
1602+ def __init__(self, function, timeout):
1603+ self.timeout = timeout
1604+ self.function = function
1605+
1606+ def handle_timeout(self, signum, frame):
1607+ raise TimeoutFunctionException()
1608+
1609+ def __call__(self, *args, **kwargs):
1610+ old = signal.signal(signal.SIGALRM, self.handle_timeout)
1611+ signal.alarm(self.timeout)
1612+ try:
1613+ result = self.function(*args, **kwargs)
1614+ finally:
1615+ signal.signal(signal.SIGALRM, old)
1616+ signal.alarm(0)
1617+ return result
1618+
1619+def main():
1620+ print "hi"
1621+ unittest.main()

Subscribers

People subscribed via source and target branches

to all changes: