1
# -*- coding: UTF-8 -*-
3
'''Provide a test environment with a fake /sys, modprobe, etc., and
4
implementations of OSLib, AbstractUI, DriverDB, and various handlers suitable
7
# (c) 2007 Canonical Ltd.
9
# This program is free software; you can redistribute it and/or modify
10
# it under the terms of the GNU General Public License as published by
11
# the Free Software Foundation; either version 2 of the License, or
12
# (at your option) any later version.
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# GNU General Public License for more details.
19
# You should have received a copy of the GNU General Public License along
20
# with this program; if not, write to the Free Software Foundation, Inc.,
21
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
import tempfile, atexit, shutil, os, os.path
25
from jockey.oslib import OSLib
26
from jockey.detection import HardwareID, DriverID, DriverDB
27
from jockey.ui import AbstractUI
31
'filename': '/lib/modules/0.8.15/foo/vanilla.ko',
33
'description': 'free module with available hardware, graphics card',
34
'alias': 'pci:v00001001d*sv*sd*bc03sc*i*'
37
'filename': '/lib/modules/0.8.15/bar/chocolate.ko',
39
'description': 'free module with nonavailable hardware',
40
'alias': ['pci:v00001002d00000001sv*bc*sc*i*', 'pci:v00001002d00000002sv*sd*bc*sc*i*'],
43
'filename': '/lib/modules/0.8.15/extra/cherry.ko',
45
'description': 'nonfree module with nonavailable hardware, wifi',
46
'alias': 'pci:v0000EEEEd*sv*sd*bc02sc80i*',
49
'filename': '/lib/modules/0.8.15/extra/mint.ko',
50
'license': "Palpatine's Revenge",
51
'description': 'nonfree module with available hardware, wifi',
52
'alias': 'pci:v0000AAAAd000012*sv*sd*bc02sc*i*',
55
'filename': '/lib/modules/0.8.15/extra/vanilla3d.ko',
57
'description': 'nonfree, replacement graphics driver for vanilla',
58
'alias': 'pci:v00001001d00003D*sv*sd*bc03sc*i*'
61
'filename': '/lib/modules/0.8.15/extra/spam.ko',
63
'description': 'free mystical module without modaliases',
66
'filename': '/lib/modules/0.8.15/standard/dullstd.ko',
68
'description': 'standard module which should be ignored for detection',
69
'alias': 'pci:v0000DEAFd*sv*sd*bc99sc*i*'
74
# graphics card, can use vanilla or vanilla3d, defaulting to free driver
75
'pci0000:00/0000:00:11.2': {
76
'modalias': 'pci:v00001001d00003D01sv00001234sd00000001bc03sc00i00',
77
'driver': '../../../bus/pci/drivers/vanilla',
81
# pretend that we have two devices using vanilla, to check for duplication
83
'pci0000:02/0000:00:03.4': {
84
'modalias': 'pci:v00001001d00003D01sv00001234sd00000001bc03sc00i00',
85
'driver': '../../../bus/pci/drivers/vanilla',
89
# something that uses a statically built-in kernel driver
90
'pci0000:00/0000:00:22.3': {
91
'modalias': 'pci:v00003456d00000001sv00000000sd00000000bc09sc23i00',
92
'driver': '../../../bus/pci/drivers/southbridge',
95
# wifi which can use mint, but not enabled
96
'pci0000:00/0000:01:01.0': {
97
'modalias': 'pci:v0000AAAAd000012AAsv00000000sdDEADBEEFbc02sc80i00',
100
# unknown piece of hardware
101
'pci0000:01/0000:05:05.0': {
102
'modalias': 'pci:v0000FFF0d00000001sv00000000sd00000000bc06sc01i01',
105
# uninteresting standard component
106
'pci0000:02/0000:01:02.3': {
107
'modalias': 'pci:v0000DEAFd00009999sv00000000sd00000000bc99sc00i00',
108
'driver': '../../../bus/pci/drivers/dullstd',
114
# vanilla3d option for the graphics card
115
HardwareID('modalias', 'pci:v00001001d00003D*sv*sd*bc03sc*i*'): {
116
('Foonux', '42'): [DriverID(handler='VanillaGfxHandler')],
117
('Foonux', '41'): [DriverID(handler='IShallNotExist')],
118
('RedSock', '2.0'): [DriverID(handler='Vanilla3DHandler',
119
repository='http://nonfree.redsock.com/addons')]
122
# driver for the unknown piece of hardware; test multiple handlers here
123
HardwareID('modalias', 'pci:v0000FFF0d0000*sv*sd*bc06sc01i*'): {
125
DriverID(handler='KernelModuleHandler', module='spam'),
126
DriverID(handler='KernelModuleHandler', module='mint', xopt1='unbreak')
131
HardwareID('pci', '9876/FEDC'): {
132
('Foonux', '42'): [DriverID(handler='BogusHandler')],
133
('Oobuntu', 'Eccentric Emu'): [DriverID(handler='BogusHandler')],
139
'description': ('X.org libraries for the Vanilla 3D driver',
140
'This package provides optimized Mesa libraries for the Vanilla '
145
'description': ('standard system mesa libraries',
146
'default mesa libs for free drivers'),
150
'description': ('unrelated system package',
151
'This package should always be installed.'),
156
#-------------------------------------------------------------------#
158
class TestOSLib(OSLib):
159
'''Test suite implementation of OSLib'''
164
# set up a fake environment
165
self.workdir = tempfile.mkdtemp()
166
atexit.register(shutil.rmtree, self.workdir)
168
self._make_proc_modules()
169
self._make_modprobe()
170
self._make_modalias()
172
self._make_xorg_conf()
174
self.module_blacklist_file = os.path.join(self.workdir, 'module-blacklist')
176
self.handler_dir = os.path.join(self.workdir, 'handlers')
177
os.mkdir(self.handler_dir)
178
self.check_cache = os.path.join(self.workdir, 'check_cache')
179
self.backup_dir = os.path.join(self.workdir, 'backup')
180
os.mkdir(self.backup_dir)
182
self.installed_packages = set(['mesa-std', 'coreutils'])
184
self.reboot_flag = False
186
def _make_modinfo(self):
187
'''Create a dummy modinfo program which outputs the fake_modinfo data
188
and set self.modinfo_path.
190
Note that this fake modinfo only supports one module argument, not
191
several (as the original modinfo), and no options.'''
193
os.mkdir(os.path.join(self.workdir, 'bin'))
194
self.modinfo_path = os.path.join(self.workdir, 'bin', 'modinfo')
195
mi = open(self.modinfo_path, 'w')
196
mi.write('''#!/usr/bin/python
201
if len(sys.argv) != 2:
202
print >> sys.stderr, 'Usage: modinfo module'
207
attrs = data[m].keys()
210
if hasattr(data[m][k], 'isspace'):
211
print '%%-16s%%s' %% (k + ':', data[m][k])
214
print '%%-16s%%s' %% (k + ':', i)
216
print >> sys.stderr, 'modinfo: could not find module', m
219
''' % repr(fake_modinfo))
221
os.chmod(self.modinfo_path, 0755)
223
def _make_modprobe(self):
224
'''Create a dummy modprobe and set self.modprobe_path.'''
226
self.modprobe_path = os.path.join(self.workdir, 'bin', 'modprobe')
227
mp = open(self.modprobe_path, 'w')
228
mp.write('''#!/usr/bin/python
234
if len(sys.argv) != 2:
235
print >> sys.stderr, 'Usage: modprobe module'
240
if m not in open(proc_modules).read():
241
print >> open(proc_modules, 'a'), '%%s 11111 2 - Live 0xbeefbeef' %% m
243
print >> sys.stderr, 'FATAL: Module %%s not found.' %% m
245
''' % (repr(fake_modinfo), repr(self.proc_modules)))
248
os.chmod(self.modprobe_path, 0755)
251
'''Create a dummy /sys tree from fake_sys and set self.sys_dir.'''
253
self.sys_dir = os.path.join(self.workdir, 'sys')
255
for pcipath, info in fake_sys.iteritems():
256
# create /sys/device entry
257
device_dir = os.path.join(self.sys_dir, 'devices', pcipath)
258
os.makedirs(device_dir)
259
open(os.path.join(device_dir, 'modalias'), 'w').write(info['modalias'])
261
# create driver dir and symlink to device, if existing
262
if 'driver' not in info:
264
driver_dir = os.path.join(device_dir, info['driver'])
266
os.makedirs(driver_dir)
269
os.symlink(info['driver'], os.path.join(device_dir, 'driver'))
270
os.symlink(device_dir, os.path.join(driver_dir,
271
os.path.basename(pcipath)))
273
# create module dir and symlink to driver, if existing
274
if 'module' not in info:
276
module_dir = os.path.join(self.workdir, 'sys', 'module', info['module'])
278
os.makedirs(os.path.join(module_dir, 'drivers'))
281
if not os.path.islink(os.path.join(driver_dir, 'module')):
282
os.symlink(module_dir, os.path.join(driver_dir, 'module'))
283
driver_comp = info['driver'].split('/')
284
mod_driver_link = os.path.join(module_dir, 'drivers',
285
'%s:%s' % (driver_comp[-3], driver_comp[-1]))
286
if not os.path.islink(mod_driver_link):
287
os.symlink(driver_dir, mod_driver_link)
289
def _make_proc_modules(self):
290
'''Create a dummy /proc/modules and set self.proc_modules.'''
292
self.proc_modules = os.path.join(self.workdir, 'proc', 'modules')
293
if not os.path.isdir(os.path.dirname(self.proc_modules)):
294
os.mkdir(os.path.dirname(self.proc_modules))
296
for info in fake_sys.itervalues():
298
mods.add(info['module'])
302
f = open(self.proc_modules, 'w')
304
print >> f, '%s 12345 0 - Live 0xdeadbeef' % m
307
def _make_modalias(self):
308
'''Create a dummy modules.alias and set self.modaliases
311
# prepare one fake kernel modules.alias and an override directory
313
os.path.join(self.workdir, 'kernelmods', 'modules.alias'),
314
os.path.join(self.workdir, 'modalias-overrides'),
315
'/nonexisting', # should not stumble over those
317
os.mkdir(os.path.dirname(self.modaliases[0]))
318
os.mkdir(self.modaliases[1])
320
f = open(self.modaliases[0], 'w')
321
print >> f, '# Aliases extracted from modules themselves.'
322
for mod in sorted(fake_modinfo.keys()):
323
aliases = fake_modinfo[mod].get('alias', [])
324
if hasattr(aliases, 'isspace'):
325
print >> f, 'alias %s %s' % (aliases, mod)
328
print >> f, 'alias %s %s' % (a, mod)
331
def _make_xorg_conf(self):
332
'''Create a dummy xorg.conf and set self.xorg_conf_path appropriately.'''
334
self.xorg_conf_path = os.path.join(self.workdir, 'xorg.conf')
335
f = open(self.xorg_conf_path, 'w')
336
f.write('''# xorg.conf (xorg X Window System server configuration file)
338
Section "InputDevice"
339
Identifier "Generic Keyboard"
341
Option "CoreKeyboard"
342
Option "XkbRules" "xorg"
343
Option "XkbModel" "pc105"
344
Option "XkbLayout" "us"
348
Identifier "Standard ice cream graphics"
353
Identifier "My Monitor"
360
Identifier "Default Screen"
361
Device "Standard ice cream graphics"
366
Section "ServerLayout"
367
Identifier "Default Layout"
368
Screen "Default Screen"
369
InputDevice "Generic Keyboard"
374
def _get_os_version(self):
375
self.os_vendor = 'Foonux'
376
self.os_version = '42'
378
def ignored_modules(self):
379
return set(['dullstd'])
381
def package_description(self, package):
382
'''Return a tupe (short_description, long_description) for given
385
This should raise a ValueError if the package is not available.'''
388
return fake_pkginfo[package]['description']
390
raise ValueError, 'no such package'
392
def is_package_free(self, package):
393
'''Return if given package is free software.'''
395
return fake_pkginfo[package]['free']
397
def package_installed(self, package):
398
'''Return if the given package is installed.'''
400
return package in self.installed_packages
402
def install_package(self, package, ui):
403
'''Install the given package.
405
The current UI object is passed as well, in case package installation
406
wants to do some callbacks/confirmation dialogs/queries.
408
If this succeeds, subsequent package_installed(package) calls must
411
assert package in fake_pkginfo
412
self.installed_packages.add(package)
414
def remove_package(self, package, ui):
415
'''Uninstall the given package.
417
The current UI object is passed as well, in case package installation
418
wants to do some callbacks/confirmation dialogs/queries.
420
If this succeeds, subsequent package_installed(package) calls must
423
assert package in fake_pkginfo
424
self.installed_packages.remove(package)
426
def ui_notify_reboot(self, ui):
427
'''Indicate that the user should do a reboot to activate changes (such
428
as changing an X.org video driver).'''
430
self.reboot_flag = True
432
def pop_reboot_flag(self):
433
result = self.reboot_flag
434
self.reboot_flag = False
437
#-------------------------------------------------------------------#
439
class TestDriverDB(DriverDB):
440
'''Test suite implementation of DriverDB'''
442
def query(self, hwid):
443
return fake_db.get(hwid, {}).get(
444
(OSLib.inst.os_vendor, OSLib.inst.os_version), [])
446
#-------------------------------------------------------------------#
448
class TestUI(AbstractUI):
450
AbstractUI.__init__(self)
451
self.error_stack = []
452
self.confirm_response = None
453
self.notification_stack = []
454
self.main_loop_active = False
455
self.cur_download = [None, None, None] # (url, size, total)
456
self.cancel_download = False # whether to cancel the next download at 30%
458
def convert_keybindings(self, str):
464
def ui_main_loop(self):
465
'''Main loop for the user interface.
467
This should return if the user wants to quit the program, and return
470
self.main_loop_active = True
472
def error_message(self, title, text):
473
'''Show an error message box with given title and text.'''
475
self.error_stack.append((title, text))
478
'''Return the last error message (title, text) tuple.'''
480
return self.error_stack.pop()
482
def confirm_action(self, title, text, subtext = None, action=None):
483
assert self.confirm_response is not None, 'must set confirm_response'
485
self.last_confirm_title = title
486
self.last_confirm_text = text
487
self.last_confirm_subtext = subtext
488
self.last_confirm_action = action
490
r = self.confirm_response
491
self.confirm_response = None
495
def ui_notification(self, title, text):
496
self.notification_stack.append((title, text))
498
def pop_notification(self):
499
'''Return the last notification (title, text) tuple.'''
501
return self.notification_stack.pop()
503
def ui_download_start(self, url, total_size):
504
'''Create a progress dialog for a download of given URL.
506
total_size specifes the number of bytes to download, or -1 if it cannot
507
be determined. In this case the dialog should display an indeterminated
508
progress bar (bouncing back and forth).'''
511
self.cur_download = [url, 0, total_size]
513
def ui_download_progress(self, current_size, total_size):
514
'''Update download progress of current download.
516
This should return True to cancel the current download, and False
519
assert self.cur_download[0]
520
assert self.cur_download[1] is not None
521
assert self.cur_download[2] is not None
522
assert current_size > self.cur_download[1]
523
assert total_size == self.cur_download[2]
525
self.cur_download[1] = current_size
527
return self.cancel_download and float(current_size)/total_size >= 0.3
529
def ui_download_finish(self):
530
'''Close the current download progress dialog.'''
532
self.cancel_download = False
534
#-------------------------------------------------------------------#
535
# TestOSLib consistency tests
537
import unittest, subprocess
539
class TestOSLibConsistencyTest(unittest.TestCase):
540
def test_modinfo_output(self):
541
'''test suite's modinfo output for known modules'''
543
m = subprocess.Popen([OSLib.inst.modinfo_path, 'vanilla'],
544
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
545
(out, err) = m.communicate()
546
self.assertEqual(err, '')
547
self.assertEqual(out, '''alias: pci:v00001001d*sv*sd*bc03sc*i*
548
description: free module with available hardware, graphics card
549
filename: /lib/modules/0.8.15/foo/vanilla.ko
552
self.assertEqual(m.returncode, 0)
554
m = subprocess.Popen([OSLib.inst.modinfo_path, 'chocolate'],
555
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
556
(out, err) = m.communicate()
557
self.assertEqual(err, '')
558
self.assertEqual(m.returncode, 0)
559
self.assert_('chocolate.ko' in out)
561
self.assert_('alias: pci:v00001002d00000001sv*' in out)
562
self.assert_('alias: pci:v00001002d00000002sv*' in out)
564
def test_modinfo_error(self):
565
'''test suite's modinfo output for unknown modules and invalid invocation'''
567
m = subprocess.Popen([OSLib.inst.modinfo_path, 'nonexisting'],
568
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
569
(out, err) = m.communicate()
570
self.assertEqual(out, '')
571
self.assert_('could not find module nonexisting' in err)
572
self.assertEqual(m.returncode, 1)
574
m = subprocess.Popen([OSLib.inst.modinfo_path],
575
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
576
(out, err) = m.communicate()
577
self.assertEqual(out, '')
578
self.assert_('Usage' in err)
579
self.assertEqual(m.returncode, 0)
581
def test_modprobe(self):
582
'''test suite's modprobe works'''
585
m = subprocess.Popen([OSLib.inst.modprobe_path],
586
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
587
(out, err) = m.communicate()
588
self.assertEqual(out, '')
589
self.assert_('Usage' in err)
590
self.assertEqual(m.returncode, 1)
593
m = subprocess.Popen([OSLib.inst.modprobe_path, 'nonexisting'],
594
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
595
(out, err) = m.communicate()
596
self.assertEqual(out, '')
597
self.assert_('FATAL' in err)
598
self.assert_('nonexisting' in err)
599
self.assertEqual(m.returncode, 1)
602
orig_content = open(OSLib.inst.proc_modules).read()
604
self.failIf('cherry' in orig_content)
605
m = subprocess.Popen([OSLib.inst.modprobe_path, 'cherry'],
606
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
607
(out, err) = m.communicate()
608
self.assertEqual(out, '')
609
self.assertEqual(err, '')
610
self.assertEqual(m.returncode, 0)
611
self.assert_('cherry' in open(OSLib.inst.proc_modules).read())
613
open(OSLib.inst.proc_modules, 'w').write(orig_content)
615
def test_sys_symlinks(self):
616
'''all symlinks in fake /sys are valid'''
618
for path, dirs, files in os.walk(os.path.join(OSLib.inst.workdir, 'sys')):
620
p = os.path.join(path, f)
621
if os.path.islink(p):
622
rp = os.path.realpath(p)
623
self.assert_(os.path.exists(rp),
624
'symbolic link %s -> %s is valid' % (p, rp))
626
def test_module_aliases_file(self):
627
'''module.aliases correctness'''
629
self.assertEqual(open(os.path.join(OSLib.inst.workdir, 'kernelmods',
630
'modules.alias')).read(),
631
'''# Aliases extracted from modules themselves.
632
alias pci:v0000EEEEd*sv*sd*bc02sc80i* cherry
633
alias pci:v00001002d00000001sv*bc*sc*i* chocolate
634
alias pci:v00001002d00000002sv*sd*bc*sc*i* chocolate
635
alias pci:v0000DEAFd*sv*sd*bc99sc*i* dullstd
636
alias pci:v0000AAAAd000012*sv*sd*bc02sc*i* mint
637
alias pci:v00001001d*sv*sd*bc03sc*i* vanilla
638
alias pci:v00001001d00003D*sv*sd*bc03sc*i* vanilla3d
641
def test_proc_modules(self):
642
'''/proc/modules correctness'''
644
m = open(OSLib.inst.proc_modules).read()
645
self.assertEqual(len(m.splitlines()), 2)
646
self.assert_('dullstd' in m)
647
self.assert_('vanilla' in m)
648
self.failIf('vanilla3d' in m)
649
self.failIf('cherry' in m)
650
self.failIf('mint' in m)
652
#-------------------------------------------------------------------#
656
from jockey.handlers import KernelModuleHandler
657
class AvailMod(KernelModuleHandler):
658
def __init__(self, ui):
659
KernelModuleHandler.__init__(self, ui, 'vanilla',
660
description='Test suite: available kernel module')
666
from jockey.handlers import KernelModuleHandler
667
class NotAvailMod(KernelModuleHandler):
668
def __init__(self, ui):
669
KernelModuleHandler.__init__(self, ui, 'chocolate',
670
description='Test suite: non-available kernel module')
676
class NoDetectMod(jockey.handlers.KernelModuleHandler):
677
def __init__(self, ui):
678
jockey.handlers.KernelModuleHandler.__init__(self, ui, 'spam',
679
description='Test suite: non-detectable kernel module')
682
# complete .py file contents with above three handlers
683
h_availability_py = 'import jockey.handlers\n%s\n%s\n%s\n' % (
684
h_avail_mod, h_notavail_mod, h_nodetectmod)
687
class NoChangeMod(jockey.handlers.KernelModuleHandler):
688
def __init__(self, ui):
689
jockey.handlers.KernelModuleHandler.__init__(self, ui, 'vanilla')
692
def can_change(self):
696
#-------------------------------------------------------------------#