#!/usr/bin/python
#
# udisks test suite
#
# Run in udisks built tree to test local built binaries (needs
# --localstatedir=/var), or from anywhere else to test system installed
# binaries.
#
# Usage:
# - Run all tests: 
#   tests/run  
# - Run only a particular class of tests:
#   tests/run Luks
# - Run only a single test:
#   tests/run FS.test_ext3

# Copyright: (C) 2009, 2010 Martin Pitt <martin.pitt@ubuntu.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# TODO:
# - Add hotplug stresstest: stop/rebuild array with some partitions in a loop,
#   and check added/removed devices and emitted signals
# - Add LUKS stresstest (was racy in the past)
# - Test LinuxMd* D-BUS interface

import subprocess
import os
import unittest
import sys
import tempfile
import atexit
import time
import shutil
import dbus
import signal
import optparse
import re
from glob import glob

VDEV_SIZE = 300000000 # size of virtual test device

# Those file systems are known to have a broken handling of permissions, in
# particular the executable bit
BROKEN_PERMISSIONS_FS = ['ntfs']

# Some D-BUS API methods cause properties to not be up to date yet when a
# method call finishes, thus we do an udevadm settle as a workaround. Those
# methods should eventually get fixed properly, but it's unnerving to have
# the tests fail on them when you are working on something else. This flag
# gets set by the --no-workarounds option to disable those syncs, so that these
# race conditions can be fixed.
disable_dbus_udev_syncs = False

I_D = 'org.freedesktop.UDisks.Device'

# ----------------------------------------------------------------------------

class UDisksTestCase(unittest.TestCase):
    '''Base class for udisks test cases.
    
    This provides static functions which are useful for all test cases.
    '''

    tool_path = None
    daemon = None
    daemon_log = None
    device = None

    manager_obj = None
    manager_props = None

    @classmethod
    def init(klass, logfile=None):
        '''start daemon and set up test environment'''

        assert os.geteuid() == 0, 'need to be root for running this'

        # run from local build tree if we are in one, otherwise use system instance
        if (os.access ('src/udisks-daemon', os.X_OK)):
            daemon_path = 'src/udisks-daemon'
            daemon_args = ['--helper-dir', os.path.join(os.getcwd(), 'src',
                'helpers')]
            klass.tool_path = 'tools/udisks'
            print('Testing binaries from local build tree')
            klass.check_build_tree_config()
        else:
            print('Testing installed system binaries')
            daemon_path = None
            for l in open('/usr/share/dbus-1/system-services/org.freedesktop.UDisks.service'):
                if l.startswith('Exec='):
                    daemon_path = l.split('=', 1)[1].strip()
                    break
            assert daemon_path, 'could not determine daemon path from D-BUS .service file'

            daemon_args = []
            klass.tool_path = 'udisks'

        print('daemon path: ' + daemon_path)

        klass.device = klass.setup_vdev()

        # inhibit GNOME automounting/nautilus pop ups
        subprocess.call(['killall', '-STOP', 'gvfs-gdu-volume-monitor'])

        # start daemon
        if logfile:
            klass.daemon_log = open(logfile, 'w')
        else:
            klass.daemon_log = tempfile.TemporaryFile()
        klass.daemon = subprocess.Popen([daemon_path, '--replace'] + daemon_args,
            stdout=klass.daemon_log, stderr=subprocess.STDOUT)
        assert klass.daemon.pid, 'daemon failed to start'
        time.sleep(0.5) # give it some time to settle

        atexit.register(klass.cleanup)

        obj = dbus.SystemBus().get_object('org.freedesktop.UDisks',
            '/org/freedesktop/UDisks')
        klass.manager_iface = dbus.Interface(obj, 'org.freedesktop.UDisks')
        klass.manager_props = dbus.Interface(obj, dbus.PROPERTIES_IFACE)

    @classmethod
    def cleanup(klass):
        '''stop daemon again and clean up test environment'''

        subprocess.call(['umount', klass.device], stderr=subprocess.PIPE) # if a test failed

        os.kill(klass.daemon.pid, signal.SIGTERM)
        os.wait()
        klass.daemon = None

        klass.teardown_vdev(klass.device)
        klass.device = None

        # resume GNOME automounting/nautilus pop ups
        subprocess.call(['killall', '-CONT', 'gvfs-gdu-volume-monitor'])

    @classmethod
    def sync(klass):
        '''Wait until pending events finished processing.
        
        This should only be called for situations where we genuinely have an
        asynchronous response, like invoking a CLI program and waiting for
        udev/udisks to catch up on the change events.
        '''
        subprocess.call(['udevadm', 'settle'])

    @classmethod
    def sync_workaround(klass):
        '''Wait until pending events finished processing (bug workaround).
        
        This should be called for race conditions in the D-BUS API which cause
        properties to not be up to date yet when a method call finishes. Those
        should eventually get fixed properly, but it's unnerving to have the
        tests fail on them when you are working on something else.

        This sync is not done if running with --no-workarounds.
        '''
        if not disable_dbus_udev_syncs:
            subprocess.call(['udevadm', 'settle'])

    @classmethod
    def zero_device(klass):
        subprocess.call(['dd', 'if=/dev/zero', 'of='+klass.device, 'bs=10M'],
                stderr=subprocess.PIPE)
        klass.sync()

    @classmethod
    def devname(klass, partition=None):
        '''Get name of test device or one of its partitions'''

        if partition:
            if klass.device[-1].isdigit():
                return klass.device + 'p' + str(partition)
            else:
                return klass.device + str(partition)
        else:
            return klass.device

    @classmethod
    def partition_obj(klass, partition=None):
        '''Get D-Bus object of test device or one of its partitions'''

        p = '/org/freedesktop/UDisks/devices/' + \
            os.path.basename(klass.devname(partition))
        if partition:
            p += 'p' + str(partition)

        return dbus.SystemBus().get_object('org.freedesktop.UDisks', p)

    @classmethod
    def partition_iface(klass, partition=None):
        '''Get D-Bus Disks interface of test device or one of its partitions'''

        return dbus.Interface(klass.partition_obj(partition), I_D)

    @classmethod
    def partition_props(klass, partition=None):
        '''Get D-Bus Disks properties of test device or one of its partitions'''

        return dbus.Interface(klass.partition_obj(partition),
                dbus.PROPERTIES_IFACE)

    @classmethod
    def get_info(klass, partition=None, devname=None):
        '''Return udisks --info in a dictionary.
        
        If no partition number is given, this queries device. If devname is
        given, info for that is returned instead.
        '''
        info = subprocess.Popen([klass.tool_path, '--show-info',
            devname or klass.devname(partition)],
                stdout=subprocess.PIPE)
        out = info.communicate()[0]
        assert info.returncode == 0, 'udisks --info failed'

        props = {}
        prefix = ''
        for l in out.splitlines():
            if not l.startswith('  ') or not ':' in l:
                continue

            if l.startswith('  linux md:'):
                prefix = 'md_'
                continue
            elif l.startswith('  partition table:'):
                prefix = 'partition_'
                continue
            if prefix and not l.startswith('    '):
                prefix = ''

            (k, v) = l.split(':', 1)
            props[prefix + k.strip()] = v.strip()

        return props

    @classmethod
    def get_uuid(klass, partition=None):
        '''Use blkid to determine UUID.'''

        uuid = None
        blkid = subprocess.Popen(['blkid', '-p', '-o', 'udev', 
            klass.devname(partition)], stdout=subprocess.PIPE)
        for l in blkid.stdout:
            if l.startswith('ID_FS_UUID='):
                uuid = l.split('=', 1)[1].strip()
        assert blkid.wait() == 0
        return uuid

    @classmethod
    def get_partitions(klass):
        '''Return list of test device partitions known to udisks.'''

        info = subprocess.Popen([klass.tool_path, '--enumerate-device-files'],
                stdout=subprocess.PIPE)
        out = info.communicate()[0]
        assert info.returncode == 0, 'udisks --enumerate-device-files failed'

        partitions = []
        for l in out.splitlines():
            l = l.strip()
            if l.startswith(klass.device) and l != klass.device:
                partitions.append(l[len(klass.device):])
        return partitions

    @classmethod
    def mkfs(klass, type, label=None, partition=None):
        '''Create file system using mkfs.'''

        if type == 'minix':
            assert label is None, 'minix does not support labels'

        mkcmd =     { 'swap': 'mkswap',
                    }
        label_opt = { 'vfat': '-n', 
                      'reiserfs': '-l',
                    }
        extra_opt = { 'vfat': [ '-I', '-F', '32'],
                      'swap': ['-f'],
                      'xfs': ['-f'], # XFS complains if there's an existing FS, so force
                      'ext2': ['-F'], # ext* complains about using entire device, so force
                      'ext3': ['-F'],
                      'ext4': ['-F'],
                      'ntfs': ['-F'],
                      'reiserfs': ['-f', '-q'],
                    }

        cmd = [mkcmd.get(type, 'mkfs.' + type)] + extra_opt.get(type, [])
        if label:
            cmd += [label_opt.get(type, '-L'), label]
        cmd.append(klass.devname(partition))

        assert subprocess.call(cmd, stdout=subprocess.PIPE,
                stderr=subprocess.PIPE) == 0

        # kernel/udev generally detect those changes itself, but do not quite
        # tell us when they are done; so do a little kludge here to know how
        # long we need to wait
        subprocess.call(['udevadm', 'trigger', '--action=change',
            '--sysname-match=' + os.path.basename(klass.devname(partition))])
        klass.sync()

    @classmethod
    def fs_create(klass, partition, type, options):
        '''Call FilesystemCreate() on partition with given type and options.'''

        klass.partition_iface(partition).FilesystemCreate(type, options)
        # .FilesystemCreate() already blocks until the mkfs job is done; but
        # without udevsettle the property updating is racy
        klass.sync_workaround()

    @classmethod
    def retry_busy(klass, fn, *args):
        '''Call a function until it does not fail with "Busy".'''

        timeout = 10
        while timeout >= 0:
            try:
                return fn(*args)
            except dbus.DBusException as e:
                if e._dbus_error_name != 'org.freedesktop.UDisks.Error.Busy':
                    raise
                sys.stderr.write('[busy] ')
                time.sleep(0.3)
                timeout -= 1

    @classmethod
    def check_build_tree_config(klass):
        '''Check configuration of build tree'''

        # read make variables
        make_vars = {}
        var_re = re.compile('^([a-zA-Z_]+) = (.*)$')
        make = subprocess.Popen(['make', '-p', '/dev/null'],
                stdout=subprocess.PIPE)
        for l in make.stdout:
            m = var_re.match(l)
            if m:
                make_vars[m.group(1)] = m.group(2)
        make.wait()

        # expand make variables
        subst_re = re.compile('\${([a-zA-Z_]+)}')
        for (k, v) in make_vars.items():
            while True:
                m = subst_re.search(v)
                if m:
                    v = subst_re.sub(make_vars.get(m.group(1), ''), v)
                    make_vars[k] = v
                else:
                    break

        # check localstatedir
        for d in (os.path.join(make_vars['localstatedir'], 'run', 'udisks'),
                os.path.join(make_vars['localstatedir'], 'lib', 'udisks')):
            if not os.path.exists(d):
                sys.stderr.write('The directory %s does not exist; please create it before running these tests.\n' % d)
                sys.exit(0)
        
    @classmethod
    def setup_vdev(klass):
        '''create virtual test device
        
        It is zeroed out initially.

        Return the device path.
        '''
        # ensure that the scsi_debug module is loaded
        if os.path.isdir('/sys/module/scsi_debug'):
            sys.stderr.write('The scsi_debug module is already loaded; please remove before running this test.\n')
            sys.exit(1)

        assert subprocess.call(['modprobe', 'scsi_debug', 'dev_size_mb=%i' % (
            VDEV_SIZE/1048576)]) == 0, 'Failure to modprobe scsi_debug'

        # wait until all drives are created
        dirs = []
        while len(dirs) < 1:
            dirs = glob('/sys/bus/pseudo/drivers/scsi_debug/adapter*/host*/target*/*:*/block')
            time.sleep(0.1)
        assert len(dirs) == 1

        # determine the debug block devices
        devs = os.listdir(dirs[0])
        assert len(devs) == 1
        dev = '/dev/' + devs[0]
        assert os.path.exists(dev)

        # let's be 100% sure that we pick a virtual one
        assert open('/sys/block/%s/device/model' % devs[0]).read().strip() == 'scsi_debug'

        print('Set up test device: ' + dev)
        return dev

    @classmethod
    def teardown_vdev(klass, device):
        '''release and remove virtual test device'''

        klass.remove_device(device)
        assert subprocess.call(['rmmod', 'scsi_debug']) == 0, \
                'Failure to rmmod scsi_debug'

    @classmethod
    def remove_device(klass, device):
        '''remove virtual test device'''

        device = device.split('/')[-1]
        if os.path.exists('/sys/block/' + device):
            f = open('/sys/block/%s/device/delete' % device, 'w')
            f.write('1')
            f.close()
        while os.path.exists(device):
            time.sleep(0.1)
        klass.sync()
        time.sleep(0.5) # TODO

    @classmethod
    def readd_devices(klass):
        '''re-add virtual test devices after removal'''

        scan_files = glob('/sys/bus/pseudo/devices/adapter*/host*/scsi_host/host*/scan')
        assert len(scan_files) > 0
        for f in scan_files:
            open(f, 'w').write('- - -\n')
        while not os.path.exists(klass.device):
            time.sleep(0.1)
        time.sleep(0.5)
        klass.sync()

# ----------------------------------------------------------------------------

class FS(UDisksTestCase):
    '''Test detection of all supported file systems'''

    def setUp(self):
        self.workdir = tempfile.mkdtemp()

    def tearDown(self):
        if subprocess.call(['umount', self.device], stderr=subprocess.PIPE) == 0:
            sys.stderr.write('[cleanup unmount] ')
        shutil.rmtree (self.workdir)

    def test_zero(self):
        '''properties of zeroed out device'''

        self.zero_device()
        info = self.get_info()
        self.assertEqual(info['is mounted'], '0')
        self.assertEqual(info['mount paths'], '')
        self.assertEqual(info['presentation hide'], '0')
        self.assertEqual(info['presentation name'], '')
        self.assertEqual(info['usage'], '')
        self.assertEqual(info['type'], '')
        self.assertEqual(info['uuid'], '')
        self.assertEqual(info['label'], '')

        self.assertEqual(self.get_partitions(), [])

    def test_ext2(self):
        '''fs: ext2'''
        self._do_fs_check('ext2')

    def test_ext3(self):
        '''fs: ext3'''
        self._do_fs_check('ext3')

    def test_ext4(self):
        '''fs: ext4'''
        self._do_fs_check('ext4')

    def test_btrfs(self):
        '''fs: btrfs'''
        self._do_fs_check('btrfs')

    def test_minix(self):
        '''fs: minix'''
        self._do_fs_check('minix')

    def test_xfs(self):
        '''fs: XFS'''
        self._do_fs_check('xfs')

    def test_ntfs(self):
        '''fs: NTFS'''
        self._do_fs_check('ntfs')

    def test_vfat(self):
        '''fs: FAT'''
        self._do_fs_check('vfat')

    def test_reiserfs(self):
        '''fs: reiserfs'''
        self._do_fs_check('reiserfs')

    def test_swap(self):
        '''fs: swap'''
        self._do_fs_check('swap')

    def test_nilfs2(self):
        '''fs: nilfs2'''
        self._do_fs_check('nilfs2')

    def test_force_removal(self):
        '''fs: forced removal'''

        # create a fs and mount it
        self.fs_create(None, 'ext4', ['label=udiskstest'])
        mount_path = self.partition_iface().FilesystemMount('', [])
        self.assertEqual(mount_path, '/media/udiskstest')

        # removal should clean up mounts
        self.remove_device(self.device)
        self.assertFalse(os.path.exists(mount_path))
        self.assertFalse(self.device in self.manager_iface.EnumerateDeviceFiles())
        props = self.partition_props()
        self.assertRaises(dbus.DBusException, props.Get, I_D, 'DeviceIsPartition')

        # after putting it back, it should be mountable again
        self.readd_devices()
        self.assertTrue(self.device in self.manager_iface.EnumerateDeviceFiles())
        props = self.partition_props()
        self.assertEqual(props.Get(I_D, 'DeviceIsPartition'), False)
        self.assertEqual(props.Get(I_D, 'DeviceIsMounted'), False)

        mount_path = self.partition_iface().FilesystemMount('', [])
        self.assertEqual(mount_path, '/media/udiskstest')
        self.assertEqual(props.Get(I_D, 'DeviceIsMounted'), True)

        self.retry_busy(self.partition_iface().FilesystemUnmount, [])

    def _do_fs_check(self, type):
        '''Run checks for a particular file system.'''

        if type != 'swap' and subprocess.call(['which', 'mkfs.' + type],
                stdout=subprocess.PIPE) != 0:
            sys.stderr.write('[no mkfs.%s, skip] ' % type)

            # check correct D-Bus exception
            try:
                self.fs_create(None, type, [])
                self.fail('Expected failure for missing mkfs.' + type)
            except dbus.DBusException as e:
                self.assertEqual(e._dbus_error_name,
                        'org.freedesktop.UDisks.Error.FilesystemToolsMissing',
                        str(e))

            return

        # do checks with command line tools (mkfs/mount/umount)
        sys.stderr.write('[cli] ')

        self._do_mkfs_check(type)
        if type != 'minix':
            self._do_mkfs_check(type, 'test%stst' % type)

        # put a different fs here instead of zeroing, so that we verify that
        # DK-D overrides existing FS (e. g. XFS complains then), and does not
        # leave traces of other FS around
        if type == 'ext3':
            self.mkfs('swap')
        else:
            self.mkfs('ext3')

        # do checks with DK-Disks D-BUS operations
        sys.stderr.write('[ud] ')
        self._do_dbus_fs_check(type)
        if type != 'minix':
            self._do_dbus_fs_check(type, 'test%stst' % type)

    def _do_mkfs_check(self, type, label=None):
        '''Run mkfs/mount/umount check for a fs and label.
        
        This checks that DK-Disks correctly picks up command line too actions.
        '''
        self.mkfs(type, label)
        i = self.get_info()

        self.assertEqual(i['usage'], (type == 'swap') and 'other' or 'filesystem')

        self.assertEqual(i['type'], type)
        self.assertEqual(i['label'], label or '')
        self.assertFalse ('partition_scheme' in i)
        if type != 'swap':
            self.assertEqual(i['is mounted'], '0')
            self.assertEqual(i['mount paths'], '')
        self.assertEqual(i['presentation name'], '')
        if type != 'minix':
            self.assertEqual(i['uuid'], self.get_uuid())

        if type == 'swap':
            return

        # mount it using "mount"
        if type == 'ntfs' and subprocess.call(['which', 'mount.ntfs-3g'],
                stdout=subprocess.PIPE) == 0:
            # prefer mount.ntfs-3g if we have it (on Debian; Ubuntu
            # defaults to ntfs-3g if installed); TODO: check other distros
            mount_prog = 'mount.ntfs-3g'
        else:
            mount_prog = 'mount'
        ret = subprocess.call([mount_prog, self.device, self.workdir])

        if ret == 32:
            # missing fs driver
            sys.stderr.write('[missing kernel driver, skip] ')
            return

        self.assertEqual(ret, 0)
        i = self.get_info()
        self.assertEqual(i['is mounted'], '1')
        self.assertEqual(i['mount paths'], self.workdir)

        # unmount it using "umount"
        subprocess.call(['umount', self.workdir])
        i = self.get_info()
        self.assertEqual(i['is mounted'], '0')
        self.assertEqual(i['mount paths'], '')

    def _do_dbus_fs_check(self, type, label=None):
        '''Run DK-D FSCreate/Mount/Unmount check for a fs and label.
        
        This checks the D-Bus methods that DK-Disks offers.
        '''
        # check that DK-disks reports the fs as supported
        for fs in self.manager_props.Get('org.freedesktop.UDisks',
                'KnownFilesystems'):
            if fs[0] == type:
                supports_unix_owners = fs[2]
                self.assertTrue(supports_unix_owners in (True, False))
                self.assertEqual(fs[3], type != 'swap') # can_mount
                self.assertTrue(fs[4]) # can_create
                supports_label_rename = fs[6]
                # minix does not support labels; EXFAIL: swap, btrfs don't have a program for it
                self.assertEqual(supports_label_rename, type not in ('btrfs', 'minix', 'swap'))
                break
        else:
            self.fail('KnownFilesystems does not contain ' + type)

        options = []
        if label:
            options.append('label=' + label)

        # create fs
        self.fs_create(None, type, options)
        i = self.get_info()

        self.assertEqual(i['usage'], (type == 'swap') and 'other' or 'filesystem')
        self.assertEqual(i['type'], type)
        self.assertEqual(i['label'], label or '')
        if type != 'swap':
            self.assertEqual(i['is mounted'], '0')
            self.assertEqual(i['mount paths'], '')
        self.assertEqual(i['presentation name'], '')
        self.assertFalse ('partition_scheme' in i)
        if type != 'minix':
            self.assertEqual(i['uuid'], self.get_uuid())

        # open files when unmounted
        iface = self.partition_iface()
        self.assertRaises(dbus.DBusException,
                iface.FilesystemListOpenFiles)

        if type != 'swap':
            # mount
            try:
                mount_path = iface.FilesystemMount('', [])
            except dbus.DBusException as e:
                self.assertEqual(e._dbus_error_name,
                        'org.freedesktop.UDisks.Error.FilesystemDriverMissing',
                        str(e))
                sys.stderr.write('[missing kernel driver, skip] ')
                return

            if label:
                self.assertEqual(mount_path, '/media/' + label)
            else:
                self.assertTrue(mount_path.startswith('/media/'))
            i = self.get_info()
            self.assertEqual(i['is mounted'], '1')
            self.assertEqual(i['mount paths'], mount_path)

            # no ownership taken, should be root owned
            st = os.stat(mount_path)
            self.assertEqual((st.st_uid, st.st_gid), (0, 0))

            # open files when mounted
            num_open_file = 0
            if type == 'nilfs2':
                # garbage collector opens /.nilfs file
                num_open_file = 1

            result = iface.FilesystemListOpenFiles()
            self.assertEqual(len(result), num_open_file)

            f = open(os.path.join(mount_path, 'test.txt'), 'w')
            num_open_file += 1
            result = iface.FilesystemListOpenFiles()
            self.assertEqual(len(result), num_open_file)
            self.assertEqual(result[0][0], os.getpid())
            self.assertEqual(result[0][1], os.geteuid())
            self.assertTrue(sys.argv[0] in result[0][2])
            f.close()
            num_open_file -= 1

            result = iface.FilesystemListOpenFiles()
            self.assertEqual(len(result), num_open_file)

            self._do_file_perms_checks(type, mount_path)
            # unmount
            self.retry_busy(self.partition_iface().FilesystemUnmount, [])
            self.assertFalse(os.path.exists(mount_path), 'mount point was not removed')

            i = self.get_info()
            self.assertEqual(i['is mounted'], '0')
            self.assertEqual(i['mount paths'], '')

            # create fs with taking ownership (daemon:mail == 1:8)
            if supports_unix_owners:
                options.append('take_ownership_uid=1')
                options.append('take_ownership_gid=8')
                self.fs_create(None, type, options)
                mount_path = iface.FilesystemMount('', [])
                st = os.stat(mount_path)
                self.assertEqual((st.st_uid, st.st_gid), (1, 8))
                self.retry_busy(self.partition_iface().FilesystemUnmount, [])
                self.assertFalse(os.path.exists(mount_path), 'mount point was not removed')

        # change label
        if supports_label_rename:
            l = 'n"a\m\\"e' + type
            if type == 'vfat':
                # VFAT does not support some characters
                self.assertRaises(dbus.DBusException, iface.FilesystemSetLabel, l)
                l = "n@a$me"
            iface.FilesystemSetLabel(l)
            self.sync_workaround()
            i = self.get_info()
            if type == 'vfat':
                # EXFAIL: often (but not always) the label appears in all upper case
                self.assertEqual(i['label'].upper(), l.upper())
            else:
                self.assertEqual(i['label'], l)
        else:
            self.assertRaises(dbus.DBusException, iface.FilesystemSetLabel, 'foo')

        # check fs
        if type != 'swap':
            self.assertEqual(iface.FilesystemCheck([]), True)

    def _do_file_perms_checks(self, type, mount_point):
        '''Check for permissions for data files and executables.

        This particularly checks sane and useful permissions on non-Unix file
        systems like vfat.
        '''
        if type in BROKEN_PERMISSIONS_FS:
            return

        f = os.path.join(mount_point, 'simpledata.txt')
        open(f, 'w').close()
        self.assertTrue(os.access(f, os.R_OK))
        self.assertTrue(os.access(f, os.W_OK))
        self.assertFalse(os.access(f, os.X_OK))

        f = os.path.join(mount_point, 'simple.exe')
        shutil.copy('/bin/bash', f)
        self.assertTrue(os.access(f, os.R_OK))
        self.assertTrue(os.access(f, os.W_OK))
        self.assertTrue(os.access(f, os.X_OK))

        os.mkdir(os.path.join(mount_point, 'subdir'))
        f = os.path.join(mount_point, 'subdir', 'subdirdata.txt')
        open(f, 'w').close()
        self.assertTrue(os.access(f, os.R_OK))
        self.assertTrue(os.access(f, os.W_OK))
        self.assertFalse(os.access(f, os.X_OK))

        f = os.path.join(mount_point, 'subdir', 'subdir.exe')
        shutil.copy('/bin/bash', f)
        self.assertTrue(os.access(f, os.R_OK))
        self.assertTrue(os.access(f, os.W_OK))
        self.assertTrue(os.access(f, os.X_OK))


# ----------------------------------------------------------------------------

class Luks(UDisksTestCase):
    '''Check LUKS.'''

    def test_0_create_teardown(self):
        '''LUKS create/teardown'''

        self.fs_create(None, 'ext3', ['luks_encrypt=s3kr1t', 'label=treasure'])

        try:
            # check crypted device info
            i = self.get_info() 
            self.assertEqual(i['usage'], 'crypto')
            self.assertEqual(i['type'], 'crypto_LUKS')
            self.assertEqual(i['label'], '') # encrypted device
            self.assertEqual(i['is mounted'], '0')
            self.assertEqual(i['mount paths'], '')
            self.assertEqual(i['presentation name'], '')
            self.assertTrue(i['holder'].startswith('/org/freedesktop/UDisks/devices/'))
            self.assertEqual(i['uuid'], self.get_uuid())

            # check crypted device properties
            crypt_props = self.partition_props()
            self.assertEqual(crypt_props.Get(I_D, 'DeviceIsLuks'), True)
            self.assertEqual(crypt_props.Get(I_D, 'DeviceIsLuksCleartext'), False)
            self.assertEqual(crypt_props.Get(I_D, 'LuksHolder'), i['holder'])

            # check cleartext device properties
            clear_obj = dbus.SystemBus().get_object('org.freedesktop.UDisks', i['holder'])
            clear_props = dbus.Interface(clear_obj, dbus.PROPERTIES_IFACE)
            self.assertEqual(clear_props.Get(I_D, 'DeviceIsLuks'), False)
            self.assertEqual(clear_props.Get(I_D, 'DeviceIsLuksCleartext'), True)
            self.assertEqual(clear_props.Get(I_D, 'LuksCleartextUnlockedByUid'), 0)
            self.assertTrue(clear_props.Get(I_D, 'LuksCleartextSlave').endswith('/' +
                os.path.basename(self.device)))

            # check cleartext device info
            self.assertTrue(i['holder'] in self.manager_iface.EnumerateDevices())
            clear_devname = clear_props.Get(I_D, 'DeviceFile')
            ci = self.get_info(devname=clear_devname)
            self.assertTrue(os.path.exists(clear_devname))
            self.assertEqual(ci['usage'], 'filesystem')
            self.assertEqual(ci['type'], 'ext3')
            self.assertEqual(ci['label'], 'treasure')
            self.assertEqual(ci['is mounted'], '0')
            self.assertEqual(ci['mount paths'], '')
            self.assertEqual(ci['presentation name'], '')

            # check that we do not leak key information
            udev_dump = subprocess.Popen(['udevadm', 'info', '--export-db'],
                    stdout=subprocess.PIPE)
            out = udev_dump.communicate()[0]
            self.assertFalse('essiv:sha' in out, 'key information in udev properties')

        finally:
            # tear down cleartext device
            self.partition_iface().LuksLock([])
            self.assertFalse(i['holder'] in self.manager_iface.EnumerateDevices())
            self.assertFalse(os.path.exists(clear_devname))
            self.assertRaises(dbus.DBusException, clear_props.Get, I_D, 'DeviceFile')

    def test_luks_mount(self):
        '''LUKS mount/unmount'''

        # wrong password
        self.assertRaises(dbus.DBusException,
                self.partition_iface().LuksUnlock, 'h4ck3rz', [])

        # correct password
        clear_objpath = self.retry_busy(self.partition_iface().LuksUnlock, 's3kr1t', [])
        self.assertTrue(clear_objpath in self.manager_iface.EnumerateDevices())

        clear_obj = dbus.SystemBus().get_object('org.freedesktop.UDisks',
                clear_objpath)
        clear_props = dbus.Interface(clear_obj, dbus.PROPERTIES_IFACE)
        clear_iface = dbus.Interface(clear_obj, I_D)

        # mount
        mount_path = clear_iface.FilesystemMount('', [])
        clear_devname = clear_props.Get(I_D, 'DeviceFile')
        self.assertEqual(mount_path, '/media/treasure')

        i = self.get_info(devname=clear_devname)
        self.assertEqual(i['is mounted'], '1')
        self.assertEqual(i['mount paths'], mount_path)

        # can't lock, busy
        self.assertRaises(dbus.DBusException, self.partition_iface().LuksLock, [])

        # umount
        self.retry_busy(clear_iface.FilesystemUnmount, [])
        self.assertFalse(os.path.exists(mount_path), 'mount point was not removed')
        i = self.get_info()
        self.assertEqual(i['is mounted'], '0')
        self.assertEqual(i['mount paths'], '')

        # lock
        self.partition_iface().LuksLock([])
        self.assertFalse(clear_objpath in self.manager_iface.EnumerateDevices())

    def test_luks_forced_removal(self):
        '''LUKS forced removal'''

        # unlock and mount it
        clear_objpath = self.retry_busy(self.partition_iface().LuksUnlock, 's3kr1t', [])
        self.assertTrue(clear_objpath in self.manager_iface.EnumerateDevices())

        clear_obj = dbus.SystemBus().get_object('org.freedesktop.UDisks',
                clear_objpath)
        mount_path = dbus.Interface(clear_obj, I_D).FilesystemMount('', [])
        self.assertEqual(mount_path, '/media/treasure')

        # removal should clean up mounts
        self.remove_device(self.device)
        self.assertFalse(os.path.exists(mount_path))
        self.assertFalse(clear_objpath in self.manager_iface.EnumerateDevices())

        # after putting it back, it should be mountable again
        self.readd_devices()

        clear_objpath = self.retry_busy(self.partition_iface().LuksUnlock, 's3kr1t', [])
        self.assertTrue(clear_objpath in self.manager_iface.EnumerateDevices())

        clear_obj = dbus.SystemBus().get_object('org.freedesktop.UDisks',
                clear_objpath)
        clear_iface = dbus.Interface(clear_obj, I_D)
        mount_path = clear_iface.FilesystemMount('', [])
        self.assertEqual(mount_path, '/media/treasure')

        # umount
        self.retry_busy(clear_iface.FilesystemUnmount, [])
        self.assertFalse(os.path.exists(mount_path), 'mount point was not removed')
        i = self.get_info()
        self.assertEqual(i['is mounted'], '0')
        self.assertEqual(i['mount paths'], '')

        # lock
        self.partition_iface().LuksLock([])
        self.assertFalse(clear_objpath in self.manager_iface.EnumerateDevices())

    def test_luks_change_passphrase(self):
        '''LUKS change passphrase'''

        # wrong password
        self.assertRaises(dbus.DBusException,
                self.partition_iface().LuksChangePassphrase, 'h4ck3rz', 'foo')
        self.assertEqual(self.partition_props().Get(I_D, 'LuksHolder'), '/',
                'changing passphrase does not unlock')

        # correct password
        self.partition_iface().LuksChangePassphrase('s3kr1t', 'cl4ss1f13d')
        holder = self.partition_props().Get(I_D, 'LuksHolder')
        self.assertEqual(holder, '/', 
                'changing passphrase does not unlock: ' + holder)

        # old password is invalid now
        self.assertRaises(dbus.DBusException,
                self.partition_iface().LuksUnlock, 's3kr1t', [])

        # new password is accepted
        self.partition_iface().LuksUnlock('cl4ss1f13d', [])
        self.partition_iface().LuksLock([])

        # change it back so that order of tests does not matter
        self.partition_iface().LuksChangePassphrase('cl4ss1f13d', 's3kr1t')

        self.sync_workaround()

# ----------------------------------------------------------------------------

class Partitions(UDisksTestCase):
    '''Check partition operations.'''

    def setUp(self):
        self.partition_iface().PartitionTableCreate('none', [])
        self.assertEqual(self.get_partitions(), [])

        info = self.get_info()
        self.assertEqual([k for k in info if k.startswith('partition_')], [])
        self.assertEqual(self.fdisk_list(), None)

    def tearDown(self):
        self.partition_iface().PartitionTableCreate('none', [])
        info = self.get_info()
        self.assertEqual([k for k in info if k.startswith('partition_')], [])
        self.assertEqual(self.fdisk_list(), None)

    def test_mbr_nofs(self):
        '''Partitions: mbr (no file system)'''

        self._do_schema('mbr', '0x82', '0x06', 'boot', filesystem=False)

    def test_mbr_fs(self):
        '''Partitions: mbr (with file system)'''

        self._do_schema('mbr', '0x82', '0x06', 'boot', filesystem=True)

    def test_gpt_nofs(self):
        '''Partitions: GUID (no file system)'''

        self._do_schema('gpt', 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7', 
                '0657FD6D-A4AB-43C4-84E5-0933C84B4F4F', 'required',
                filesystem=False)

    def test_gpt_fs(self):
        '''Partitions: GUID (with file system)'''

        self._do_schema('gpt', 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7', 
                '0657FD6D-A4AB-43C4-84E5-0933C84B4F4F', 'required',
                filesystem=True)

    # TODO: fails in various ways
    def disabled_test_apm(self):
        '''Partitions: Apple'''

        try:
            self._do_schema('apm', 'Apple_Unix_SVR2', 'Foo', 'allow_write',
                    exp_default_partitions=2) # Apple creates bootstrap stuff by default
            self.fail('creating apple partition at offset 0 should fail due to default bootstrap partitions')
        except dbus.DBusException as e:
            self.assertTrue("Can't have overlapping partitions." in str(e))

        self._do_schema('apm', 'Apple_Unix_SVR2', 'Foo', 'allow_write',
                exp_default_partitions=2, # Apple creates bootstrap stuff by default
                first_offset=3000000)

    def _do_schema(self, schema, type1, type2, flag,
            exp_default_partitions=0, first_offset=0, filesystem=False):
        '''Run tests for a particular schema/type'''

        # create partition table
        self.partition_iface().PartitionTableCreate(schema, [])
        self.sync_workaround()

        info = self.get_info()
        self.assertEqual(info['partition_scheme'], schema)
        self.assertEqual(info['partition_count'], str(exp_default_partitions))
        self.assertEqual(len(self.get_partitions()), exp_default_partitions)

        if schema == 'mbr':
            self.assertEqual(self.fdisk_list(), [])

        # check device object properties
        props = self.partition_props()
        self.assertEqual(props.Get(I_D, 'DeviceIsPartition'), False)
        self.assertEqual(props.Get(I_D, 'DeviceIsPartitionTable'), True)
        self.assertEqual(props.Get(I_D, 'PartitionTableScheme'), schema)
        self.assertEqual(props.Get(I_D, 'PartitionTableCount'), exp_default_partitions)

        # p1: non-flagged
        if filesystem:
            p1 = self.partition_iface().PartitionCreate(first_offset, 10000000,
                    type1, '', [], [], 'vfat', ['label=testp1'])
        else:
            p1 = self.partition_iface().PartitionCreate(first_offset, 10000000,
                    type1, '', [], [], '', [])
        self.assertTrue(p1 in self.manager_iface.EnumerateDevices())
 
        # udisks internally sets the new partition flags from the
        # PartitionCreate() call; trigger udev to ensure that we actually read
        # and check the values from the udev probers
        subprocess.call(['udevadm', 'trigger', '--action=change', '--subsystem-match=block'])
        self.sync()

        if schema == 'mbr':
            fdisk = self.fdisk_list()
            self.assertEqual(len(fdisk), 1)
            fdisk = fdisk[0]
            self.assertTrue(os.path.exists(fdisk[0]), 'p1: device file does not exist')
            self.assertEqual(fdisk[1], False, 'p1 is bootable')
            self.assertEqual(fdisk[5], type1.lstrip('0x'))

        # EXFAIL: /dev/md5p1 still works and appears, but kernel/udev never
        # create /dev/md5p2 and following
        #self.partition_iface().PartitionCreate(0, 10000000, type2, '',
        #        [flag], [], '', [])
        #print (self.fdisk_list())

        # the device is not a partition, so calls should fail
        self.assertRaises(dbus.DBusException,
                self.partition_iface().PartitionDelete, [])
        self.assertRaises(dbus.DBusException,
                self.partition_iface().PartitionModify, type2, '', [flag])

        # check p1 object properties
        p1_obj = dbus.SystemBus().get_object('org.freedesktop.UDisks',
            p1)
        p1_iface = dbus.Interface(p1_obj, I_D)
        p1_props = dbus.Interface(p1_obj, dbus.PROPERTIES_IFACE)

        self.assertEqual(p1_props.Get(I_D, 'DeviceIsPartition'), True)
        self.assertEqual(p1_props.Get(I_D, 'DeviceIsPartitionTable'), False)
        self.assertEqual(p1_props.Get(I_D, 'PartitionSlave'), 
                '/org/freedesktop/UDisks/devices/' +
                os.path.basename(self.device))
        self.assertEqual(p1_props.Get(I_D, 'PartitionScheme'), schema)
        self.assertEqual(p1_props.Get(I_D, 'PartitionType'), type1)
        self.assertEqual(p1_props.Get(I_D, 'PartitionLabel'), (schema == 'apm' and 'untitled' or ''))
        self.assertEqual(p1_props.Get(I_D, 'PartitionFlags'), [])
        self.assertEqual(p1_props.Get(I_D, 'PartitionNumber'), (schema == 'apm' and 2 or 1))
        if filesystem:
            self.assertEqual(p1_props.Get(I_D, 'IdUsage'), 'filesystem')
            self.assertEqual(p1_props.Get(I_D, 'IdType'), 'vfat')
            self.assertEqual(p1_props.Get(I_D, 'IdLabel'), 'testp1')
        else:
            self.assertEqual(p1_props.Get(I_D, 'IdUsage'), '')
            self.assertEqual(p1_props.Get(I_D, 'IdType'), '')
            self.assertEqual(p1_props.Get(I_D, 'IdLabel'), '')
        off = p1_props.Get(I_D, 'PartitionOffset')
        self.assertTrue(off >= first_offset and off <= first_offset+20000)
        size = p1_props.Get(I_D, 'PartitionSize')
        self.assertTrue(size >= 9500000 and off <= 10500000)
        self.assertEqual(props.Get(I_D, 'PartitionTableCount'), exp_default_partitions + 1)

        # modify
        p1_iface.PartitionModify(type2, '', [flag])
        # udisks internally sets the new partition flags from the
        # PartitionCreate() call; trigger udev to ensure that we actually read
        # and check the values from the udev probers
        subprocess.call(['udevadm', 'trigger', '--action=change', '--subsystem-match=block'])
        self.sync()
        self.assertEqual(p1_props.Get(I_D, 'PartitionType'), type2)
        self.assertEqual(p1_props.Get(I_D, 'PartitionFlags'), [flag])

        # delete
        p1_iface.PartitionDelete([])
        self.sync_workaround()
        self.assertRaises(dbus.DBusException, p1_iface.PartitionModify, type2, '', [flag])
        self.assertRaises(dbus.DBusException, p1_props.Get, I_D, 'PartitionType')

        self.assertFalse(p1 in self.manager_iface.EnumerateDevices())
        self.assertEqual(props.Get(I_D, 'PartitionTableCount'), 0)

        if schema == 'mbr':
            self.assertFalse(os.path.exists(fdisk[0]), 'p1: device file still exists')
            self.assertEqual(self.fdisk_list(), [])

        # recreate p1: flagged, with fs
        p1 = self.partition_iface().PartitionCreate(0, 10000000, type1, 
                '', [flag], [], 'ext3', ['label=e3part'])
        self.sync_workaround()
        self.assertTrue(p1 in self.manager_iface.EnumerateDevices())

        p1_obj = dbus.SystemBus().get_object('org.freedesktop.UDisks',
            p1)
        p1_props = dbus.Interface(p1_obj, dbus.PROPERTIES_IFACE)
        self.assertEqual(p1_props.Get(I_D, 'DeviceIsPartition'), True)
        self.assertEqual(p1_props.Get(I_D, 'PartitionType'), type1)
        self.assertEqual(p1_props.Get(I_D, 'PartitionFlags'), [flag])
        self.assertEqual(p1_props.Get(I_D, 'PartitionNumber'), 1)
        self.assertEqual(p1_props.Get(I_D, 'IdUsage'), 'filesystem')
        self.assertEqual(p1_props.Get(I_D, 'IdType'), 'ext3')
        self.assertEqual(p1_props.Get(I_D, 'IdLabel'), 'e3part')

    def fdisk_list(self):
        '''Parse fdisk -l.

        Return None if device does not have a partition table, or a list of
        (device, boot, start, end, blocks, id) tuples.
        '''
        env = os.environ
        env['LC_ALL'] = 'C'
        fdisk = subprocess.Popen(['fdisk', '-l', self.device], env=env,
                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        (out, err) = fdisk.communicate()
        if err != '':
            return None
        parts = []
        for l in out.splitlines():
            if l.startswith('/dev/'):
                fields = l.split()
                if fields[1] == '*':
                    # boot flag
                    fields[1] = True
                    fields = tuple(fields[:6])
                else:
                    fields = tuple([fields[0], False] + fields[1:5])
                parts.append(fields)
        return parts

# ----------------------------------------------------------------------------

hd_smart_blob = None

class Smart(UDisksTestCase):
    '''Check SMART operation.'''

    def test_0_hd_status(self):
        '''SMART status of first internal hard disk
        
        This is a best-effort readonly test.
        '''
        hd = '/dev/sda'

        if not os.path.exists(hd):
            sys.stderr.write('[skip] ')
            return

        has_smart = subprocess.call(['skdump', '--can-smart', hd],
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT) == 0

        obj = dbus.SystemBus().get_object('org.freedesktop.UDisks', 
                self.manager_iface.FindDeviceByDeviceFile(hd))
        iface = dbus.Interface(obj, I_D)
        props = dbus.Interface(obj, dbus.PROPERTIES_IFACE)

        self.assertEqual(props.Get(I_D, 'DriveAtaSmartIsAvailable'),
                has_smart)

        if has_smart:
            sys.stderr.write('[avail] ')

            # wait for SMART data to be read
            while props.Get(I_D, 'DriveAtaSmartTimeCollected') == 0:
                sys.stderr.write('[wait for data] ')
                time.sleep(0.5)

            info = self.get_info(devname=hd)
            self.assertTrue(info['ATA SMART'].startswith('Updated at '))
            global hd_smart_blob
            hd_smart_blob = ''.join(map(chr, props.Get(I_D, 'DriveAtaSmartBlob')))
            # this is of course not truly correct for a test suite, but let's
            # consider it a courtesy for developers :-)
            self.assertEqual(info['overall assessment'], 'Good')
            self.assertEqual(props.Get(I_D, 'DriveAtaSmartStatus'), 'GOOD')

            try:
                self.partition_iface().DriveAtaSmartInitiateSelftest('bogus', [])
                self.fail('bogus mode succeeded')
            except dbus.DBusException as e:
                self.assertEqual(e._dbus_error_name, 'org.freedesktop.UDisks.Error.Failed')
        else:
            sys.stderr.write('[N/A] ')
            info = self.get_info(devname=hd)
            self.assertEqual(info['ATA SMART'], 'not available')
            self.assertEqual(props.Get(I_D, 'DriveAtaSmartTimeCollected'), 0)
            self.assertEqual(props.Get(I_D, 'DriveAtaSmartBlob'), [])
            self.assertFalse('overall assessment' in info)

            try:
                self.partition_iface().DriveAtaSmartInitiateSelftest('short', [])
                self.fail('device did not report to have SMART capabilities')
            except dbus.DBusException as e:
                self.assertTrue('does not support ATA SMART' in str(e))

    def test_simulate(self):
        '''SMART status of simulated data on test device
        
        This requires SMART being available from the first hard disk, to
        collect the blob used for testing.
        '''
        global hd_smart_blob
    
        if not hd_smart_blob:
            sys.stderr.write('[skip] ')
            return
    
        props = self.partition_props()
        self.assertFalse(props.Get(I_D, 'DriveAtaSmartIsAvailable'))
        self.assertEqual(props.Get(I_D, 'DriveAtaSmartTimeCollected'), 0)
        self.assertEqual(props.Get(I_D, 'DriveAtaSmartBlob'), [])
    
        # without simulate, DK-disks should complain about absent SMART
        try:
            self.partition_iface().DriveAtaSmartRefreshData([])
            self.fail('expected "Device does not support ATA SMART"')
        except dbus.DBusException as e:
            self.assertTrue('does not support ATA SMART' in str(e))
        try:
            self.partition_iface().DriveAtaSmartInitiateSelftest('short', [])
            self.fail('fake device is not expected to have SMART capabilities')
        except dbus.DBusException as e:
            self.assertTrue('does not support ATA SMART' in str(e))
    
        # load the blob
        blob_f = tempfile.NamedTemporaryFile()
        blob_f.write(hd_smart_blob)
        blob_f.flush()
        self.partition_iface().DriveAtaSmartRefreshData(['simulate=' + blob_f.name])

        info = self.get_info()
    
        self.assertEqual(props.Get(I_D, 'DriveAtaSmartIsAvailable'), True)
    
        self.assertTrue(info['ATA SMART'].startswith('Updated at '))
        self.assertNotEqual(props.Get(I_D, 'DriveAtaSmartTimeCollected'), 0)
    
        self.assertEqual(hd_smart_blob, ''.join(map(chr, props.Get(I_D, 'DriveAtaSmartBlob'))))
        self.assertEqual(info['overall assessment'], 'Good')
        self.assertEqual(props.Get(I_D, 'DriveAtaSmartStatus'), 'GOOD')
    
        # tool should have the entire SMART info
        tool_info = subprocess.Popen([self.tool_path, '--show-info',
            self.device], stdout=subprocess.PIPE)
        out = tool_info.communicate()[0]
        self.assertTrue('power-on-hours' in out)
        self.assertTrue('Pre-fail' in out)
    

# ----------------------------------------------------------------------------

class LVM(UDisksTestCase):
    '''Check LVM devices.'''

    def setUp(self):
        '''Create a VG "udtest".

        This uses two virtual disk partitions as PV.
        '''
        if subprocess.call(['which', 'pvcreate'], stdout=subprocess.PIPE) != 0:
            self.fail('lvm tools not installed')
            return

        partsize = VDEV_SIZE/2 * 95 / 100
        self.partition_iface().PartitionTableCreate('mbr', [])
        p1 = self.partition_iface().PartitionCreate(0, partsize,
                '0x82', '', [], [], '', [])
        p1 = self.partition_iface().PartitionCreate(partsize+1, partsize,
                '0x82', '', [], [], '', [])

        self.vgname = 'udtest'
        self.assertEqual(subprocess.call(['pvcreate', self.devname(1)],
            stdout=subprocess.PIPE), 0)
        self.assertEqual(subprocess.call(['pvcreate', self.devname(2)],
            stdout=subprocess.PIPE), 0)
        self.assertEqual(subprocess.call(['vgcreate', self.vgname,
            self.devname(1), self.devname(2)], stdout=subprocess.PIPE), 0)

    def tearDown(self):
        '''Remove udtest VG.'''

        self.assertEqual(subprocess.call(['vgremove', '-f',
            self.vgname], stdout=subprocess.PIPE), 0)

    def test_single_lv(self):
        '''LVM: Single LV, no RAID'''

        self.sync()
        objs_old = set(self.manager_iface.EnumerateDevices())

        self.assertEqual(subprocess.call(['lvcreate', '-n', 'udtestlv1', '-L',
            '52M', self.vgname], stdout=subprocess.PIPE), 0)
        self.sync()

        # there should be exactly one new device for the LV
        objs_new = set(self.manager_iface.EnumerateDevices())
        self.assertEqual(len(objs_old) + 1, len(objs_new), objs_new - objs_old)
        lvname = list(objs_new - objs_old)[0]

        lv_props = dbus.Interface(dbus.SystemBus().get_object(
            'org.freedesktop.UDisks', lvname), dbus.PROPERTIES_IFACE)

        # the LV is a real volume which should be shown, but not automounted
        self.assertTrue(lv_props.Get(I_D, 'DeviceFile').startswith('/dev/dm-'))
        self.assertEqual(lv_props.Get(I_D, 'DevicePresentationHide'), False)
        self.assertEqual(lv_props.Get(I_D, 'DevicePresentationNopolicy'), True)

        # ensure that we have a by-name and a by-UUID link
        found_uuid = False
        found_name = False
        for i in lv_props.Get(I_D, 'DeviceFileById'):
            if 'uuid-LVM' in i:
                found_uuid = True
            if 'udtestlv1' in i:
                found_name = True
        self.assertTrue(found_uuid, 'no by-uuid found in ' + str(i))
        self.assertTrue(found_uuid, 'no by-name found in ' + str(i))

    def test_single_lv_raid(self):
        '''LVM: Single LV, RAID-1'''

        self.sync()
        objs_old = set(self.manager_iface.EnumerateDevices())

        self.assertEqual(subprocess.call(['lvcreate', '-n', 'udtestlvr1', '-L',
            '50M', '-m', '1', '--mirrorlog', 'core', self.vgname],
            stdout=subprocess.PIPE), 0)
        self.sync()

        # there should be two new shadow devices for the RAID images, and one
        # real LV
        objs_new = set(self.manager_iface.EnumerateDevices())
        self.assertEqual(len(objs_old) + 3, len(objs_new))
        lv_objs = objs_new - objs_old

        #subprocess.call(['bash', '-i'])

        # find the real one; TODO: is this nameing scheme right on all distros?
        devname = '/dev/mapper/%s-udtestlvr1' % self.vgname
        real_lv_obj = self.manager_iface.FindDeviceByDeviceFile(devname)
        self.assertTrue(real_lv_obj in lv_objs)

        # put a file system onto it, for testing properties
        iface = dbus.Interface(dbus.SystemBus().get_object(
                'org.freedesktop.UDisks', real_lv_obj), I_D)
        iface.FilesystemCreate('ext3', [])
        self.sync_workaround()

        for o in lv_objs:
            props = dbus.Interface(dbus.SystemBus().get_object(
                'org.freedesktop.UDisks', o), dbus.PROPERTIES_IFACE)

            if o == real_lv_obj:
                # never hide the real LV
                self.assertEqual(props.Get(I_D, 'DevicePresentationHide'), False)
                self.assertEqual(props.Get(I_D, 'IdUsage'), 'filesystem')
                self.assertEqual(props.Get(I_D, 'IdType'), 'ext3')
                self.assertNotEqual(props.Get(I_D, 'IdUuid'), '')

                # ensure that we have a UUID
                found_uuid = False
                for i in props.Get(I_D, 'DeviceFileById'):
                    if 'uuid-LVM' in i:
                        found_uuid = True
                self.assertTrue(found_uuid, 'no by-uuid found in ' + str(i))
            else:
                # mirror images should not have any real FS usage at all
                self.assertEqual(props.Get(I_D, 'IdUsage'), '')

                # mirror images should not have an UUID, since they are not
                # "real" devices (they do have by-id/ symlinks, though)
                # that is actually the job of the lvm2 udev rules, but check it
                # here to ensure proper system integration
                self.assertEqual(props.Get(I_D, 'IdUuid'), '')

            self.assertEqual(props.Get(I_D, 'DevicePresentationNopolicy'), True)

    def test_partitions(self):
        '''LVM: Single LV, no RAID, partitions (with kpartx)'''

        if subprocess.call(['which', 'kpartx'], stdout=subprocess.PIPE) != 0:
            self.fail('kpartx not installed')
            return
        if subprocess.call(['which', 'parted'], stdout=subprocess.PIPE) != 0:
            self.fail('parted not installed')
            return

        self.assertEqual(subprocess.call(['lvcreate', '-n', 'udtestlv1', '-L',
            '52M', self.vgname], stdout=subprocess.PIPE), 0)
        devname = '/dev/mapper/%s-udtestlv1' % self.vgname

        assert subprocess.call(['parted', '-s', devname, 'mklabel', 'msdos']) == 0
        assert subprocess.call(['parted', '-s', devname, 'mkpart', 'primary',
            'fat16', '1', '9']) == 0
        assert subprocess.call(['parted', '-s', devname, 'mkpart', 'primary',
            'fat32', '10', '19']) == 0
        self.sync()

        assert os.path.exists(devname + 'p1')
        assert os.path.exists(devname + 'p2')

        try:
            # partition table properties
            dev_objpath = self.manager_iface.FindDeviceByDeviceFile(devname)
            dev_obj = dbus.SystemBus().get_object('org.freedesktop.UDisks',
                    dev_objpath)
            dev_p = dbus.Interface(dev_obj, dbus.PROPERTIES_IFACE)

            self.assertEqual(dev_p.Get(I_D, 'DeviceFile'), os.path.realpath(devname))
            self.assertEqual(dev_p.Get(I_D, 'DeviceIsPartition'), False)
            self.assertEqual(dev_p.Get(I_D, 'PartitionScheme'), '')
            self.assertEqual(dev_p.Get(I_D, 'DeviceIsPartitionTable'), True)
            self.assertEqual(dev_p.Get(I_D, 'PartitionTableScheme'), 'mbr')

            # partition 1 properties
            p1_obj = dbus.SystemBus().get_object('org.freedesktop.UDisks', 
                    self.manager_iface.FindDeviceByDeviceFile(devname + 'p1'))
            p1_p = dbus.Interface(p1_obj, dbus.PROPERTIES_IFACE)

            self.assertEqual(p1_p.Get(I_D, 'DeviceIsPartition'), True)
            self.assertEqual(p1_p.Get(I_D, 'DeviceIsPartitionTable'), False)
            self.assertEqual(p1_p.Get(I_D, 'DevicePresentationHide'), False)
            self.assertEqual(p1_p.Get(I_D, 'PartitionScheme'), 'mbr')
            self.assertEqual(p1_p.Get(I_D, 'PartitionType'), '0x0e')
            self.assertEqual(p1_p.Get(I_D, 'PartitionSlave'), dev_objpath)
            self.assertEqual(p1_p.Get(I_D, 'PartitionNumber'), 1)
            self.assertTrue(p1_p.Get(I_D, 'PartitionOffset') > 10000)
            self.assertTrue(p1_p.Get(I_D, 'PartitionSize') > 8000000)

            # partition 2 properties
            p1_obj = dbus.SystemBus().get_object('org.freedesktop.UDisks', 
                    self.manager_iface.FindDeviceByDeviceFile(devname + 'p2'))
            p1_p = dbus.Interface(p1_obj, dbus.PROPERTIES_IFACE)

            self.assertEqual(p1_p.Get(I_D, 'DeviceIsPartition'), True)
            self.assertEqual(p1_p.Get(I_D, 'DeviceIsPartitionTable'), False)
            self.assertEqual(p1_p.Get(I_D, 'DevicePresentationHide'), False)
            self.assertEqual(p1_p.Get(I_D, 'PartitionScheme'), 'mbr')
            self.assertEqual(p1_p.Get(I_D, 'PartitionType'), '0x0c')
            self.assertEqual(p1_p.Get(I_D, 'PartitionSlave'), dev_objpath)
            self.assertEqual(p1_p.Get(I_D, 'PartitionNumber'), 2)
            self.assertTrue(p1_p.Get(I_D, 'PartitionOffset') > 9000000)
            self.assertTrue(p1_p.Get(I_D, 'PartitionSize') > 8000000)

        finally:
            subprocess.call(['kpartx', '-d', devname])
            self.sync()

# ----------------------------------------------------------------------------

class GlobalOps(UDisksTestCase):
    '''Check various global operations.'''

    def test_daemon_version(self):
        '''DaemonVersion property'''

        ver = self.manager_props.Get('org.freedesktop.UDisks',
                'DaemonVersion')
        self.assertEqual(type(ver), dbus.String)
        self.assertTrue(len(ver) > 0)

    def test_enumerate_devices(self):
        '''EnumerateDevices()'''

        devs = self.manager_iface.EnumerateDevices()
        self.assertTrue(len(devs) > 1) # at least our test device and root fs
        self.assertTrue('/org/freedesktop/UDisks/devices/' +
                    os.path.basename(self.device) in devs)

    def test_enumerate_device_files(self):
        '''EnumerateDeviceFiles()'''

        devs = self.manager_iface.EnumerateDeviceFiles()
        self.assertTrue(len(devs) > 1) # at least our test device and root fs
        self.assertTrue(self.device in devs)

    def test_find_by_devpath(self):
        '''FindDeviceByDeviceFile()'''

        self.assertEqual(
                self.manager_iface.FindDeviceByDeviceFile(self.device),
                '/org/freedesktop/UDisks/devices/' +
                    os.path.basename(self.device))

        self.assertRaises(dbus.DBusException, 
                self.manager_iface.FindDeviceByDeviceFile, '/dev/nonexisting')

    def test_find_by_major_minor(self):
        '''FindDeviceByMajorMinor()'''

        st = os.stat(self.device)
        dev = self.manager_iface.FindDeviceByMajorMinor(os.major(st.st_rdev),
                os.minor(st.st_rdev))
        self.assertEqual(dev, '/org/freedesktop/UDisks/devices/' +
                    os.path.basename(self.device))

        self.assertRaises(dbus.DBusException, 
                self.manager_iface.FindDeviceByMajorMinor, 42, 42)

    def test_inhibition(self):
        '''inhibition'''

        # Inhibit()
        self.assertFalse(self.manager_props.Get('org.freedesktop.UDisks',
            'DaemonIsInhibited'))
        cookie1 = self.manager_iface.Inhibit()
        self.assertTrue(self.manager_props.Get('org.freedesktop.UDisks',
            'DaemonIsInhibited'))

        # try mounting, should fail due to inhibition
        try:
            self.partition_iface().FilesystemMount('', [])
            self.fail('.FilesystemMount() succeeded while inhibited')
        except dbus.DBusException as e:
            self.assertTrue(e._dbus_error_name.endswith('Error.Inhibited'))

        # Inhibit() another time
        cookie2 = self.manager_iface.Inhibit()
        self.assertTrue(self.manager_props.Get('org.freedesktop.UDisks',
            'DaemonIsInhibited'))

        # Uninhibit()
        self.manager_iface.Uninhibit(cookie1)
        self.assertTrue(self.manager_props.Get('org.freedesktop.UDisks',
            'DaemonIsInhibited'))

        self.assertRaises(dbus.DBusException, self.manager_iface.Uninhibit,
            '0xDEADBEEF')

        self.manager_iface.Uninhibit(cookie2)
        self.assertFalse(self.manager_props.Get('org.freedesktop.UDisks',
            'DaemonIsInhibited'))

        self.assertRaises(dbus.DBusException, self.manager_iface.Uninhibit,
            cookie1)

# ----------------------------------------------------------------------------

if __name__ == '__main__':
    optparser = optparse.OptionParser('%prog [options] [test class] [test name] [...]')
    optparser.add_option('-l', '--log-file', dest='logfile',
            help='write daemon log to a file')
    optparser.add_option('-w', '--no-workarounds', dest='noworkarounds',
            action="store_true", default=False,
            help='Disable workarounds for race conditions in the D-BUS API')
    (opts, args) = optparser.parse_args()

    disable_dbus_udev_syncs = opts.noworkarounds

    UDisksTestCase.init(logfile=opts.logfile)
    if len(args) == 0:
        tests = unittest.TestLoader().loadTestsFromName('__main__')
    else:
        tests = unittest.TestLoader().loadTestsFromNames(args,
                __import__('__main__'))
    if unittest.TextTestRunner(verbosity=2).run(tests).wasSuccessful():
        sys.exit(0)
    else:
        sys.exit(1)

