diff options
Diffstat (limited to 'build/lib/dogtail')
-rw-r--r-- | build/lib/dogtail/__init__.py | 16 | ||||
-rw-r--r-- | build/lib/dogtail/config.py | 219 | ||||
-rw-r--r-- | build/lib/dogtail/distro.py | 362 | ||||
-rw-r--r-- | build/lib/dogtail/dump.py | 33 | ||||
-rw-r--r-- | build/lib/dogtail/errors.py | 28 | ||||
-rw-r--r-- | build/lib/dogtail/i18n.py | 281 | ||||
-rw-r--r-- | build/lib/dogtail/logging.py | 218 | ||||
-rw-r--r-- | build/lib/dogtail/path.py | 113 | ||||
-rw-r--r-- | build/lib/dogtail/predicate.py | 443 | ||||
-rw-r--r-- | build/lib/dogtail/procedural.py | 455 | ||||
-rw-r--r-- | build/lib/dogtail/rawinput.py | 252 | ||||
-rw-r--r-- | build/lib/dogtail/sessions.py | 231 | ||||
-rw-r--r-- | build/lib/dogtail/tc.py | 230 | ||||
-rw-r--r-- | build/lib/dogtail/tree.py | 1318 | ||||
-rw-r--r-- | build/lib/dogtail/utils.py | 395 | ||||
-rw-r--r-- | build/lib/dogtail/version.py | 56 | ||||
-rw-r--r-- | build/lib/dogtail/wrapped.py | 33 |
17 files changed, 4683 insertions, 0 deletions
diff --git a/build/lib/dogtail/__init__.py b/build/lib/dogtail/__init__.py new file mode 100644 index 00000000000..76a4a13fcb5 --- /dev/null +++ b/build/lib/dogtail/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: UTF-8 -*- +""" +GUI test tool and automation framework that uses Accessibility (a11y) technologies to communicate with desktop applications. + +Authors: Zack Cerza <zcerza@redhat.com>, Ed Rousseau <rousseau@redhat.com>, David Malcolm <dmalcolm@redhat.com>, Vita Humpa <vhumpa@redhat.com> +""" + +__author__ = """Zack Cerza <zcerza@redhat.com>, +Ed Rousseau <rousseau@redhat.com>, +David Malcolm <dmalcolm@redhat.com>, +Vita Humpa <vhumpa@redhat.com>""" +__version__ = "0.9.0" +__copyright__ = "Copyright © 2005-2014 Red Hat, Inc." +__license__ = "GPL" +__all__ = ("config", "predicate", + "procedural", "tc", "tree", "utils", "errors") diff --git a/build/lib/dogtail/config.py b/build/lib/dogtail/config.py new file mode 100644 index 00000000000..82e197cf523 --- /dev/null +++ b/build/lib/dogtail/config.py @@ -0,0 +1,219 @@ +""" +The configuration module. +""" +__author__ = "Zack Cerza <zcerza@redhat.com>, David Malcolm <dmalcolm@redhat.com>" + +import os +import sys +import locale + + +def _userTmpDir(baseName): + # i.e. /tmp/dogtail-foo + return '-'.join(('/'.join(('/tmp', baseName)), os.environ['USER'])) + + +class _Config(object): + + """ + Contains configuration parameters for the dogtail run. + + scratchDir(str): + Directory where things like screenshots are stored. + + dataDir(str): + Directory where related data files are located. + + logDir(str): + Directory where dogtail.tc.TC*-generated logs are stored. + + scriptName(str) [Read-Only]: + The name of the script being run. + + encoding(str) + The encoding for text, used by dogtail.tc.TCString . + + actionDelay(float): + The delay after an action is executed. + + typingDelay(float): + The delay after a character is typed on the keyboard. + + runInterval(float): + The interval at which dogtail.utils.run() and dogtail.procedural.run() + check to see if the application has started up. + + runTimeout(int): + The timeout after which dogtail.utils.run() and dogtail.procedural.run() + give up on looking for the newly-started application. + + searchBackoffDuration (float): + Time in seconds for which to delay when a search fails. + + searchWarningThreshold (int): + Number of retries before logging the individual attempts at a search. + + searchCutoffCount (int): + Number of times to retry when a search fails. + + defaultDelay (float): + Default time in seconds to sleep when delaying. + + childrenLimit (int): + When there are a very large number of children of a node, only return + this many, starting with the first. + + debugSearching (boolean): + Whether to write info on search backoff and retry to the debug log. + + debugSleep (boolean): + Whether to log whenever we sleep to the debug log. + + debugSearchPaths (boolean): + Whether we should write out debug info when running the SearchPath + routines. + + absoluteNodePaths (boolean): + Whether we should identify nodes in the logs with long 'abcolute paths', or + merely with a short 'relative path'. FIXME: give examples + + ensureSensitivity (boolean): + Should we check that ui nodes are sensitive (not 'greyed out') before + performing actions on them? If this is True (the default) it will raise + an exception if this happens. Can set to False as a workaround for apps + and toolkits that don't report sensitivity properly. + + debugTranslation (boolean): + Whether we should write out debug information from the translation/i18n + subsystem. + + blinkOnActions (boolean): + Whether we should blink a rectangle around a Node when an action is + performed on it. + + fatalErrors (boolean): + Whether errors encountered in dogtail.procedural should be considered + fatal. If True, exceptions will be raised. If False, warnings will be + passed to the debug logger. + + checkForA11y (boolean): + Whether to check if accessibility is enabled. If not, just assume it is + (default True). + + logDebugToFile (boolean): + Whether to write debug output to a log file. + + logDebugToStdOut (boolean): + Whether to print log output to console or not (default True). + """ + @property + def scriptName(self): + return os.path.basename(sys.argv[0]).replace('.py', '') + + @property + def encoding(self): + return locale.getpreferredencoding().lower() + + defaults = { + # Storage + 'scratchDir': '/'.join((_userTmpDir('dogtail'), '')), + 'dataDir': '/'.join((_userTmpDir('dogtail'), 'data', '')), + 'logDir': '/'.join((_userTmpDir('dogtail'), 'logs', '')), + 'scriptName': scriptName.fget(None), + 'encoding': encoding.fget(None), + 'configFile': None, + 'baseFile': None, + + # Timing and Limits + 'actionDelay': 1.0, + 'typingDelay': 0.1, + 'runInterval': 0.5, + 'runTimeout': 30, + 'searchBackoffDuration': 0.5, + 'searchWarningThreshold': 3, + 'searchCutoffCount': 20, + 'defaultDelay': 0.5, + 'childrenLimit': 100, + + # Debug + 'debugSearching': False, + 'debugSleep': False, + 'debugSearchPaths': False, + 'logDebugToStdOut': True, + 'absoluteNodePaths': False, + 'ensureSensitivity': False, + 'debugTranslation': False, + 'blinkOnActions': False, + 'fatalErrors': False, + 'checkForA11y': True, + + # Logging + 'logDebugToFile': True + } + + options = {} + + invalidValue = "__INVALID__" + + def __init__(self): + _Config.__createDir(_Config.defaults['scratchDir']) + _Config.__createDir(_Config.defaults['logDir']) + _Config.__createDir(_Config.defaults['dataDir']) + + def __setattr__(self, name, value): + if name not in config.defaults: + raise AttributeError(name + " is not a valid option.") + + elif _Config.defaults[name] != value or \ + _Config.options.get(name, _Config.invalidValue) != value: + if 'Dir' in name: + _Config.__createDir(value) + if value[-1] != os.path.sep: + value = value + os.path.sep + elif name == 'logDebugToFile': + import logging + logging.debugLogger = logging.Logger('debug', value) + _Config.options[name] = value + + def __getattr__(self, name): + try: + return _Config.options[name] + except KeyError: + try: + return _Config.defaults[name] + except KeyError: + raise AttributeError("%s is not a valid option." % name) + + def __createDir(cls, dirName, perms=0o777): + """ + Creates a directory (if it doesn't currently exist), creating any + parent directories it needs. + + If perms is None, create with python's default permissions. + """ + dirName = os.path.abspath(dirName) + # print "Checking for %s ..." % dirName, + if not os.path.isdir(dirName): + if perms: + umask = os.umask(0) + os.makedirs(dirName, perms) + os.umask(umask) + else: + # This is probably a dead code - no other functions call this without the permissions set + os.makedirs(dirName) # pragma: no cover + __createDir = classmethod(__createDir) + + def load(self, dict): + """ + Loads values from dict, preserving any options already set that are not overridden. + """ + _Config.options.update(dict) + + def reset(self): + """ + Resets all settings to their defaults. + """ + _Config.options = {} + + +config = _Config() diff --git a/build/lib/dogtail/distro.py b/build/lib/dogtail/distro.py new file mode 100644 index 00000000000..6f70f194650 --- /dev/null +++ b/build/lib/dogtail/distro.py @@ -0,0 +1,362 @@ +"""Handles differences between different distributions + +Authors: Dave Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>""" +__author__ = "Dave Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>" + +import os +import re +from version import Version +from logging import debugLogger as logger + + +class DistributionNotSupportedError(Exception): # pragma: no cover + + """ + This distribution is not supported. + """ + PATCH_MESSAGE = "Please send patches to dogtail-devel-list@gnome.org" + + def __init__(self, distro): + self.distro = distro + + def __str__(self): + return self.distro + ". " + DistributionNotSupportedError.PATCH_MESSAGE + + +class PackageNotFoundError(Exception): + + """ + Error finding the requested package. + """ + pass + +global packageDb +global distro + + +class PackageDb(object): + + """ + Class to abstract the details of whatever software package database is in + use (RPM, APT, etc) + """ + + def __init__(self): + self.prefix = '/usr' + self.localePrefixes = [self.prefix + '/share/locale'] + + def getVersion(self, packageName): + """ + Method to get the version of an installed package as a Version + instance (or raise an exception if not found) + + Note: does not know about distributions' internal revision numbers. + """ + raise NotImplementedError + + def getFiles(self, packageName): + """ + Method to get a list of filenames owned by the package, or raise an + exception if not found. + """ + raise NotImplementedError + + def getMoFiles(self, locale=None): + """ + Method to get a list of all .mo files on the system, optionally for a + specific locale. + """ + moFiles = {} + + def appendIfMoFile(moFiles, dirName, fNames): + import re + for fName in fNames: + if re.match('(.*)\\.mo', fName): + moFiles[dirName + '/' + fName] = None + + for localePrefix in self.localePrefixes: + if locale: + localePrefix = localePrefix + '/' + locale + os.path.walk(localePrefix, appendIfMoFile, moFiles) + + return moFiles.keys() + + def getDependencies(self, packageName): + """ + Method to get a list of unique package names that this package + is dependent on, or raise an exception if the package is not + found. + """ + raise NotImplementedError + + +class _RpmPackageDb(PackageDb): # pragma: no cover + + def __init__(self): + PackageDb.__init__(self) + + def getVersion(self, packageName): + import rpm + ts = rpm.TransactionSet() + for header in ts.dbMatch("name", packageName): + return Version.fromString(header["version"]) + raise PackageNotFoundError(packageName) + + def getFiles(self, packageName): + import rpm + ts = rpm.TransactionSet() + for header in ts.dbMatch("name", packageName): + return header["filenames"] + raise PackageNotFoundError(packageName) + + def getDependencies(self, packageName): + import rpm + ts = rpm.TransactionSet() + for header in ts.dbMatch("name", packageName): + # Simulate a set using a hash (to a dummy value); + # sets were only added in Python 2.4 + result = {} + + # Get the list of requirements; these are + # sometimes package names, but can also be + # so-names of libraries, and invented virtual + # ids + for requirement in header[rpm.RPMTAG_REQUIRES]: + # Get the name of the package providing + # this requirement: + for depPackageHeader in ts.dbMatch("provides", requirement): + depName = depPackageHeader['name'] + if depName != packageName: + # Add to the Hash with a dummy value + result[depName] = None + return result.keys() + raise PackageNotFoundError(packageName) + + +class _AptPackageDb(PackageDb): + + def __init__(self): + PackageDb.__init__(self) + self.cache = None + + def getVersion(self, packageName): + if not self.cache: + import apt_pkg + apt_pkg.init() + self.cache = apt_pkg.GetCache() + packages = self.cache.Packages + for package in packages: + if package.Name == packageName: + verString = re.match( + '.*Ver:\'(.*)-.*\' Section:', str(package.CurrentVer)).group(1) + return Version.fromString(verString) + raise PackageNotFoundError(packageName) + + def getFiles(self, packageName): + files = [] + list = os.popen('dpkg -L %s' % packageName).readlines() + if not list: + raise PackageNotFoundError(packageName) + else: + for line in list: + file = line.strip() + if file: + files.append(file) + return files + + def getDependencies(self, packageName): + # Simulate a set using a hash (to a dummy value); + # sets were only added in Python 2.4 + result = {} + if not self.cache: + import apt_pkg + apt_pkg.init() + self.cache = apt_pkg.GetCache() + packages = self.cache.Packages + for package in packages: + if package.Name == packageName: + current = package.CurrentVer + if not current: + raise PackageNotFoundError(packageName) + depends = current.DependsList + list = depends['Depends'] + for dependency in list: + name = dependency[0].TargetPkg.Name + # Add to the hash using a dummy value + result[name] = None + return result.keys() + + +class _UbuntuAptPackageDb(_AptPackageDb): + + def __init__(self): + _AptPackageDb.__init__(self) + self.localePrefixes.append(self.prefix + '/share/locale-langpack') + + +class _PortagePackageDb(PackageDb): # pragma: no cover + + def __init__(self): + PackageDb.__init__(self) + + def getVersion(self, packageName): + # the portage utilities are almost always going to be in + # /usr/lib/portage/pym + import sys + sys.path.append('/usr/lib/portage/pym') + import portage + # FIXME: this takes the first package returned in the list, in the + # case that there are slotted packages, and removes the leading + # category such as 'sys-apps' + gentooPackageName = portage.db["/"][ + "vartree"].dbapi.match(packageName)[0].split('/')[1] + # this removes the distribution specific versioning returning only the + # upstream version + upstreamVersion = portage.pkgsplit(gentooPackageName)[1] + # print "Version of package is: " + upstreamVersion + return Version.fromString(upstreamVersion) + + +class _ConaryPackageDb(PackageDb): # pragma: no cover + + def __init__(self): + PackageDb.__init__(self) + + def getVersion(self, packageName): + from conaryclient import ConaryClient + client = ConaryClient() + dbVersions = client.db.getTroveVersionList(packageName) + if not len(dbVersions): + raise PackageNotFoundError(packageName) + return dbVersions[0].trailingRevision().asString().split("-")[0] + +# getVersion not implemented because on Solaris multiple modules are installed +# in single packages, so it is hard to tell what version number of a specific +# module. + + +class _SolarisPackageDb(PackageDb): # pragma: no cover + + def __init__(self): + PackageDb.__init__(self) + + +class JhBuildPackageDb(PackageDb): # pragma: no cover + + def __init__(self): + PackageDb.__init__(self) + prefixes = [] + prefixes.append(os.environ['LD_LIBRARY_PATH']) + prefixes.append(os.environ['XDG_CONFIG_DIRS']) + prefixes.append(os.environ['PKG_CONFIG_PATH']) + self.prefix = os.path.commonprefix(prefixes) + self.localePrefixes.append(self.prefix + '/share/locale') + + def getDependencies(self, packageName): + result = {} + lines = os.popen('jhbuild list ' + packageName).readlines() + for line in lines: + if line: + result[line.strip()] = None + return result.keys() + + +class Distro(object): + + """ + Class representing a distribution. + + Scripts may want to do arbitrary logic based on whichever distro is in use + (e.g. handling differences in names of packages, distribution-specific + patches, etc.) + + We can either create methods in the Distro class to handle these, or we + can use constructs like isinstance(distro, Ubuntu) to handle this. We can + even create hierarchies of distro subclasses to handle this kind of thing + (could get messy fast though) + """ + + +class Fedora(Distro): # pragma: no cover + + def __init__(self): + self.packageDb = _RpmPackageDb() + + +class RHEL(Fedora): # pragma: no cover + pass + + +class Debian(Distro): # pragma: no cover + + def __init__(self): + self.packageDb = _AptPackageDb() + + +class Ubuntu(Debian): + + def __init__(self): + self.packageDb = _UbuntuAptPackageDb() + + +class Suse(Distro): # pragma: no cover + + def __init__(self): + self.packageDb = _RpmPackageDb() + + +class Gentoo(Distro): # pragma: no cover + + def __init__(self): + self.packageDb = _PortagePackageDb() + + +class Conary(Distro): # pragma: no cover + + def __init__(self): + self.packageDb = _ConaryPackageDb() + + +class Solaris(Distro): # pragma: no cover + + def __init__(self): + self.packageDb = _SolarisPackageDb() + + +class JHBuild(Distro): # pragma: no cover + + def __init__(self): + self.packageDb = JhBuildPackageDb() + + +def detectDistro(): + logger.log("Detecting distribution:", newline=False) + + if os.environ.get("CERTIFIED_GNOMIE", "no") == "yes": + distro = JHBuild() # pragma: no cover + elif os.path.exists("/etc/SuSE-release"): + distro = Suse() # pragma: no cover + elif os.path.exists("/etc/fedora-release"): + distro = Fedora() # pragma: no cover + elif os.path.exists("/etc/redhat-release"): + distro = RHEL() # pragma: no cover + elif os.path.exists("/usr/share/doc/ubuntu-minimal"): + distro = Ubuntu() + elif os.path.exists("/etc/debian_version"): # pragma: no cover + distro = Debian() # pragma: no cover + elif os.path.exists("/etc/gentoo-release"): # pragma: no cover + distro = Gentoo() # pragma: no cover + elif os.path.exists("/etc/slackware-version"): # pragma: no cover + raise DistributionNotSupportedError("Slackware") # pragma: no cover + elif os.path.exists("/var/lib/conarydb/conarydb"): # pragma: no cover + distro = Conary() # pragma: no cover + elif os.path.exists("/etc/release") and \ + re.match(".*Solaris", open("/etc/release").readline()): # pragma: no cover + distro = Solaris() # pragma: no cover + else: + raise DistributionNotSupportedError("Unknown") # pragma: no cover + logger.log(distro.__class__.__name__) + return distro + +distro = detectDistro() +packageDb = distro.packageDb diff --git a/build/lib/dogtail/dump.py b/build/lib/dogtail/dump.py new file mode 100644 index 00000000000..3756820a510 --- /dev/null +++ b/build/lib/dogtail/dump.py @@ -0,0 +1,33 @@ +"""Utility functions for 'dumping' trees of Node objects. + +Author: Zack Cerza <zcerza@redhat.com>""" +__author__ = "Zack Cerza <zcerza@redhat.com>" + +from __builtin__ import file + +spacer = ' ' + + +def plain(node, fileName=None): + """ + Plain-text dump. The hierarchy is represented through indentation. + """ + def crawl(node, depth): + dump(node, depth) + for action in node.actions.values(): + dump(action, depth + 1) + for child in node.children: + crawl(child, depth + 1) + + def dumpFile(item, depth): + _file.write(spacer * depth + str(item) + '\n') + + def dumpStdOut(item, depth): + print(spacer * depth + str(item)) + if fileName: + dump = dumpFile + _file = file(fileName, 'w') + else: + dump = dumpStdOut + + crawl(node, 0) diff --git a/build/lib/dogtail/errors.py b/build/lib/dogtail/errors.py new file mode 100644 index 00000000000..648af9d598e --- /dev/null +++ b/build/lib/dogtail/errors.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +General exceptions; not overly module-specific +""" +__author__ = "Zack Cerza <zcerza@redhat.com>" +from logging import debugLogger as logger + +import inspect + + +def warn(message, caller=True): + """ + Generate a warning, and pass it to the debug logger. + """ + frameRec = inspect.stack()[-1] + message = "Warning: %s:%s: %s" % (frameRec[1], frameRec[2], message) + if caller and frameRec[1] != '<stdin>' and frameRec[1] != '<string>': + message = message + ':\n ' + frameRec[4][0] + del frameRec + logger.log(message) + + +class DependencyNotFoundError(Exception): + + """ + A dependency was not found. + """ + pass diff --git a/build/lib/dogtail/i18n.py b/build/lib/dogtail/i18n.py new file mode 100644 index 00000000000..8117f8d82c6 --- /dev/null +++ b/build/lib/dogtail/i18n.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +""" +Internationalization facilities + +Authors: David Malcolm <dmalcolm@redhat.com> +""" + +__author__ = """David Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>""" + +import config + +import os +import re +import gettext + +from logging import debugLogger as logger +from __builtin__ import unicode + + +def safeDecode(string): + try: + string = string.decode('utf-8', 'replace') + except UnicodeEncodeError: + string = string.encode('utf-8', 'replace') + return string + + +def safeEncode(string): + pass + + +""" +Singleton list of TranslationDb instances, to be initialized by the script with +whatever translation databases it wants. +""" +translationDbs = [] + + +class TranslationDb(object): + + """ + Abstract base class representing a database of translations + """ + + def getTranslationsOf(self, srcName): + """ + Pure virtual method to look up the translation of a string. + Returns a list of candidate strings (the translation), empty if not found. + + Note that a source string can map to multiple translated strings. For + example, in the French translation of Evolution, the string "Forward" can + translate to both + (i) "Faire suivre" for forwarding an email, and + (ii) "Suivant" for the next page in a wizard. + """ + raise NotImplementedError + + +class GettextTranslationDb(TranslationDb): + + """ + Implementation of TranslationDb which leverages gettext, using a single + translation mo-file. + """ + + def __init__(self, moFile): + self.__moFile = moFile + self.__gnutranslations = gettext.GNUTranslations(open(moFile)) + + def getTranslationsOf(self, srcName): + srcName = safeDecode(srcName) + # print "searching for translations of %s"%srcName + # Use a dict to get uniqueness: + results = {} + result = self.__gnutranslations.ugettext(srcName) + if result != srcName: + results[result] = None + + # Hack alert: + # + # Note that typical UI definition in GTK etc contains strings with + # underscores to denote accelerators. + # For example, the stock GTK "Add" item has text "_Add" which e.g. + # translates to "A_jouter" in French + # + # Since these underscores have been stripped out before we see these strings, + # we are looking for a translation of "Add" into "Ajouter" in this case, so + # we need to fake it, by looking up the string multiple times, with underscores + # inserted in all possible positions, stripping underscores out of the result. + # Ugly, but it works. + + for index in range(len(srcName)): + candidate = srcName[:index] + "_" + srcName[index:] + result = self.__gnutranslations.ugettext(candidate) + if result != candidate: + # Strip out the underscore, and add to the result: + results[result.replace('_', '')] = True + + return results.keys() + + +def translate(srcString): + """ + Look up srcString in the various translation databases (if any), returning + a list of all matches found (potentially the empty list) + """ + # Use a dict to get uniqueness: + results = {} + # Try to translate the string: + for translationDb in translationDbs: + for result in translationDb.getTranslationsOf(srcString): + result = safeDecode(result) + results[result] = True + + # No translations found: + if len(results) == 0: + if config.config.debugTranslation: + logger.log('Translation not found for "%s"' % srcString) + return results.keys() + + +class TranslatableString(object): + + """ + Class representing a string that we want to match strings against, handling + translation for us, by looking it up once at construction time. + """ + + def __init__(self, untranslatedString): + """ + Constructor looks up the string in all of the translation databases, storing + the various translations it finds. + """ + untranslatedString = safeDecode(untranslatedString) + self.untranslatedString = untranslatedString + self.translatedStrings = translate(untranslatedString) + + def matchedBy(self, string): + """ + Compare the test string against either the translation of the original + string (or simply the original string, if no translation was found). + """ + # print "comparing %s against %s"%(string, self) + def stringsMatch(inS, outS): + """ + Compares a regular expression to a string + + inS: the regular expression (or normal string) + outS: the normal string to be compared against + """ + inString = str(inS) + outString = outS + if inString == outString: + return True + inString = inString + '$' + inString = safeDecode(inString) + outString = safeDecode(outString) + if inString[0] == '*': + inString = "\\" + inString + # Escape all parentheses, since grouping will never be needed here + inString = re.sub('([\(\)])', r'\\\1', inString) + match = re.match(inString, outString) + matched = match is not None + return matched + + matched = False + # the 'ts' variable keeps track of whether we're working with + # translated strings. it's only used for debugging purposes. + #ts = 0 + # print string, str(self) + for translatedString in self.translatedStrings: + #ts = ts + 1 + matched = stringsMatch(translatedString, string) + if not matched: + matched = translatedString == string + if matched: + return matched + # ts=0 + return stringsMatch(self.untranslatedString, string) + + def __str__(self): + """ + Provide a meaningful debug version of the string (and the translation in + use) + """ + if len(self.translatedStrings) > 0: + # build an output string, with commas in the correct places + translations = "" + for tString in self.translatedStrings: + translations += u'"%s", ' % safeDecode(tString) + result = u'"%s" (%s)' % ( + safeDecode(self.untranslatedString), translations) + return safeDecode(result) + else: + return '"%s"' % (self.untranslatedString) + + +def isMoFile(filename, language=''): + """ + Does the given filename look like a gettext mo file? + + Optionally: Does the file also contain translations for a certain language, + for example 'ja'? + """ + if re.match('(.*)\\.mo$', filename): + if not language: + return True + elif re.match('/usr/share/locale(.*)/%s(.*)/LC_MESSAGES/(.*)\\.mo$' % + language, filename): + return True + else: + return False + else: + return False + + +def loadAllTranslationsForLanguage(language): + import distro + for moFile in distro.packageDb.getMoFiles(language): + translationDbs.append(GettextTranslationDb(moFile)) + + +def getMoFilesForPackage(packageName, language='', getDependencies=True): + """ + Look up the named package and find all gettext mo files within it and its + dependencies. It is possible to restrict the results to those of a certain + language, for example 'ja'. + """ + import distro + + result = [] + for filename in distro.packageDb.getFiles(packageName): + if isMoFile(filename, language): + result.append(filename) + + if getDependencies: + # Recurse: + for dep in distro.packageDb.getDependencies(packageName): + # We pass False to the inner call because getDependencies has already + # walked the full tree + result.extend(getMoFilesForPackage(dep, language, False)) + + return result + + +def loadTranslationsFromPackageMoFiles(packageName, getDependencies=True): + """ + Helper function which appends all of the gettext translation mo-files used by + the package (and its dependencies) to the translation database list. + """ + # Keep a list of mo-files that are already in use to avoid duplicates. + moFiles = {} + + def load(packageName, language='', getDependencies=True): + for moFile in getMoFilesForPackage(packageName, language, getDependencies): + # Searching the popt mo-files for translations makes gettext bail out, + # so we ignore them here. This is + # https://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=172155 . + if not('popt.mo' in moFile or moFile in moFiles): + try: + translationDbs.append(GettextTranslationDb(moFile)) + moFiles[moFile] = None + except (AttributeError, IndexError): + if config.config.debugTranslation: + #import traceback + # logger.log(traceback.format_exc()) + logger.log( + "Warning: Failed to load mo-file for translation: " + moFile) + + # Hack alert: + # + # The following special-case is necessary for Ubuntu, since their + # translations are shipped in a single huge package. The downside to + # this special case, aside from the simple fact that there is one, + # is that it makes automatic translations much slower. + + import distro + language = os.environ.get('LANGUAGE', os.environ['LANG'])[0:2] + if isinstance(distro.distro, distro.Ubuntu): + load('language-pack-gnome-%s' % language, language) + load(packageName, language, getDependencies) diff --git a/build/lib/dogtail/logging.py b/build/lib/dogtail/logging.py new file mode 100644 index 00000000000..7e73f163b83 --- /dev/null +++ b/build/lib/dogtail/logging.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +""" +Logging facilities + +Authors: Ed Rousseau <rousseau@redhat.com>, Zack Cerza <zcerza@redhat.com, David Malcolm <dmalcolm@redhat.com> +""" + +__author__ = """Ed Rousseau <rousseau@redhat.com>, +Zack Cerza <zcerza@redhat.com, +David Malcolm <dmalcolm@redhat.com> +""" +import os +import sys +import time +from config import config +import codecs + +# Timestamp class for file logs + + +class TimeStamp(object): + + """ + Generates timestamps tempfiles and log entries + """ + + def __init__(self): + self.now = "0" + self.timetup = time.localtime() + + def zeroPad(self, int, width=2): + """ + Pads an integer 'int' with zeroes, up to width 'width'. + + Returns a string. + + It will not truncate. If you call zeroPad(100, 2), '100' will be returned. + """ + if int < 10 ** width: + return ("0" * (width - len(str(int)))) + str(int) + else: + return str(int) + + # file stamper + def fileStamp(self, filename, addTime=True): + """ + Generates a filename stamp in the format of filename_YYYYMMDD-hhmmss. + A format of filename_YYYYMMDD can be used instead by specifying addTime = False. + """ + self.now = filename.strip() + "_" + self.timetup = time.localtime() + + # Should produce rel-eng style filestamps + # format it all pretty by chopping the tuple + fieldCount = 3 + if addTime: + fieldCount = fieldCount + 3 + for i in range(fieldCount): + if i == 3: + self.now = self.now + '-' + self.now = self.now + self.zeroPad(self.timetup[i]) + return self.now + + # Log entry stamper + def entryStamp(self): + """ + Generates a logfile entry stamp of YYYY.MM.DD HH:MM:SS + """ + self.timetup = time.localtime() + + # This will return a log entry formatted string in YYYY.MM.DD HH:MM:SS + for i in range(6): + # put in the year + if i == 0: + self.now = str(self.timetup[i]) + # Format Month and Day + elif i == 1 or i == 2: + self.now = self.now + "." + self.zeroPad(self.timetup[i]) + else: + # make the " " between Day and Hour and put in the hour + if i == 3: + self.now = self.now + " " + self.zeroPad(self.timetup[i]) + # Otherwise Use the ":" divider + else: + self.now = self.now + ":" + self.zeroPad(self.timetup[i]) + return self.now + + +class Logger(object): + + """ + Writes entries to standard out. + """ + stamper = TimeStamp() + + def __init__(self, logName, file=False, stdOut=True): + """ + name: the name of the log + file: The file object to log to. + stdOut: Whether to log to standard out. + """ + self.logName = logName + self.stdOut = stdOut + self.file = file # Handle to the logfile + if not self.file: + return + + scriptName = config.scriptName + if not scriptName: + scriptName = 'log' + self.fileName = scriptName + + # check to see if we can write to the logDir + if os.path.isdir(config.logDir): + self.findUniqueName() + else: + # If path doesn't exist, raise an exception + raise IOError( + "Log path %s does not exist or is not a directory" % config.logDir) + + def findUniqueName(self): + # generate a logfile name and check if it already exists + self.fileName = config.logDir + self.stamper.fileStamp(self.fileName) \ + + '_' + self.logName + i = 0 + while os.path.exists(self.fileName): + # Append the pathname + if i == 0: + self.fileName = self.fileName + "." + str(i) + else: + logsplit = self.fileName.split(".") + logsplit[-1] = str(i) + self.fileName = ".".join(logsplit) + i += 1 + + def createFile(self): + # Try to create the file and write the header info + print("Creating logfile at %s ..." % self.fileName) + self.file = codecs.open(self.fileName, mode='wb', encoding= + 'utf-8') + self.file.write("##### " + os.path.basename(self.fileName) + '\n') + self.file.flush() + + def log(self, message, newline=True, force=False): + """ + Hook used for logging messages. Might eventually be a virtual + function, but nice and simple for now. + + If force is True, log to a file irrespective of config.logDebugToFile. + """ + try: + message = message.decode('utf-8', 'replace') + except UnicodeEncodeError: + pass + + + # Try to open and write the result to the log file. + if isinstance(self.file, bool) and (force or config.logDebugToFile): + self.createFile() + + if force or config.logDebugToFile: + if newline: + self.file.write(message + '\n') + else: + self.file.write(message + ' ') + self.file.flush() + + if self.stdOut and config.logDebugToStdOut: + if newline: + print(message) + else: + print(message) + + +class ResultsLogger(Logger): + + """ + Writes entries into the Dogtail log + """ + + def __init__(self, stdOut=True): + Logger.__init__(self, 'results', file=True, stdOut=stdOut) + + # Writes the result of a test case comparison to the log + def log(self, entry): + """ + Writes the log entry. Requires a 1 {key: value} pair dict for an argument or else it will throw an exception. + """ + # We require a 1 key: value dict + # Strip all leading and trailing witespace from entry dict and convert + # to string for writing + + if len(entry) == 1: + key = entry.keys() + value = entry.values() + key = key[0] + value = value[0] + entry = str(key) + ": " + str(value) + else: + raise ValueError(entry) + print( + "Method argument requires a 1 {key: value} dict. Supplied argument not one {key: value}") + + Logger.log(self, self.stamper.entryStamp() + " " + entry, + force=True) + +debugLogger = Logger('debug', config.logDebugToFile) + +import traceback + + +def exceptionHook(exc, value, tb): # pragma: no cover + tbStringList = traceback.format_exception(exc, value, tb) + tbString = ''.join(tbStringList) + debugLogger.log(tbString) + sys.exc_clear() + +sys.excepthook = exceptionHook diff --git a/build/lib/dogtail/path.py b/build/lib/dogtail/path.py new file mode 100644 index 00000000000..5742cfb03e6 --- /dev/null +++ b/build/lib/dogtail/path.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +""" +Author: David Malcolm <dmalcolm@redhat.com> +""" +__author__ = """David Malcolm <dmalcolm@redhat.com>""" + + +class SearchPath(object): + + """ + Class used by the recording framework (and for more verbose script + logging) for identifying nodes in a persistent way, independent of the + style of script being written. + + Implemented as a list of (predicate, isRecursive) pairs, giving the + 'best' way to find the Accessible wrapped by a Node, starting at the + root and applying each search in turn. + + This is somewhat analagous to an absolute path in a filesystem, except + that some of searches may be recursive, rather than just searching + direct children. + + FIXME: try to ensure uniqueness + FIXME: need some heuristics to get 'good' searches, whatever + that means + """ + + def __init__(self): + self.__list = [] + + def __str__(self): + result = "{" + for (predicate, isRecursive) in self.__list: + result += "/(%s,%s)" % ( + predicate.describeSearchResult(), isRecursive) + return result + "}" + + # We need equality to work so that dicts of these work: + def __eq__(self, other): + # print "eq: self:%s"%self + # print " other:%s"%other + if len(self.__list) != len(other.__list): + # print "nonequal length" + return False + else: + for i in range(len(self.__list)): + if self.__list[i] != other.__list[i]: + return False + # print True + return True + + def append(self, predicate, isRecursive): + assert predicate + self.__list.append((predicate, isRecursive)) + + def __iter__(self): + return iter(self.__list) + + def length(self): + return len(self.__list) + + def makeScriptMethodCall(self): + """ + Used by the recording system. + + Generate the Python source code that will carry out this search. + """ + result = "" + for (predicate, isRecursive) in self.__list: + # print predicate + # print self.generateVariableName(predicate) + result += "." + predicate.makeScriptMethodCall(isRecursive) + return result + + def getRelativePath(self, other): + """ + Given another SearchPath instance, if the other is 'below' this + one, return a SearchPath that describes how to reach it relative + to this one (a copy of the second part of the list). Otherwise + return None. + """ + for i in range(len(self.__list)): + if self.__list[i] != other.__list[i]: + break + if i > 0: + # Slice from this point to the end: + result = SearchPath() + result.__list = other.__list[i + 1:] + + if False: + print("....................") + print("from %s" % self) + print("to %s" % other) + print("i=%s" % i) + print("relative path %s" % result) + print("....................") + + return result + else: + return None + + def getPrefix(self, n): + """ + Get the first n components of this instance as a new instance + """ + result = SearchPath() + for i in range(n): + result.__list.append(self.__list[i]) + return result + + def getPredicate(self, i): + (predicate, isRecursive) = self.__list[i] + return predicate diff --git a/build/lib/dogtail/predicate.py b/build/lib/dogtail/predicate.py new file mode 100644 index 00000000000..aa100e1f054 --- /dev/null +++ b/build/lib/dogtail/predicate.py @@ -0,0 +1,443 @@ +"""Predicates that can be used when searching for nodes. + +Author: David Malcolm <dmalcolm@redhat.com>""" +__author__ = 'David Malcolm <dmalcolm@redhat.com>' + +from i18n import TranslatableString +from gi.repository import GLib +from time import sleep +from logging import debugLogger as logger +from config import config + +def stringMatches(scriptName, reportedName): + assert isinstance(scriptName, TranslatableString) + + return scriptName.matchedBy(reportedName) + + +def makeScriptRecursiveArgument(isRecursive, defaultValue): + if isRecursive == defaultValue: + return "" + else: + return ", recursive=%s" % isRecursive + + +def makeCamel(string): + """ + Convert string to camelCaps + """ + string = str(string) + # FIXME: this function is probably really fragile, lots of difficult cases + # here + + # Sanitize string, replacing bad characters with spaces: + for char in ":;!@#$%^&*()-+=_~`\\/?|[]{}<>,.\t\n\r\"'": + string = string.replace(char, " ") + words = string.strip().split(" ") + for word in words: + word.strip + result = "" + firstWord = True + for word in words: + lowercaseWord = word.lower() + if firstWord: + result += lowercaseWord + firstWord = False + else: + result += lowercaseWord.capitalize() + return result + + +class Predicate(object): + + """Abstract base class representing a predicate function on nodes. + + It's more than just a function in that it has data and can describe itself""" + + def satisfiedByNode(self, node): + """Pure virtual method returning a boolean if the predicate is satisfied by the node""" + raise NotImplementedError + + def describeSearchResult(self, node): + raise NotImplementedError + + def makeScriptMethodCall(self, isRecursive): + """ + Method to generate a string containing a (hopefully) readable search + method call on a node (to be used when generating Python source code in + the event recorder) + """ + raise NotImplementedError + + def makeScriptVariableName(self): + """ + Method to generate a string containing a (hopefully) readable name + for a Node instance variable that would be the result of a search on + this predicate (to be used when generating Python source code in the + event recorder). + """ + raise NotImplementedError + + def __eq__(self, other): + """ + Predicates are considered equal if they are of the same subclass and + have the same data + """ + # print "predeq: self:%s"%self + # print " other:%s"%other + # print "predeq: selfdict:%s"%self.__dict__ + # print " otherdict:%s"%other.__dict__ + + if type(self) != type(other): + return False + else: + return self.__dict__ == other.__dict__ + + +class IsAnApplicationNamed(Predicate): + + """Search subclass that looks for an application by name""" + + def __init__(self, appName): + self.appName = TranslatableString(appName) + self.debugName = self.describeSearchResult() + self.satisfiedByNode = self._genCompareFunc() + + def _genCompareFunc(self): + def satisfiedByNode(node): + try: + return node.roleName == 'application' and stringMatches(self.appName, node.name) + except GLib.GError as e: + if 'name :1.0 was not provided' in e.message: + logger.log("Dogtail: warning: omiting possibly broken at-spi application record") + return False + else: + try: + sleep(config.defaults['searchWarningThreshold']) + return node.roleName == 'application' and stringMatches(self.appName, node.name) + except GLib.GError: + logger.log("Dogtail: warning: application may be hanging") + return False + return satisfiedByNode + + def describeSearchResult(self): + return '%s application' % self.appName + + def makeScriptMethodCall(self, isRecursive): + # ignores the isRecursive parameter + return "application(%s)" % self.appName + + def makeScriptVariableName(self): + return makeCamel(self.appName) + "App" + + +class GenericPredicate(Predicate): + + """SubtreePredicate subclass that takes various optional search fields""" + + def __init__(self, name=None, roleName=None, description=None, label=None, debugName=None): + if name: + self.name = TranslatableString(name) + else: + self.name = None + self.roleName = roleName + self.description = description + if label: + self.label = TranslatableString(label) + else: + self.label = None + + if debugName: + self.debugName = debugName + else: + if label: + self.debugName = "labelled '%s'" % self.label + else: + self.debugName = "child with" + if name: + self.debugName += " name=%s" % self.name + if roleName: + self.debugName += " roleName='%s'" % roleName + if description: + self.debugName += " description='%s'" % description + assert self.debugName + + self.satisfiedByNode = self._genCompareFunc() + + def _genCompareFunc(self): + def satisfiedByNode(node): + # labelled nodes are handled specially: + if self.label: + # this reverses the search; we're looking for a node with LABELLED_BY + # and then checking the label, rather than looking for a label and + # then returning whatever LABEL_FOR targets + if node.labeller: + return stringMatches(self.label, node.labeller.name) + else: + return False + else: + # Ensure the node matches any criteria that were set: + try: + if self.name: + if not stringMatches(self.name, node.name): + return False + if self.roleName: + if self.roleName != node.roleName: + return False + if self.description: + if self.description != node.description: + return False + except GLib.GError as e: + if 'name :1.0 was not provided' in e.message: + logger.log("Dogtail: warning: omiting possibly broken at-spi application record") + return False + else: + raise e + return True + return satisfiedByNode + + def describeSearchResult(self): + return self.debugName + + def makeScriptMethodCall(self, isRecursive): + if self.label: + args = "label=%s" % self.label + else: + args = "" + if self.name: + print(self.name) + args += " name=%s" % self.name + if self.roleName: + args += " roleName='%s'" % self.roleName + if self.description: + args += " description='%s'" % self.description + return "child(%s%s)" % (args, makeScriptRecursiveArgument(isRecursive, True)) + + def makeScriptVariableName(self): + if self.label: + return makeCamel(self.label) + "Node" + else: + if self.name: + return makeCamel(self.name) + "Node" + if self.roleName: + return makeCamel(self.roleName) + "Node" + if self.description: + return makeCamel(self.description) + "Node" + + +class IsNamed(Predicate): + + """Predicate subclass that looks simply by name""" + + def __init__(self, name): + self.name = TranslatableString(name) + self.debugName = self.describeSearchResult() + self.satisfiedByNode = self._genCompareFunc() + + def _genCompareFunc(self): + def satisfiedByNode(node): + return stringMatches(self.name, node.name) + return satisfiedByNode + + def describeSearchResult(self): + return "named %s" % self.name + + def makeScriptMethodCall(self, isRecursive): + return "child(name=%s%s)" % (self.name, makeScriptRecursiveArgument(isRecursive, True)) + + def makeScriptVariableName(self): + return makeCamel(self.name) + "Node" + + +class IsAWindowNamed(Predicate): + + """Predicate subclass that looks for a top-level window by name""" + + def __init__(self, windowName): + self.windowName = TranslatableString(windowName) + self.debugName = self.describeSearchResult() + self.satisfiedByNode = self._genCompareFunc() + + def _genCompareFunc(self): + def satisfiedByNode(node): + return node.roleName == 'frame' and stringMatches(self.windowName, node.name) + return satisfiedByNode + + def describeSearchResult(self): + return "%s window" % self.windowName + + def makeScriptMethodCall(self, isRecursive): + return "window(%s%s)" % (self.windowName, makeScriptRecursiveArgument(isRecursive, False)) + + def makeScriptVariableName(self): + return makeCamel(self.windowName) + "Win" + + +class IsAWindow(Predicate): + + """Predicate subclass that looks for top-level windows""" + + def __init__(self): + self.satisfiedByNode = lambda node: node.roleName == 'frame' + + def describeSearchResult(self): + return "window" + + +class IsADialogNamed(Predicate): + + """Predicate subclass that looks for a top-level dialog by name""" + + def __init__(self, dialogName): + self.dialogName = TranslatableString(dialogName) + self.debugName = self.describeSearchResult() + self.satisfiedByNode = self._genCompareFunc() + + def _genCompareFunc(self): + def satisfiedByNode(node): + return node.roleName == 'dialog' and stringMatches(self.dialogName, node.name) + return satisfiedByNode + + def describeSearchResult(self): + return '%s dialog' % self.dialogName + + def makeScriptMethodCall(self, isRecursive): + return "dialog(%s%s)" % (self.dialogName, makeScriptRecursiveArgument(isRecursive, False)) + + def makeScriptVariableName(self): + return makeCamel(self.dialogName) + "Dlg" + + +class IsLabelledBy(Predicate): + + """Predicate: is this node labelled by another node""" + pass + + +class IsLabelledAs(Predicate): + + """Predicate: is this node labelled with the text string (i.e. by another node with that as a name)""" + + def __init__(self, labelText): + self.labelText = TranslatableString(labelText) + self.debugName = self.describeSearchResult() + self.satisfiedByNode = self._genCompareFunc() + + def _genCompareFunc(self): + def satisfiedByNode(node): + # FIXME + if node.labeller: + return stringMatches(self.labelText, node.labeller.name) + else: + return False + return satisfiedByNode + + def describeSearchResult(self): + return 'labelled %s' % self.labelText + + def makeScriptMethodCall(self, isRecursive): + return "child(label=%s%s)" % (self.labelText, makeScriptRecursiveArgument(isRecursive, True)) + + def makeScriptVariableName(self): + return makeCamel(self.labelText) + "Node" + + +class IsAMenuNamed(Predicate): + + """Predicate subclass that looks for a menu by name""" + + def __init__(self, menuName): + self.menuName = TranslatableString(menuName) + self.debugName = self.describeSearchResult() + self.satisfiedByNode = lambda node: node.roleName == 'menu' and \ + stringMatches(self.menuName, node.name) + + def describeSearchResult(self): + return '%s menu' % (self.menuName) + + def makeScriptMethodCall(self, isRecursive): + return "menu(%s%s)" % (self.menuName, makeScriptRecursiveArgument(isRecursive, True)) + + def makeScriptVariableName(self): + return makeCamel(self.menuName) + "Menu" + + +class IsAMenuItemNamed(Predicate): + + """Predicate subclass that looks for a menu item by name""" + + def __init__(self, menuItemName): + self.menuItemName = TranslatableString(menuItemName) + self.debugName = self.describeSearchResult() + self.satisfiedByNode = lambda node: \ + node.roleName.endswith('menu item') and \ + stringMatches(self.menuItemName, node.name) + + def describeSearchResult(self): + return '%s menuitem' % (self.menuItemName) + + def makeScriptMethodCall(self, isRecursive): + return "menuItem(%s%s)" % (self.menuItemName, makeScriptRecursiveArgument(isRecursive, True)) + + def makeScriptVariableName(self): + return makeCamel(self.menuItemName) + "MenuItem" + + +class IsATextEntryNamed(Predicate): + + """Predicate subclass that looks for a text entry by name""" + + def __init__(self, textEntryName): + self.textEntryName = TranslatableString(textEntryName) + self.debugName = self.describeSearchResult() + self.satisfiedByNode = lambda node: node.roleName == 'text' and \ + stringMatches(self.textEntryName, node.name) + + def describeSearchResult(self): + return '%s textentry' % (self.textEntryName) + + def makeScriptMethodCall(self, isRecursive): + return "textentry(%s%s)" % (self.textEntryName, makeScriptRecursiveArgument(isRecursive, True)) + + def makeScriptVariableName(self): + return makeCamel(self.textEntryName) + "Entry" + + +class IsAButtonNamed(Predicate): + + """Predicate subclass that looks for a button by name""" + + def __init__(self, buttonName): + self.buttonName = TranslatableString(buttonName) + self.debugName = self.describeSearchResult() + self.satisfiedByNode = lambda node: node.roleName == 'push button' \ + and stringMatches(self.buttonName, node.name) + + def describeSearchResult(self): + return '%s button' % (self.buttonName) + + def makeScriptMethodCall(self, isRecursive): + return "button(%s%s)" % (self.buttonName, makeScriptRecursiveArgument(isRecursive, True)) + + def makeScriptVariableName(self): + return makeCamel(self.buttonName) + "Button" + + +class IsATabNamed(Predicate): + + """Predicate subclass that looks for a tab by name""" + + def __init__(self, tabName): + self.tabName = TranslatableString(tabName) + self.debugName = self.describeSearchResult() + self.satisfiedByNode = lambda node: node.roleName == 'page tab' and \ + stringMatches(self.tabName, node.name) + + def describeSearchResult(self): + return '%s tab' % (self.tabName) + + def makeScriptMethodCall(self, isRecursive): + return "tab(%s%s)" % (self.tabName, makeScriptRecursiveArgument(isRecursive, True)) + + def makeScriptVariableName(self): + return makeCamel(self.tabName) + "Tab" diff --git a/build/lib/dogtail/procedural.py b/build/lib/dogtail/procedural.py new file mode 100644 index 00000000000..c87ae86181c --- /dev/null +++ b/build/lib/dogtail/procedural.py @@ -0,0 +1,455 @@ +""" +Dogtail's procedural UI +All the classes here are intended to be single-instance, except for Action. +""" +__author__ = 'Zack Cerza <zcerza@redhat.com>' +# +# +# WARNING: Here There Be Dragons (TM) # +# +# If you don't understand how to use this API, you almost certainly don't # +# want to read the code first. We make use of some very non-intuitive # +# features of Python in order to make the API very simplistic. Therefore, # +# you should probably only read this code if you're already familiar with # +# some of Python's advanced features. You have been warned. ;) # +# +# + +import tree +import predicate +from config import config +from utils import Lock +import rawinput + +#FocusError = "FocusError: %s not found" + + +class FocusError(Exception): + pass + +import errors + + +def focusFailed(pred): + errors.warn('The requested widget could not be focused: %s' % + pred.debugName) + +ENOARGS = "At least one argument is needed" + + +class FocusBase(object): + + """ + The base for every class in the module. Does nothing special, really. + """ + node = None + + def __getattr__(self, name): + # Fold all the Node's AT-SPI properties into the Focus object. + try: + return getattr(self.node, name) + except AttributeError: + raise AttributeError(name) + + def __setattr__(self, name, value): + # Fold all the Node's AT-SPI properties into the Focus object. + if name == 'node': + setattr(self.__class__, name, value) + else: + try: + setattr(self.node, name, value) + except AttributeError: + raise AttributeError(name) + + +class FocusApplication (FocusBase): + + """ + Keeps track of which application is currently focused. + """ + desktop = tree.root + + def __call__(self, name): + """ + Search for an application that matches and refocus on the given name. + """ + try: + pred = predicate.IsAnApplicationNamed(name) + app = self.desktop.findChild( + pred, recursive=False, retry=False) + except tree.SearchError: + if config.fatalErrors: + raise FocusError(name) + else: + focusFailed(pred) + return False + if app: + FocusApplication.node = app + FocusDialog.node = None + FocusWindow.node = None + FocusWidget.node = None + return True + + +class FocusDesktop (FocusBase): + + """ + This isn't used yet, and may never be used. + """ + pass + + +class FocusWindow (FocusBase): + + """ + Keeps track of which window is currently focused. + """ + + def __call__(self, name): + """ + Search for a dialog that matches the given name and refocus on it. + """ + result = None + pred = predicate.IsAWindowNamed(name) + try: + result = FocusApplication.node.findChild( + pred, requireResult=False, recursive=False) + except AttributeError: + pass + if result: + FocusWindow.node = result + FocusDialog.node = None + FocusWidget.node = None + else: + if config.fatalErrors: + raise FocusError(pred.debugName) + else: + focusFailed(pred) + return False + return True + + +class FocusDialog (FocusBase): + + """ + Keeps track of which dialog is currently focused. + """ + + def __call__(self, name): + """ + Search for a dialog that matches the given name and refocus on it. + """ + result = None + pred = predicate.IsADialogNamed(name) + try: + result = FocusApplication.node.findChild( + pred, requireResult=False, recursive=False) + except AttributeError: + pass + if result: + FocusDialog.node = result + FocusWidget.node = None + else: + if config.fatalErrors: + raise FocusError(pred.debugName) + else: + focusFailed(pred) + return False + return True + + +class FocusWidget (FocusBase): + + """ + Keeps track of which widget is currently focused. + """ + + def findByPredicate(self, pred): + result = None + try: + result = FocusWidget.node.findChild( + pred, requireResult=False, retry=False) + except AttributeError: + pass + if result: + FocusWidget.node = result + else: + try: + result = FocusDialog.node.findChild( + pred, requireResult=False, retry=False) + except AttributeError: + pass + if result: + FocusWidget.node = result + else: + try: + result = FocusWindow.node.findChild( + pred, requireResult=False, retry=False) + except AttributeError: + pass + if result: + FocusWidget.node = result + else: + try: + result = FocusApplication.node.findChild( + pred, requireResult=False, retry=False) + if result: + FocusWidget.node = result + except AttributeError: + if config.fatalErrors: + raise FocusError(pred) + else: + focusFailed(pred) + return False + + if result is None: + FocusWidget.node = result + if config.fatalErrors: + raise FocusError(pred.debugName) + else: + focusFailed(pred) + return False + return True + + def __call__(self, name='', roleName='', description=''): + """ + If name, roleName or description are specified, search for a widget that matches and refocus on it. + """ + if not name and not roleName and not description: + raise TypeError(ENOARGS) + + # search for a widget. + pred = predicate.GenericPredicate(name=name, + roleName=roleName, description=description) + return self.findByPredicate(pred) + + +class Focus (FocusBase): + + """ + The container class for the focused application, dialog and widget. + """ + + def __getattr__(self, name): + raise AttributeError(name) + + def __setattr__(self, name, value): + if name in ('application', 'dialog', 'widget', 'window'): + self.__dict__[name] = value + else: + raise AttributeError(name) + + desktop = tree.root + application = FocusApplication() + app = application # shortcut :) + dialog = FocusDialog() + window = FocusWindow() + frame = window + widget = FocusWidget() + + def button(self, name): + """ + A shortcut to self.widget.findByPredicate(predicate.IsAButtonNamed(name)) + """ + return self.widget.findByPredicate(predicate.IsAButtonNamed(name)) + + def icon(self, name): + """ + A shortcut to self.widget(name, roleName = 'icon') + """ + return self.widget(name=name, roleName='icon') + + def menu(self, name): + """ + A shortcut to self.widget.findByPredicate(predicate.IsAMenuNamed(name)) + """ + return self.widget.findByPredicate(predicate.IsAMenuNamed(name)) + + def menuItem(self, name): + """ + A shortcut to self.widget.findByPredicate(predicate.IsAMenuItemNamed(name)) + """ + return self.widget.findByPredicate(predicate.IsAMenuItemNamed(name)) + + def table(self, name=''): + """ + A shortcut to self.widget(name, roleName 'table') + """ + return self.widget(name=name, roleName='table') + + def tableCell(self, name=''): + """ + A shortcut to self.widget(name, roleName 'table cell') + """ + return self.widget(name=name, roleName='table cell') + + def text(self, name=''): + """ + A shortcut to self.widget.findByPredicate(IsATextEntryNamed(name)) + """ + return self.widget.findByPredicate(predicate.IsATextEntryNamed(name)) + + +class Action (FocusWidget): + + """ + Aids in executing AT-SPI actions, refocusing the widget if necessary. + """ + + def __init__(self, action): + """ + action is a string with the same name as the AT-SPI action you wish to execute using this class. + """ + self.action = action + + def __call__(self, name='', roleName='', description='', delay=config.actionDelay): + """ + If name, roleName or description are specified, first search for a widget that matches and refocus on it. + Then execute the action. + """ + if name or roleName or description: + FocusWidget.__call__( + self, name=name, roleName=roleName, description=description) + self.node.doActionNamed(self.action) + + def __getattr__(self, attr): + return getattr(FocusWidget.node, attr) + + def __setattr__(self, attr, value): + if attr == 'action': + self.__dict__[attr] = value + else: + setattr(FocusWidget, attr, value) + + def button(self, name): + """ + A shortcut to self(name, roleName = 'push button') + """ + self.__call__(name=name, roleName='push button') + + def menu(self, name): + """ + A shortcut to self(name, roleName = 'menu') + """ + self.__call__(name=name, roleName='menu') + + def menuItem(self, name): + """ + A shortcut to self(name, roleName = 'menu item') + """ + self.__call__(name=name, roleName='menu item') + + def table(self, name=''): + """ + A shortcut to self(name, roleName 'table') + """ + self.__call__(name=name, roleName='table') + + def tableCell(self, name=''): + """ + A shortcut to self(name, roleName 'table cell') + """ + self.__call__(name=name, roleName='table cell') + + def text(self, name=''): + """ + A shortcut to self(name, roleName = 'text') + """ + self.__call__(name=name, roleName='text') + + +class Click (Action): + + """ + A special case of Action, Click will eventually handle raw mouse events. + """ + primary = 1 + middle = 2 + secondary = 3 + + def __init__(self): + Action.__init__(self, 'click') + + def __call__(self, name='', roleName='', description='', raw=True, button=primary, delay=config.actionDelay): + """ + By default, execute a raw mouse event. + If raw is False or if button evaluates to False, just pass the rest of + the arguments to Action. + """ + if name or roleName or description: + FocusWidget.__call__( + self, name=name, roleName=roleName, description=description) + if raw and button: + # We're doing a raw mouse click + Click.node.click(button) + else: + Action.__call__( + self, name=name, roleName=roleName, description=description, delay=delay) + + +class Select (Action): + + """ + Aids in selecting and deselecting widgets, i.e. page tabs + """ + select = 'select' + deselect = 'deselect' + + def __init__(self, action): + """ + action must be 'select' or 'deselect'. + """ + if action not in (self.select, self.deselect): + raise ValueError(action) + Action.__init__(self, action) + + def __call__(self, name='', roleName='', description='', delay=config.actionDelay): + """ + If name, roleName or description are specified, first search for a widget that matches and refocus on it. + Then execute the action. + """ + if name or roleName or description: + FocusWidget.__call__( + self, name=name, roleName=roleName, description=description) + func = getattr(self.node, self.action) + func() + + +def type(text): + if focus.widget.node: + focus.widget.node.typeText(text) + else: + rawinput.typeText(text) + + +def keyCombo(combo): + if focus.widget.node: + focus.widget.node.keyCombo(combo) + else: + rawinput.keyCombo(combo) + + +def run(application, arguments='', appName=''): + from utils import run as utilsRun + pid = utilsRun(application + ' ' + arguments, appName=appName) + focus.application(application) + return pid + +import os +# tell sniff not to use auto-refresh while script using this module is running +# may have already been locked by dogtail.tree +if not os.path.exists('/tmp/sniff_refresh.lock'): # pragma: no cover + sniff_lock = Lock(lockname='sniff_refresh.lock', randomize=False) + try: + sniff_lock.lock() + except OSError: + pass # lock was already present from other script instance or leftover from killed instance + # lock should unlock automatically on script exit. + +focus = Focus() +click = Click() +activate = Action('activate') +openItem = Action('open') +menu = Action('menu') +select = Select(Select.select) +deselect = Select(Select.deselect) diff --git a/build/lib/dogtail/rawinput.py b/build/lib/dogtail/rawinput.py new file mode 100644 index 00000000000..7100b29bfae --- /dev/null +++ b/build/lib/dogtail/rawinput.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8 -*- +""" +Handles raw input using AT-SPI event generation. + +Note: Think of keyvals as keysyms, and keynames as keystrings. + +Authors: David Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com> +""" + +__author__ = """ +David Malcolm <dmalcolm@redhat.com>, +Zack Cerza <zcerza@redhat.com> +""" +import gi + +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') + +from gi.repository import Gdk +from config import config +from utils import doDelay +from logging import debugLogger as logger +from pyatspi import Registry as registry +from pyatspi import (KEY_SYM, KEY_PRESS, KEY_PRESSRELEASE, KEY_RELEASE) +from exceptions import ValueError +from __builtin__ import unicode, unichr + + +def doTypingDelay(): + doDelay(config.typingDelay) + + +def checkCoordinates(x, y): + if x < 0 or y < 0: + raise ValueError( + "Attempting to generate a mouse event at negative coordinates: (%s,%s)" % (x, y)) + + +def click(x, y, button=1, check=True): + """ + Synthesize a mouse button click at (x,y) + """ + if check: + checkCoordinates(x, y) + logger.log("Mouse button %s click at (%s,%s)" % (button, x, y)) + registry.generateMouseEvent(x, y, 'b%sc' % button) + doDelay(config.actionDelay) + + +def doubleClick(x, y, button=1, check=True): + """ + Synthesize a mouse button double-click at (x,y) + """ + if check: + checkCoordinates(x, y) + logger.log("Mouse button %s doubleclick at (%s,%s)" % (button, x, y)) + registry.generateMouseEvent(x, y, 'b%sd' % button) + doDelay() + + +def press(x, y, button=1, check=True): + """ + Synthesize a mouse button press at (x,y) + """ + if check: + checkCoordinates(x, y) + logger.log("Mouse button %s press at (%s,%s)" % (button, x, y)) + registry.generateMouseEvent(x, y, 'b%sp' % button) + doDelay() + + +def release(x, y, button=1, check=True): + """ + Synthesize a mouse button release at (x,y) + """ + if check: + checkCoordinates(x, y) + logger.log("Mouse button %s release at (%s,%s)" % (button, x, y)) + registry.generateMouseEvent(x, y, 'b%sr' % button) + doDelay() + + +def absoluteMotion(x, y, mouseDelay=None, check=True): + """ + Synthesize mouse absolute motion to (x,y) + """ + if check: + checkCoordinates(x, y) + logger.log("Mouse absolute motion to (%s,%s)" % (x, y)) + registry.generateMouseEvent(x, y, 'abs') + if mouseDelay: + doDelay(mouseDelay) + else: + doDelay() + + +def relativeMotion(x, y, mouseDelay=None): + logger.log("Mouse relative motion of (%s,%s)" % (x, y)) + registry.generateMouseEvent(x, y, 'rel') + if mouseDelay: + doDelay(mouseDelay) + else: + doDelay() + + +def drag(fromXY, toXY, button=1, check=True): + """ + Synthesize a mouse press, drag, and release on the screen. + """ + logger.log("Mouse button %s drag from %s to %s" % (button, fromXY, toXY)) + + (x, y) = fromXY + press(x, y, button, check) + # doDelay() + + (x, y) = toXY + absoluteMotion(x, y, check=check) + doDelay() + + release(x, y, button, check) + doDelay() + + +def typeText(string): + """ + Types the specified string, one character at a time. + Please note, you may have to set a higher typing delay, + if your machine misses/switches the characters typed. + Needed sometimes on slow setups/VMs typing non-ASCII utf8 chars. + """ + if not isinstance(string, unicode): + string = string.decode('utf-8') + for char in string: + pressKey(char) + +keyNameAliases = { + 'enter': 'Return', + 'esc': 'Escape', + 'alt': 'Alt_L', + 'control': 'Control_L', + 'ctrl': 'Control_L', + 'shift': 'Shift_L', + 'del': 'Delete', + 'ins': 'Insert', + 'pageup': 'Page_Up', + 'pagedown': 'Page_Down', + ' ': 'space', + '\t': 'Tab', + '\n': 'Return' +} + + +# TODO: Dead code +def keySymToUniChar(keySym): # pragma: no cover + i = Gdk.keyval_to_unicode(keySym) + if i: + UniChar = unichr(i) + else: + UniChar = '' + return UniChar + + +def uniCharToKeySym(uniChar): + # OK, if it's not actually unicode we can fix that, right? + if not isinstance(uniChar, unicode): + uniChar = unicode(uniChar, 'utf-8') + i = ord(uniChar) + keySym = Gdk.unicode_to_keyval(i) + return keySym + + +# dead code +def keySymToKeyName(keySym): # pragma: no cover + return Gdk.keyval_name(keySym) + + +def keyNameToKeySym(keyName): + keyName = keyNameAliases.get(keyName.lower(), keyName) + keySym = Gdk.keyval_from_name(keyName) + # various error 'codes' returned for non-recognized chars in versions of GTK3.X + if keySym == 0xffffff or keySym == 0x0 or keySym is None: + try: + keySym = uniCharToKeySym(keyName) + except: # not even valid utf-8 char + try: # Last attempt run at a keyName ('Meta_L', 'Dash' ...) + keySym = getattr(Gdk, 'KEY_' + keyName) + except AttributeError: + raise KeyError(keyName) + return keySym + + +def keyNameToKeyCode(keyName): + """ + Use GDK to get the keycode for a given keystring. + + Note that the keycode returned by this function is often incorrect when + the requested keystring is obtained by holding down the Shift key. + + Generally you should use uniCharToKeySym() and should only need this + function for nonprintable keys anyway. + """ + keymap = Gdk.Keymap.get_for_display(Gdk.Display.get_default()) + entries = keymap.get_entries_for_keyval( + Gdk.keyval_from_name(keyName)) + try: + return entries[1][0].keycode + except TypeError: + pass + + +def pressKey(keyName): + """ + Presses (and releases) the key specified by keyName. + keyName is the English name of the key as seen on the keyboard. Ex: 'enter' + Names are looked up in Gdk.KEY_ If they are not found there, they are + looked up by uniCharToKeySym(). + """ + keySym = keyNameToKeySym(keyName) + registry.generateKeyboardEvent(keySym, None, KEY_SYM) + doTypingDelay() + + +def keyCombo(comboString): + """ + Generates the appropriate keyboard events to simulate a user pressing the + specified key combination. + + comboString is the representation of the key combo to be generated. + e.g. '<Control><Alt>p' or '<Control><Shift>PageUp' or '<Control>q' + """ + strings = [] + for s in comboString.split('<'): + if s: + for S in s.split('>'): + if S: + S = keyNameAliases.get(S.lower(), S) + strings.append(S) + for s in strings: + if not hasattr(Gdk, s): + if not hasattr(Gdk, 'KEY_' + s): + raise ValueError("Cannot find key %s" % s) + modifiers = strings[:-1] + finalKey = strings[-1] + for modifier in modifiers: + code = keyNameToKeyCode(modifier) + registry.generateKeyboardEvent(code, None, KEY_PRESS) + code = keyNameToKeyCode(finalKey) + registry.generateKeyboardEvent(code, None, KEY_PRESSRELEASE) + for modifier in modifiers: + code = keyNameToKeyCode(modifier) + registry.generateKeyboardEvent(code, None, KEY_RELEASE) + doDelay() diff --git a/build/lib/dogtail/sessions.py b/build/lib/dogtail/sessions.py new file mode 100644 index 00000000000..8d4bcce6a37 --- /dev/null +++ b/build/lib/dogtail/sessions.py @@ -0,0 +1,231 @@ +import time +import os +import pwd +import errno +import re +import subprocess +import signal +import tempfile +import random +import glob +from dogtail.config import config + + +def scratchFile(label): # pragma: no cover + """Uses tempfile.NamedTemporaryFile() to create a unique tempfile in + config.scratchDir, with a filename like: + dogtail-headless-<label>.<random junk>""" + prefix = "dogtail-headless-" + return tempfile.NamedTemporaryFile(prefix="%s%s." % (prefix, label), + dir=config.scratchDir) + + +def testBinary(path): # pragma: no cover + if (path.startswith(os.path.sep) or + path.startswith(os.path.join('.', '')) or + path.startswith(os.path.join('..', ''))): + if not os.path.exists(path): + raise IOError(errno.ENOENT, "No such file", path) + if not os.access(path, os.X_OK): + raise IOError(errno.ENOEXEC, "Permission denied", path) + return True + + +def get_username(): # pragma: no cover + return pwd.getpwuid(os.getuid())[0] + + +class Subprocess(object): # pragma: no cover + + def __init__(self, cmdList, environ=None): + testBinary(cmdList[0]) + self.cmdList = cmdList + self.environ = environ + self._exitCode = None + + def start(self): + if self.environ is None: + self.environ = os.environ + self.popen = subprocess.Popen( + self.cmdList, env=self.environ) # , stdout = subprocess.PIPE, + # stderr = subprocess.STDOUT, close_fds = True) + return self.popen.pid + + def wait(self): + return self.popen.wait() + + def stop(self): + # The following doesn't exist in python < 2.6, if you can believe it. + # self.popen.terminate() + os.kill(self.popen.pid, signal.SIGTERM) + + @property + def exitCode(self): + if self._exitCode is None: + self._exitCode = self.wait() + return self._exitCode + + +class XServer(Subprocess): # pragma: no cover + + def __init__(self, server="/usr/bin/Xorg", + xinitrc="/etc/X11/xinit/Xclients", + resolution="1024x768x16"): + """resolution is only used with Xvfb.""" + testBinary(server) + self.server = server + self._exitCode = None + self.xinit = "/usr/bin/xinit" + self.display = None + self.xinitrc = xinitrc + self.resolution = resolution + + @staticmethod + def findFreeDisplay(): + tmp = os.listdir('/tmp') + pattern = re.compile('\.X([0-9]+)-lock') + usedDisplays = [] + for file in tmp: + match = re.match(pattern, file) + if match: + usedDisplays.append(int(match.groups()[0])) + if not usedDisplays: + return ':0' + usedDisplays.sort() + return ':' + str(usedDisplays[-1] + 1) + + @property + def cmdList(self): + self.display = self.findFreeDisplay() + cmd = [] + if self.xinit: + cmd.append(self.xinit) + if self.xinitrc: + cmd.append(self.xinitrc) + cmd.append('--') + cmd.append(self.server) + cmd.append(self.display) + cmd.extend(['-ac', '-noreset']) + if self.server.endswith('Xvfb'): + cmd.extend(['-screen', '0', self.resolution]) + cmd.append('-shmem') + return cmd + + def start(self): + print(' '.join(self.cmdList)) + self.popen = subprocess.Popen(self.cmdList) + return self.popen.pid + + +class Script(Subprocess): # pragma: no cover + pass + + +class Session(object): # pragma: no cover + + cookieName = "DOGTAIL_SESSION_COOKIE" + + def __init__(self, sessionBinary, scriptCmdList=[], scriptDelay=20, logout=True): + testBinary(sessionBinary) + self.sessionBinary = sessionBinary + self.script = Script(scriptCmdList) + self.scriptDelay = scriptDelay + self.logout = logout + self.xserver = XServer() + self._cookie = None + self._environment = None + + def start(self): + self.xinitrcFileObj = scratchFile('xinitrc') + self.xserver.xinitrc = self.xinitrcFileObj.name + self._buildXInitRC(self.xinitrcFileObj) + xServerPid = self.xserver.start() + time.sleep(self.scriptDelay) + self.script.environ = self.environment + scriptPid = self.script.start() + return (xServerPid, scriptPid) + + @property + def environment(self): + def isSessionProcess(fileName): + try: + if os.path.realpath(path + 'exe') != ('/usr/bin/plasma-desktop' + if self.sessionBinary.split('/')[-1] == 'startkde' + else self.sessionBinary): + return False + except OSError: + return False + pid = fileName.split('/')[2] + if pid == 'self' or pid == str(os.getpid()): + return False + return True + + def getEnvDict(fileName): + try: + envString = open(fileName, 'r').read() + except IOError: + return {} + envItems = envString.split('\x00') + envDict = {} + for item in envItems: + if not '=' in item: + continue + k, v = item.split('=', 1) + envDict[k] = v + return envDict + + def isSessionEnv(envDict): + if not envDict: + return False + if envDict.get(self.cookieName, 'notacookie') == self.cookie: + return True + return False + + for path in glob.glob('/proc/*/'): + if not isSessionProcess(path): + continue + envFile = path + 'environ' + envDict = getEnvDict(envFile) + if isSessionEnv(envDict): + # print path + # print envDict + self._environment = envDict + if not self._environment: + raise RuntimeError("Can't find our environment!") + return self._environment + + def wait(self): + self.script.wait() + return self.xserver.wait() + + def stop(self): + try: + self.script.stop() + except OSError: + pass + self.xserver.stop() + + def attemptLogout(self): + logoutScript = Script('dogtail-logout', + environ=self.environment) + logoutScript.start() + logoutScript.wait() + + @property + def cookie(self): + if not self._cookie: + self._cookie = "%X" % random.getrandbits(16) + return self._cookie + + def _buildXInitRC(self, fileObj): + lines = [ + "export %s=%s" % (self.cookieName, self.cookie), + "gsettings set org.gnome.desktop.interface toolkit-accessibility true", + ". /etc/X11/xinit/xinitrc-common", + "export %s" % self.cookieName, + "exec -l $SHELL -c \"$CK_XINIT_SESSION $SSH_AGENT %s\"" % + (self.sessionBinary), + ""] + + fileObj.write('\n'.join(lines).strip()) + fileObj.flush() diff --git a/build/lib/dogtail/tc.py b/build/lib/dogtail/tc.py new file mode 100644 index 00000000000..c7ef9f5bc23 --- /dev/null +++ b/build/lib/dogtail/tc.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +"""Test Case magic + +Author: Ed Rousseau <rousseau@redhat.com>""" +__author__ = "Ed Rousseau <rousseau@redhat.com>" + +import os +import os.path +from config import config +from logging import ResultsLogger, TimeStamp +from PIL import Image, ImageChops, ImageStat +from __builtin__ import unicode, long + + +class TC(object): # pragma: no cover + + """ + The Test Case Superclass + """ + logger = ResultsLogger() + + def __init__(self): + self.encoding = config.encoding + # ascii + unicode. 8 bit extended char has been ripped out + self.supportedtypes = ( + "ascii", "utf-8", "utf-16", "utf-16-be", "utf-16-le", "unicode-escape", "raw-unicode-escape", + "big5", "gb18030", "eucJP", "eucKR", "shiftJIS") + + # String comparison function + def compare(self, label, baseline, undertest, encoding=config.encoding): + """ + Compares 2 strings to see if they are the same. The user may specify + the encoding to which the two strings are to be normalized for the + comparison. Default encoding is the default system encoding. + Normalization to extended 8 bit charactersets is not supported. + + When the origin of either baseline or undertest is a text file whose + encoding is something other than ASCII, it is necessary to use + codecs.open() instead of open(), so the file's encoding may be + specified. + """ + self.label = label.strip() + self.baseline = baseline + self.undertest = undertest + for string in [self.baseline, self.undertest]: + try: + string = unicode(string, 'utf-8') + except TypeError: + pass + self.encoding = encoding + + # Normalize the encoding type for the comparaison based on + # self.encoding + if self.encoding in self.supportedtypes: + self.baseline = (self.baseline).encode(self.encoding) + self.undertest = (self.undertest).encode(self.encoding) + # Compare the strings + if self.baseline == self.undertest: + self.result = {self.label: "Passed"} + else: + self.result = {self.label: "Failed - " + self.encoding + + " strings do not match. " + self.baseline + " expected: Got " + self.undertest} + # Pass the test result to the ResultsLogger for writing + TC.logger.log(self.result) + return self.result + + else: + # We should probably raise an exception here + self.result = { + self.label: "ERROR - " + self.encoding + " is not a supported encoding type"} + TC.logger.log(self.result) + return self.result + + +# String Test Case subclass +class TCString(TC): # pragma: no cover + + """ + String Test Case Class + """ + + def __init__(self): + TC.__init__(self) + +# Image test case subclass + + +class TCImage(TC): # pragma: no cover + + """ + Image Test Case Class. + """ + + def compare(self, label, baseline, undertest): + for _file in (baseline, undertest): + if type(_file) is not unicode and type(_file) is not str: + raise TypeError("Need filenames!") + self.label = label.strip() + self.baseline = baseline.strip() + self.undertest = undertest.strip() + diffName = TimeStamp().fileStamp("diff") + ".png" + self.diff = os.path.normpath( + os.path.sep.join((config.scratchDir, diffName))) + + self.baseImage = Image.open(self.baseline) + self.testImage = Image.open(self.undertest) + try: + if self.baseImage.size != self.testImage.size: + self.result = { + self.label: "Failed - images are different sizes"} + raise StopIteration + + self.diffImage = ImageChops.difference(self.baseImage, + self.testImage) + self.diffImage.save(self.diff) + result = False + for stat in ('stddev', 'mean', 'sum2'): + for item in getattr(ImageStat.Stat(self.diffImage), stat): + if item: + self.result = {self.label: "Failed - see %s" % + self.diff} + raise StopIteration + else: + result = True + except StopIteration: + result = False + + if result: + self.result = {self.label: "Passed"} + + TC.logger.log(self.result) + return self.result + + +class TCNumber(TC): # pragma: no cover + + """ + Number Comparaison Test Case Class + """ + + def __init__(self): + TC.__init__(self) + self.supportedtypes = ("int", "long", "float", "complex", "oct", "hex") + + # Compare 2 numbers by the type provided in the type arg + def compare(self, label, baseline, undertest, type): + """ + Compares 2 numbers to see if they are the same. The user may specify + how to normalize mixed type comparisons via the type argument. + """ + self.label = label.strip() + self.baseline = baseline + self.undertest = undertest + self.type = type.strip() + + # If we get a valid type, convert to that type and compare + if self.type in self.supportedtypes: + # Normalize for comparison + if self.type == "int": + self.baseline = int(self.baseline) + self.undertest = int(self.undertest) + elif self.type == "long": + self.baseline = long(self.baseline) + self.undertest = long(self.undertest) + elif self.type == "float": + self.baseline = float(self.baseline) + self.undertest = float(self.undertest) + else: + self.baseline = complex(self.baseline) + self.undertest = complex(self.undertest) + + # compare + if self.baseline == self.undertest: + self.result = {self.label: "Passed - numbers are the same"} + else: + self.result = {self.label: "Failed - " + str( + self.baseline) + " expected: Got " + str(self.undertest)} + TC.logger.log(self.result) + return self.result + else: + self.result = { + self.label: "Failed - " + self.type + " is not in list of supported types"} + TC.logger.log(self.result) + return self.result + + +class TCBool(TC): # pragma: no cover + + def __init__(self): + pass + + def compare(self, label, _bool): + """ + If _bool is True, pass. + If _bool is False, fail. + """ + if type(_bool) is not bool: + raise TypeError + if _bool: + result = {label: "Passed"} + else: + result = {label: "Failed"} + TC.logger.log(result) + +from tree import Node + + +class TCNode(TC): # pragma: no cover + + def __init__(self): + pass + + def compare(self, label, baseline, undertest): + """ + If baseline is None, simply check that undertest is a Node. + If baseline is a Node, check that it is equal to undertest. + """ + if baseline is not None and not isinstance(baseline, Node): + raise TypeError + + if not isinstance(undertest, Node): + result = {label: "Failed - %s is not a Node" % undertest} + elif baseline is None: + result = {label: "Passed - %s is a Node" % undertest} + elif isinstance(baseline, Node): + if baseline == undertest: + result = {label: "Passed - %s == %s" % (baseline, undertest)} + else: + result = {label: "Failed - %s != %s" % (baseline, undertest)} + TC.logger.log(result) diff --git a/build/lib/dogtail/tree.py b/build/lib/dogtail/tree.py new file mode 100644 index 00000000000..e6050bbb98b --- /dev/null +++ b/build/lib/dogtail/tree.py @@ -0,0 +1,1318 @@ +"""Makes some sense of the AT-SPI API + +The tree API handles various things for you: + - fixes most timing issues + - can automatically generate (hopefully) highly-readable logs of what the +script is doing + - traps various UI malfunctions, raising exceptions for them (again, +hopefully improving the logs) + +The most important class is Node. Each Node is an element of the desktop UI. +There is a tree of nodes, starting at 'root', with applications as its +children, with the top-level windows and dialogs as their children. The various +widgets that make up the UI appear as descendents in this tree. All of these +elements (root, the applications, the windows, and the widgets) are represented +as instances of Node in a tree (provided that the program of interest is +correctly exporting its user-interface to the accessibility system). The Node +class is a mixin for Accessible and the various Accessible interfaces. + +The Action class represents an action that the accessibility layer exports as +performable on a specific node, such as clicking on it. It's a wrapper around +Accessibility.Action. + +We often want to look for a node, based on some criteria, and this is provided +by the Predicate class. + +Dogtail implements a high-level searching system, for finding a node (or +nodes) satisfying whatever criteria you are interested in. It does this with +a 'backoff and retry' algorithm. This fixes most timing problems e.g. when a +dialog is in the process of opening but hasn't yet done so. + +If a search fails, it waits 'config.searchBackoffDuration' seconds, and then +tries again, repeatedly. After several failed attempts (determined by +config.searchWarningThreshold) it will start sending warnings about the search +to the debug log. If it still can't succeed after 'config.searchCutoffCount' +attempts, it raises an exception containing details of the search. You can see +all of this process in the debug log by setting 'config.debugSearching' to True + +We also automatically add a short delay after each action +('config.defaultDelay' gives the time in seconds). We'd hoped that the search +backoff and retry code would eliminate the need for this, but unfortunately we +still run into timing issues. For example, Evolution (and probably most +other apps) set things up on new dialogs and wizard pages as they appear, and +we can run into 'setting wars' where the app resets the widgetry to defaults +after our script has already filled out the desired values, and so we lose our +values. So we give the app time to set the widgetry up before the rest of the +script runs. + +The classes trap various UI malfunctions and raise exceptions that better +describe what went wrong. For example, they detects attempts to click on an +insensitive UI element and raise a specific exception for this. + +Unfortunately, some applications do not set up the 'sensitive' state +correctly on their buttons (e.g. Epiphany on form buttons in a web page). The +current workaround for this is to set config.ensureSensitivity=False, which +disables the sensitivity testing. + +Authors: Zack Cerza <zcerza@redhat.com>, David Malcolm <dmalcolm@redhat.com> +""" +__author__ = """Zack Cerza <zcerza@redhat.com>, +David Malcolm <dmalcolm@redhat.com> +""" + +from config import config +if config.checkForA11y: + from utils import checkForA11y + checkForA11y() + +import predicate +from time import sleep +from utils import doDelay +from utils import Blinker +from utils import Lock +import rawinput +import path +from __builtin__ import xrange + +from logging import debugLogger as logger + +try: + import pyatspi + import Accessibility +except ImportError: # pragma: no cover + raise ImportError("Error importing the AT-SPI bindings") + +# We optionally import the bindings for libWnck. +try: + from gi.repository import Wnck + gotWnck = True # pragma: no cover +except ImportError: + # Skip this warning, since the functionality is almost entirely nonworking anyway. + # print "Warning: Dogtail could not import the Python bindings for + # libwnck. Window-manager manipulation will not be available." + gotWnck = False + +from gi.repository import GLib + +haveWarnedAboutChildrenLimit = False + + +class SearchError(Exception): + pass + + +class NotSensitiveError(Exception): + + """ + The widget is not sensitive. + """ + message = "Cannot %s %s. It is not sensitive." + + def __init__(self, action): + self.action = action + + def __str__(self): + return self.message % (self.action.name, self.action.node.getLogString()) + + +class ActionNotSupported(Exception): + + """ + The widget does not support the requested action. + """ + message = "Cannot do '%s' action on %s" + + def __init__(self, actionName, node): + self.actionName = actionName + self.node = node + + def __str__(self): + return self.message % (self.actionName, self.node.getLogString()) + + +class Action(object): + + """ + Class representing an action that can be performed on a specific node + """ + # Valid types of actions we know about. Feel free to add any you see. + types = ('click', + 'press', + 'release', + 'activate', + 'jump', + 'check', + 'dock', + 'undock', + 'open', + 'menu') + + def __init__(self, node, action, index): + self.node = node + self.__action = action + self.__index = index + + @property + def name(self): + return self.__action.getName(self.__index) + + @property + def description(self): + return self.__action.getDescription(self.__index) + + @property + def keyBinding(self): + return self.__action.getKeyBinding(self.__index) + + def __str__(self): + return "[action | %s | %s ]" % \ + (self.name, self.keyBinding) + + def do(self): + """ + Performs the given tree.Action, with appropriate delays and logging. + """ + logger.log("%s on %s" % (self.name, self.node.getLogString())) + if not self.node.sensitive: + if config.ensureSensitivity: + raise NotSensitiveError(self) + else: + nSE = NotSensitiveError(self) + logger.log("Warning: " + str(nSE)) + if config.blinkOnActions: + self.node.blink() + result = self.__action.doAction(self.__index) + doDelay(config.actionDelay) + return result + + +class Node(object): + + """ + A node in the tree of UI elements. This class is mixed in with + Accessibility.Accessible to both make it easier to use and to add + additional functionality. It also has a debugName which is set up + automatically when doing searches. + """ + + def __setupUserData(self): + try: + len(self.user_data) + except (AttributeError, TypeError): + self.user_data = {} + + def debugName(): + doc = "debug name assigned during search operations" + + def fget(self): + self.__setupUserData() + return self.user_data.get('debugName', None) + + def fset(self, debugName): + self.__setupUserData() + self.user_data['debugName'] = debugName + + return property(**locals()) + debugName = debugName() + # + # Accessible + # + + @property + def dead(self): + """Is the node dead (defunct) ?""" + try: + if self.roleName == 'invalid': + return True + self.role + self.name + if len(self) > 0: + self[0] + except: + return True + return False + + @property + def children(self): + """a list of this Accessible's children""" + if self.parent and self.parent.roleName == 'hyper link': + print(self.parent.role) + return [] + children = [] + childCount = self.childCount + if childCount > config.childrenLimit: + global haveWarnedAboutChildrenLimit + if not haveWarnedAboutChildrenLimit: + logger.log("Only returning %s children. You may change " + "config.childrenLimit if you wish. This message will only" + " be printed once." % str(config.childrenLimit)) + haveWarnedAboutChildrenLimit = True + childCount = config.childrenLimit + for i in range(childCount): + # Workaround for GNOME bug #465103 + # also solution for GNOME bug #321273 + try: + child = self[i] + except LookupError: + child = None + if child: + children.append(child) + + invalidChildren = childCount - len(children) + if invalidChildren and config.debugSearching: + logger.log("Skipped %s invalid children of %s" % + (invalidChildren, str(self))) + try: + ht = self.queryHypertext() + for li in range(ht.getNLinks()): + link = ht.getLink(li) + for ai in range(link.nAnchors): + child = link.getObject(ai) + child.__setupUserData() + child.user_data['linkAnchor'] = \ + LinkAnchor(node=child, + hypertext=ht, + linkIndex=li, + anchorIndex=ai) + children.append(child) + except (NotImplementedError, AttributeError): + pass + + return children + + roleName = property(Accessibility.Accessible.getRoleName) + + role = property(Accessibility.Accessible.getRole) + + indexInParent = property(Accessibility.Accessible.getIndexInParent) + + # + # Action + # + + # Needed to be renamed from doAction due to conflicts + # with 'Accessibility.Accessible.doAction' in gtk3 branch + def doActionNamed(self, name): + """ + Perform the action with the specified name. For a list of actions + supported by this instance, check the 'actions' property. + """ + actions = self.actions + if name in actions: + return actions[name].do() + raise ActionNotSupported(name, self) + + @property + def actions(self): + """ + A dictionary of supported action names as keys, with Action objects as + values. Common action names include: + + 'click' 'press' 'release' 'activate' 'jump' 'check' 'dock' 'undock' + 'open' 'menu' + """ + actions = {} + try: + action = self.queryAction() + for i in range(action.nActions): + a = Action(self, action, i) + actions[action.getName(i)] = a + finally: + return actions + + def combovalue(): + doc = "The value (as a string) currently selected in the combo box." + + def fget(self): + return self.name + + def fset(self, value): + logger.log("Setting combobox %s to '%s'" % (self.getLogString(), + value)) + self.childNamed(childName=value).doActionNamed('click') + doDelay() + + return property(**locals()) + combovalue = combovalue() + # + # Hypertext and Hyperlink + # + + @property + def URI(self): + try: + return self.user_data['linkAnchor'].URI + except (KeyError, AttributeError): + raise NotImplementedError + + # + # Text and EditableText + # + def text(): + doc = """For instances with an AccessibleText interface, the text as a + string. This is read-only, unless the instance also has an + AccessibleEditableText interface. In this case, you can write values + to the attribute. This will get logged in the debug log, and a delay + will be added. + + If this instance corresponds to a password entry, use the passwordText + property instead.""" + + def fget(self): + try: + return self.queryText().getText(0, -1) + except NotImplementedError: + return None + + def fset(self, text): + try: + if config.debugSearching: + msg = "Setting text of %s to %s" + # Let's not get too crazy if 'text' is really large... + # FIXME: Sometimes the next line screws up Unicode strings. + if len(text) > 140: + txt = text[:134] + " [...]" + else: + txt = text + logger.log(msg % (self.getLogString(), "'%s'" % txt)) + self.queryEditableText().setTextContents(text) + except NotImplementedError: + raise AttributeError("can't set attribute") + + return property(**locals()) + text = text() + + def caretOffset(): + + def fget(self): + """For instances with an AccessibleText interface, the caret + offset as an integer.""" + return self.queryText().caretOffset + + def fset(self, offset): + return self.queryText().setCaretOffset(offset) + + return property(**locals()) + caretOffset = caretOffset() + + # + # Component + # + + @property + def position(self): + """A tuple containing the position of the Accessible: (x, y)""" + return self.queryComponent().getPosition(pyatspi.DESKTOP_COORDS) + + @property + def size(self): + """A tuple containing the size of the Accessible: (w, h)""" + return self.queryComponent().getSize() + + @property + def extents(self): + """A tuple containing the location and size of the Accessible: + (x, y, w, h)""" + try: + ex = self.queryComponent().getExtents(pyatspi.DESKTOP_COORDS) + return (ex.x, ex.y, ex.width, ex.height) + except NotImplementedError: + return None + + def contains(self, x, y): + try: + return self.queryComponent().contains(x, y, pyatspi.DESKTOP_COORDS) + except NotImplementedError: + return False + + def getChildAtPoint(self, x, y): + node = self + while True: + try: + child = node.queryComponent().getAccessibleAtPoint(x, y, + pyatspi.DESKTOP_COORDS) + if child and child.contains(x, y): + node = child + else: + break + except NotImplementedError: + break + if node and node.contains(x, y): + return node + else: + return None + + def grabFocus(self): + "Attempts to set the keyboard focus to this Accessible." + return self.queryComponent().grabFocus() + + # def blink(self, count=2): + #""" + # Blink, baby! + #""" + # if not self.extents: return False + # else: + #(x, y, w, h) = self.extents + #from utils import Blinker + #blinkData = Blinker(x, y, w, h, count) + # return True + + def click(self, button=1): + """ + Generates a raw mouse click event, using the specified button. + - 1 is left, + - 2 is middle, + - 3 is right. + """ + logger.log("Clicking on %s" % self.getLogString()) + clickX = self.position[0] + self.size[0] / 2 + clickY = self.position[1] + self.size[1] / 2 + if config.debugSearching: + logger.log("raw click on %s %s at (%s,%s)" % + (self.name, self.getLogString(), str(clickX), str(clickY))) + rawinput.click(clickX, clickY, button) + + def doubleClick(self, button=1): + """ + Generates a raw mouse double-click event, using the specified button. + """ + clickX = self.position[0] + self.size[0] / 2 + clickY = self.position[1] + self.size[1] / 2 + if config.debugSearching: + logger.log("raw click on %s %s at (%s,%s)" % + (self.name, self.getLogString(), str(clickX), str(clickY))) + rawinput.doubleClick(clickX, clickY, button) + + def point(self, mouseDelay=None): + """ + Move mouse cursor to the center of the widget. + """ + pointX = self.position[0] + self.size[0] / 2 + pointY = self.position[1] + self.size[1] / 2 + logger.log("Pointing on %s %s at (%s,%s)" % + (self.name, self.getLogString(), str(pointX), str(pointY))) + rawinput.registry.generateMouseEvent(pointX, pointY, 'abs') + if mouseDelay: + doDelay(mouseDelay) + else: + doDelay() + + # + # RelationSet + # + @property + def labeler(self): + """'labeller' (read-only list of Node instances): + The node(s) that is/are a label for this node. Generated from + 'relations'. + """ + relationSet = self.getRelationSet() + for relation in relationSet: + if relation.getRelationType() == pyatspi.RELATION_LABELLED_BY: + if relation.getNTargets() == 1: + return relation.getTarget(0) + targets = [] + for i in range(relation.getNTargets()): + targets.append(relation.getTarget(i)) + return targets + labeller = labeler + + @property + def labelee(self): + """'labellee' (read-only list of Node instances): + The node(s) that this node is a label for. Generated from 'relations'. + """ + relationSet = self.getRelationSet() + for relation in relationSet: + if relation.getRelationType() == pyatspi.RELATION_LABEL_FOR: + if relation.getNTargets() == 1: + return relation.getTarget(0) + targets = [] + for i in range(relation.getNTargets()): + targets.append(relation.getTarget(i)) + return targets + labellee = labelee + + # + # StateSet + # + @property + def sensitive(self): + """Is the Accessible sensitive (i.e. not greyed out)?""" + return self.getState().contains(pyatspi.STATE_SENSITIVE) + + @property + def showing(self): + return self.getState().contains(pyatspi.STATE_SHOWING) + + @property + def focusable(self): + """Is the Accessible capable of having keyboard focus?""" + return self.getState().contains(pyatspi.STATE_FOCUSABLE) + + @property + def focused(self): + """Does the Accessible have keyboard focus?""" + return self.getState().contains(pyatspi.STATE_FOCUSED) + + @property + def checked(self): + """Is the Accessible a checked checkbox?""" + return self.getState().contains(pyatspi.STATE_CHECKED) + + @property + def isChecked(self): + """Is the Accessible a checked checkbox? Compatibility property, same as Node.checked.""" + return self.checked + + # + # Selection + # + + def selectAll(self): + """Selects all children.""" + result = self.querySelection().selectAll() + doDelay() + return result + + def deselectAll(self): + """Deselects all selected children.""" + result = self.querySelection().clearSelection() + doDelay() + return result + + def select(self): + """Selects the Accessible.""" + try: + parent = self.parent + except AttributeError: + raise NotImplementedError + result = parent.querySelection().selectChild(self.indexInParent) + doDelay() + return result + + def deselect(self): + """Deselects the Accessible.""" + try: + parent = self.parent + except AttributeError: + raise NotImplementedError + result = parent.querySelection().deselectChild(self.indexInParent) + doDelay() + return result + + @property + def isSelected(self): + """Is the Accessible selected? Compatibility property, same as Node.selected.""" + try: + parent = self.parent + except AttributeError: + raise NotImplementedError + return parent.querySelection().isChildSelected(self.indexInParent) + + @property + def selected(self): + """Is the Accessible selected?""" + return self.isSelected + + @property + def selectedChildren(self): + """Returns a list of children that are selected.""" + # TODO: hideChildren for Hyperlinks? + selection = self.querySelection() + selectedChildren = [] + for i in xrange(selection.nSelectedChildren): + selectedChildren.append(selection.getSelectedChild(i)) + + # + # Value + # + + def value(): + doc = "The value contained by the AccessibleValue interface." + + def fget(self): + try: + return self.queryValue().currentValue + except NotImplementedError: + pass + + def fset(self, value): + self.queryValue().currentValue = value + + return property(**locals()) + value = value() + + @property + def minValue(self): + """The minimum value of self.value""" + try: + return self.queryValue().minimumValue + except NotImplementedError: + pass + + @property + def minValueIncrement(self): + """The minimum value increment of self.value""" + try: + return self.queryValue().minimumIncrement + except NotImplementedError: + pass + + @property + def maxValue(self): + """The maximum value of self.value""" + try: + return self.queryValue().maximumValue + except NotImplementedError: + pass + + def typeText(self, string): + """ + Type the given text into the node, with appropriate delays and + logging. + """ + logger.log("Typing text into %s: '%s'" % (self.getLogString(), string)) + + if self.focusable: + if not self.focused: + try: + self.grabFocus() + except Exception: + logger.log("Node is focusable but I can't grabFocus!") + rawinput.typeText(string) + else: + logger.log("Node is not focusable; falling back to inserting text") + et = self.queryEditableText() + et.insertText(self.caretOffset, string, len(string)) + self.caretOffset += len(string) + doDelay() + + def keyCombo(self, comboString): + if config.debugSearching: + logger.log("Pressing keys '%s' into %s" % + (comboString, self.getLogString())) + if self.focusable: + if not self.focused: + try: + self.grabFocus() + except Exception: + logger.log("Node is focusable but I can't grabFocus!") + else: + logger.log("Node is not focusable; trying key combo anyway") + rawinput.keyCombo(comboString) + + def getLogString(self): + """ + Get a string describing this node for the logs, + respecting the config.absoluteNodePaths boolean. + """ + if config.absoluteNodePaths: + return self.getAbsoluteSearchPath() + else: + return str(self) + + def satisfies(self, pred): + """ + Does this node satisfy the given predicate? + """ + # the logic is handled by the predicate: + assert isinstance(pred, predicate.Predicate) + return pred.satisfiedByNode(self) + + def dump(self, type='plain', fileName=None): + import dump + dumper = getattr(dump, type) + dumper(self, fileName) + + def getAbsoluteSearchPath(self): + """ + FIXME: this needs rewriting... + Generate a SearchPath instance giving the 'best' + way to find the Accessible wrapped by this node again, starting + at the root and applying each search in turn. + + This is somewhat analagous to an absolute path in a filesystem, + except that some of searches may be recursive, rather than just + searching direct children. + + Used by the recording framework for identifying nodes in a + persistent way, independent of the style of script being + written. + + FIXME: try to ensure uniqueness + FIXME: need some heuristics to get 'good' searches, whatever + that means + """ + if config.debugSearchPaths: + logger.log("getAbsoluteSearchPath(%s)" % self) + + if self.roleName == 'application': + result = path.SearchPath() + result.append(predicate.IsAnApplicationNamed(self.name), False) + return result + else: + if self.parent: + (ancestor, pred, isRecursive) = self.getRelativeSearch() + if config.debugSearchPaths: + logger.log("got ancestor: %s" % ancestor) + + ancestorPath = ancestor.getAbsoluteSearchPath() + ancestorPath.append(pred, isRecursive) + return ancestorPath + else: + # This should be the root node: + return path.SearchPath() + + def getRelativeSearch(self): + """ + Get a (ancestorNode, predicate, isRecursive) triple that identifies the + best way to find this Node uniquely. + FIXME: or None if no such search exists? + FIXME: may need to make this more robust + FIXME: should this be private? + """ + if config.debugSearchPaths: + logger.log("getRelativeSearchPath(%s)" % self) + + assert self + assert self.parent + + isRecursive = False + ancestor = self.parent + + # iterate up ancestors until you reach an identifiable one, + # setting the search to be isRecursive if need be: + while not self.__nodeIsIdentifiable(ancestor): + ancestor = ancestor.parent + isRecursive = True + + # Pick the most appropriate predicate for finding this node: + if self.labellee: + if self.labellee.name: + return (ancestor, predicate.IsLabelledAs(self.labellee.name), isRecursive) + + if self.roleName == 'menu': + return (ancestor, predicate.IsAMenuNamed(self.name), isRecursive) + elif self.roleName == 'menu item' or self.roleName == 'check menu item': + return (ancestor, predicate.IsAMenuItemNamed(self.name), isRecursive) + elif self.roleName == 'text': + return (ancestor, predicate.IsATextEntryNamed(self.name), isRecursive) + elif self.roleName == 'push button': + return (ancestor, predicate.IsAButtonNamed(self.name), isRecursive) + elif self.roleName == 'frame': + return (ancestor, predicate.IsAWindowNamed(self.name), isRecursive) + elif self.roleName == 'dialog': + return (ancestor, predicate.IsADialogNamed(self.name), isRecursive) + else: + pred = predicate.GenericPredicate( + name=self.name, roleName=self.roleName) + return (ancestor, pred, isRecursive) + + def __nodeIsIdentifiable(self, ancestor): + if ancestor.labellee: + return True + elif ancestor.name: + return True + elif not ancestor.parent: + return True + else: + return False + + def _fastFindChild(self, pred, recursive=True): + """ + Searches for an Accessible using methods from pyatspi.utils + """ + if isinstance(pred, predicate.Predicate): + pred = pred.satisfiedByNode + if not recursive: + cIter = iter(self) + while True: + try: + child = cIter.next() + except StopIteration: + break + if child is not None: + if pred(child): + return child + else: + return pyatspi.utils.findDescendant(self, pred) + + def findChild(self, pred, recursive=True, debugName=None, + retry=True, requireResult=True): + """ + Search for a node satisyfing the predicate, returning a Node. + + If retry is True (the default), it makes multiple attempts, + backing off and retrying on failure, and eventually raises a + descriptive exception if the search fails. + + If retry is False, it gives up after one attempt. + + If requireResult is True (the default), an exception is raised after all + attempts have failed. If it is false, the function simply returns None. + """ + def describeSearch(parent, pred, recursive, debugName): + """ + Internal helper function + """ + if recursive: + noun = "descendent" + else: + noun = "child" + if debugName is None: + debugName = pred.describeSearchResult() + return "%s of %s: %s" % (noun, parent.getLogString(), debugName) + + assert isinstance(pred, predicate.Predicate) + numAttempts = 0 + while numAttempts < config.searchCutoffCount: + if numAttempts >= config.searchWarningThreshold or config.debugSearching: + logger.log("searching for %s (attempt %i)" % + (describeSearch(self, pred, recursive, debugName), numAttempts)) + + result = self._fastFindChild(pred.satisfiedByNode, recursive) + if result: + assert isinstance(result, Node) + if debugName: + result.debugName = debugName + else: + result.debugName = pred.describeSearchResult() + return result + else: + if not retry: + break + numAttempts += 1 + if config.debugSearching or config.debugSleep: + logger.log("sleeping for %f" % + config.searchBackoffDuration) + sleep(config.searchBackoffDuration) + if requireResult: + raise SearchError(describeSearch(self, pred, recursive, debugName)) + + # The canonical "search for multiple" method: + def findChildren(self, pred, recursive=True, isLambda=False): + """ + Find all children/descendents satisfying the predicate. + """ + if isLambda is True: + nodes = self.findChildren(predicate.GenericPredicate(), recursive=recursive) + result = [] + for node in nodes: + try: + if pred(node): + result.append(node) + except: + pass + return result + if isinstance(pred, predicate.Predicate): + pred = pred.satisfiedByNode + if not recursive: + cIter = iter(self) + result = [] + while True: + try: + child = cIter.next() + except StopIteration: + break + if child is not None and pred(child): + result.append(child) + return result + else: + descendants = [] + while True: + try: + descendants = pyatspi.utils.findAllDescendants(self, pred) + break + except (GLib.GError, TypeError): + continue + return descendants + + # The canonical "search above this node" method: + def findAncestor(self, pred): + """ + Search up the ancestry of this node, returning the first Node + satisfying the predicate, or None. + """ + assert isinstance(pred, predicate.Predicate) + candidate = self.parent + while candidate is not None: + if candidate.satisfies(pred): + return candidate + else: + candidate = candidate.parent + # Not found: + return None + + # Various wrapper/helper search methods: + def child(self, name='', roleName='', description='', label='', recursive=True, retry=True, debugName=None): + """ + Finds a child satisying the given criteria. + + This is implemented using findChild, and hence will automatically retry + if no such child is found, and will eventually raise an exception. It + also logs the search. + """ + return self.findChild(predicate.GenericPredicate(name=name, roleName=roleName, description=description, label=label), recursive=recursive, retry=retry, debugName=debugName) + + def isChild(self, name='', roleName='', description='', label='', recursive=True, retry=False, debugName=None): + """ + Determines whether a child satisying the given criteria exists. + + This is implemented using findChild, but will not automatically retry + if no such child is found. To make the function retry multiple times set retry to True. + Returns a boolean value depending on whether the child was eventually found. Similar to + 'child', yet it catches SearchError exception to provide for False results, will raise + any other exceptions. It also logs the search. + """ + found = True + try: + self.findChild( + predicate.GenericPredicate( + name=name, roleName=roleName, description=description, label=label), + recursive=recursive, retry=retry, debugName=debugName) + except SearchError: + found = False + return found + + def menu(self, menuName, recursive=True): + """ + Search below this node for a menu with the given name. + + This is implemented using findChild, and hence will automatically retry + if no such child is found, and will eventually raise an exception. It + also logs the search. + """ + return self.findChild(predicate.IsAMenuNamed(menuName=menuName), recursive) + + def menuItem(self, menuItemName, recursive=True): + """ + Search below this node for a menu item with the given name. + + This is implemented using findChild, and hence will automatically retry + if no such child is found, and will eventually raise an exception. It + also logs the search. + """ + return self.findChild(predicate.IsAMenuItemNamed(menuItemName=menuItemName), recursive) + + def textentry(self, textEntryName, recursive=True): + """ + Search below this node for a text entry with the given name. + + This is implemented using findChild, and hence will automatically retry + if no such child is found, and will eventually raise an exception. It + also logs the search. + """ + return self.findChild(predicate.IsATextEntryNamed(textEntryName=textEntryName), recursive) + + def button(self, buttonName, recursive=True): + """ + Search below this node for a button with the given name. + + This is implemented using findChild, and hence will automatically retry + if no such child is found, and will eventually raise an exception. It + also logs the search. + """ + return self.findChild(predicate.IsAButtonNamed(buttonName=buttonName), recursive) + + def childLabelled(self, labelText, recursive=True): + """ + Search below this node for a child labelled with the given text. + + This is implemented using findChild, and hence will automatically retry + if no such child is found, and will eventually raise an exception. It + also logs the search. + """ + return self.findChild(predicate.IsLabelledAs(labelText), recursive) + + def childNamed(self, childName, recursive=True): + """ + Search below this node for a child with the given name. + + This is implemented using findChild, and hence will automatically retry + if no such child is found, and will eventually raise an exception. It + also logs the search. + """ + return self.findChild(predicate.IsNamed(childName), recursive) + + def tab(self, tabName, recursive=True): + """ + Search below this node for a tab with the given name. + + This is implemented using findChild, and hence will automatically retry + if no such child is found, and will eventually raise an exception. It + also logs the search. + """ + return self.findChild(predicate.IsATabNamed(tabName=tabName), recursive) + + def getUserVisibleStrings(self): + """ + Get all user-visible strings in this node and its descendents. + + (Could be implemented as an attribute) + """ + result = [] + if self.name: + result.append(self.name) + if self.description: + result.append(self.description) + try: + children = self.children + except Exception: + return result + for child in children: + result.extend(child.getUserVisibleStrings()) + return result + + def blink(self): + """ + Blink, baby! + """ + if not self.extents: + return False + else: + (x, y, w, h) = self.extents + Blinker(x, y, w, h) + return True + + +class LinkAnchor(object): + + """ + Class storing info about an anchor within an Accessibility.Hyperlink, which + is in turn stored within an Accessibility.Hypertext. + """ + + def __init__(self, node, hypertext, linkIndex, anchorIndex): + self.node = node + self.hypertext = hypertext + self.linkIndex = linkIndex + self.anchorIndex = anchorIndex + + @property + def link(self): + return self.hypertext.getLink(self.linkIndex) + + @property + def URI(self): + return self.link.getURI(self.anchorIndex) + + +class Root (Node): + + """ + FIXME: + """ + + def applications(self): + """ + Get all applications. + """ + return root.findChildren(predicate.GenericPredicate( + roleName="application"), recursive=False) + + def application(self, appName, retry=True): + """ + Gets an application by name, returning an Application instance + or raising an exception. + + This is implemented using findChild, and hence will automatically retry + if no such child is found, and will eventually raise an exception. It + also logs the search. + """ + return root.findChild(predicate.IsAnApplicationNamed(appName), recursive=False, retry=retry) + + +class Application (Node): + + def dialog(self, dialogName, recursive=False): + """ + Search below this node for a dialog with the given name, + returning a Window instance. + + This is implemented using findChild, and hence will automatically retry + if no such child is found, and will eventually raise an exception. It + also logs the search. + + FIXME: should this method activate the dialog? + """ + return self.findChild(predicate.IsADialogNamed(dialogName=dialogName), recursive) + + def window(self, windowName, recursive=False): + """ + Search below this node for a window with the given name, + returning a Window instance. + + This is implemented using findChild, and hence will automatically retry + if no such child is found, and will eventually raise an exception. It + also logs the search. + + FIXME: this bit isn't true: + The window will be automatically activated (raised and focused + by the window manager) if wnck bindings are available. + """ + result = self.findChild( + predicate.IsAWindowNamed(windowName=windowName), recursive) + # FIXME: activate the WnckWindow ? + # if gotWnck: + # result.activate() + return result + + def getWnckApplication(self): # pragma: no cover + """ + Get the wnck.Application instance for this application, or None + + Currently implemented via a hack: requires the app to have a + window, and looks up the application of that window + + wnck.Application can give you the pid, the icon, etc + + FIXME: untested + """ + window = self.child(roleName='frame') + if window: + wnckWindow = window.getWnckWindow() + return wnckWindow.get_application() + + +class Window (Node): + + def getWnckWindow(self): # pragma: no cover + """ + Get the wnck.Window instance for this window, or None + """ + # FIXME: this probably needs rewriting: + screen = Wnck.screen_get_default() + + # You have to force an update before any of the wnck methods + # do anything: + screen.force_update() + + for wnckWindow in screen.get_windows(): + # FIXME: a dubious hack: search by window title: + if wnckWindow.get_name() == self.name: + return wnckWindow + + def activate(self): # pragma: no cover + """ + Activates the wnck.Window associated with this Window. + + FIXME: doesn't yet work + """ + wnckWindow = self.getWnckWindow() + # Activate it with a timestamp of 0; this may confuse + # alt-tabbing through windows etc: + # FIXME: is there a better way of getting a timestamp? + # gdk_x11_get_server_time (), with a dummy window + wnckWindow.activate(0) + + +class Wizard (Window): + + """ + Note that the buttons of a GnomeDruid were not accessible until + recent versions of libgnomeui. This is + http://bugzilla.gnome.org/show_bug.cgi?id=157936 + and is fixed in gnome-2.10 and gnome-2.12 (in CVS libgnomeui); + there's a patch attached to that bug. + + This bug is known to affect FC3; fixed in FC5 + """ + + def __init__(self, node, debugName=None): + Node.__init__(self, node) + if debugName: + self.debugName = debugName + logger.log("%s is on '%s' page" % (self, self.getPageTitle())) + + def currentPage(self): + """ + Get the current page of this wizard + + FIXME: this is currently a hack, supporting only GnomeDruid + """ + pageHolder = self.child(roleName='panel') + for child in pageHolder.children: + # current child has SHOWING state set, we hope: + # print child + # print child.showing + if child.showing: + return child + raise "Unable to determine current page of %s" % self + + def getPageTitle(self): + """ + Get the string title of the current page of this wizard + + FIXME: this is currently a total hack, supporting only GnomeDruid + """ + currentPage = self.currentPage() + return currentPage.child(roleName='panel').child(roleName='panel').child(roleName='label', recursive=False).text + + def clickForward(self): + """ + Click on the 'Forward' button to advance to next page of wizard. + + It will log the title of the new page that is reached. + + FIXME: what if it's Next rather than Forward ??? + + This will only work if your libgnomeui has accessible buttons; + see above. + """ + fwd = self.child("Forward") + fwd.click() + + # Log the new wizard page; it's helpful when debugging scripts + logger.log("%s is now on '%s' page" % (self, self.getPageTitle())) + # FIXME disabled for now (can't get valid page titles) + + def clickApply(self): + """ + Click on the 'Apply' button to advance to next page of wizard. + FIXME: what if it's Finish rather than Apply ??? + + This will only work if your libgnomeui has accessible buttons; + see above. + """ + fwd = self.child("Apply") + fwd.click() + + # FIXME: debug logging? + +Accessibility.Accessible.__bases__ = ( + Application, Root, Node,) + Accessibility.Accessible.__bases__ + +try: + root = pyatspi.Registry.getDesktop(0) + root.debugName = 'root' +except Exception: # pragma: no cover + # Warn if AT-SPI's desktop object doesn't show up. + logger.log( + "Error: AT-SPI's desktop is not visible. Do you have accessibility enabled?") + +# Check that there are applications running. Warn if none are. +children = root.children +if not children: # pragma: no cover + logger.log( + "Warning: AT-SPI's desktop is visible but it has no children. Are you running any AT-SPI-aware applications?") +del children + +import os +# sniff also imports from tree and we don't want to run this code from +# sniff itself +if not os.path.exists('/tmp/sniff_running.lock'): + if not os.path.exists('/tmp/sniff_refresh.lock'): # may have already been locked by dogtail.procedural + # tell sniff not to use auto-refresh while script using this module is + # running + sniff_lock = Lock(lockname='sniff_refresh.lock', randomize=False) + try: + sniff_lock.lock() + except OSError: # pragma: no cover + pass # lock was already present from other script instance or leftover from killed instance + # lock should unlock automatically on script exit. + +# Convenient place to set some debug variables: +#config.debugSearching = True +#config.absoluteNodePaths = True +#config.logDebugToFile = False diff --git a/build/lib/dogtail/utils.py b/build/lib/dogtail/utils.py new file mode 100644 index 00000000000..be3c78adec2 --- /dev/null +++ b/build/lib/dogtail/utils.py @@ -0,0 +1,395 @@ +# -*- coding: utf-8 -*- +""" +Various utilities + +Authors: Ed Rousseau <rousseau@redhat.com>, Zack Cerza <zcerza@redhat.com, David Malcolm <dmalcolm@redhat.com> +""" + +__author__ = """Ed Rousseau <rousseau@redhat.com>, +Zack Cerza <zcerza@redhat.com, +David Malcolm <dmalcolm@redhat.com> +""" + +import os +import sys +import subprocess +import cairo +import predicate +import errno +import shlex + +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') + +from gi.repository import Gtk +from gi.repository import GLib +from config import config +from time import sleep +from logging import debugLogger as logger +from logging import TimeStamp +from __builtin__ import file + + +def screenshot(file='screenshot.png', timeStamp=True): + """ + This function wraps the ImageMagick import command to take a screenshot. + + The file argument may be specified as 'foo', 'foo.png', or using any other + extension that ImageMagick supports. PNG is the default. + + By default, screenshot filenames are in the format of foo_YYYYMMDD-hhmmss.png . + The timeStamp argument may be set to False to name the file foo.png. + """ + if not isinstance(timeStamp, bool): + raise TypeError("timeStampt must be True or False") + # config is supposed to create this for us. If it's not there, bail. + assert os.path.isdir(config.scratchDir) + + baseName = ''.join(file.split('.')[0:-1]) + fileExt = file.split('.')[-1].lower() + if not baseName: + baseName = file + fileExt = 'png' + + if timeStamp: + ts = TimeStamp() + newFile = ts.fileStamp(baseName) + '.' + fileExt + path = config.scratchDir + newFile + else: + newFile = baseName + '.' + fileExt + path = config.scratchDir + newFile + + from gi.repository import Gdk + from gi.repository import GLib + from gi.repository import GdkPixbuf + rootWindow = Gdk.get_default_root_window() + geometry = rootWindow.get_geometry() + pixbuf = GdkPixbuf.Pixbuf(colorspace=GdkPixbuf.Colorspace.RGB, + has_alpha=False, + bits_per_sample=8, + width=geometry[2], + height=geometry[3]) + + pixbuf = Gdk.pixbuf_get_from_window(rootWindow, 0, 0, + geometry[2], geometry[3]) + # GdkPixbuf.Pixbuf.save() needs 'jpeg' and not 'jpg' + if fileExt == 'jpg': + fileExt = 'jpeg' + try: + pixbuf.savev(path, fileExt, [], []) + except GLib.GError: + raise ValueError("Failed to save screenshot in %s format" % fileExt) + assert os.path.exists(path) + logger.log("Screenshot taken: " + path) + return path + + +def run(string, timeout=config.runTimeout, interval=config.runInterval, desktop=None, dumb=False, appName=''): + """ + Runs an application. [For simple command execution such as 'rm *', use os.popen() or os.system()] + If dumb is omitted or is False, polls at interval seconds until the application is finished starting, or until timeout is reached. + If dumb is True, returns when timeout is reached. + """ + if not desktop: + from tree import root as desktop + args = shlex.split(string) + os.environ['GTK_MODULES'] = 'gail:atk-bridge' + pid = subprocess.Popen(args, env=os.environ).pid + + if not appName: + appName = args[0] + + if dumb: + # We're starting a non-AT-SPI-aware application. Disable startup + # detection. + doDelay(timeout) + else: + # Startup detection code + # The timing here is not totally precise, but it's good enough for now. + time = 0 + while time < timeout: + time = time + interval + try: + for child in desktop.children[::-1]: + if child.name == appName: + for grandchild in child.children: + if grandchild.roleName == 'frame': + from procedural import focus + focus.application.node = child + doDelay(interval) + return pid + except AttributeError: # pragma: no cover + pass + doDelay(interval) + return pid + + +def doDelay(delay=None): + """ + Utility function to insert a delay (with logging and a configurable + default delay) + """ + if delay is None: + delay = config.defaultDelay + if config.debugSleep: + logger.log("sleeping for %f" % delay) + sleep(delay) + + +class Highlight (Gtk.Window): # pragma: no cover + + def __init__(self, x, y, w, h): # pragma: no cover + super(Highlight, self).__init__() + self.set_decorated(False) + self.set_has_resize_grip(False) + self.set_default_size(w, h) + self.screen = self.get_screen() + self.visual = self.screen.get_rgba_visual() + if self.visual is not None and self.screen.is_composited(): + self.set_visual(self.visual) + self.set_app_paintable(True) + self.connect("draw", self.area_draw) + self.show_all() + self.move(x, y) + + def area_draw(self, widget, cr): # pragma: no cover + cr.set_source_rgba(.0, .0, .0, 0.0) + cr.set_operator(cairo.OPERATOR_SOURCE) + cr.paint() + cr.set_operator(cairo.OPERATOR_OVER) + cr.set_source_rgb(0.9, 0.1, 0.1) + cr.set_line_width(6) + cr.rectangle(0, 0, self.get_size()[0], self.get_size()[1]) + cr.stroke() + + +class Blinker(object): # pragma: no cover + INTERVAL_MS = 1000 + main_loop = GLib.MainLoop() + + def __init__(self, x, y, w, h): # pragma: no cover + self.highlight_window = Highlight(x, y, w, h) + if self.highlight_window.screen.is_composited() is not False: + self.timeout_handler_id = GLib.timeout_add( + Blinker.INTERVAL_MS, self.destroyHighlight) + self.main_loop.run() + else: + self.highlight_window.destroy() + + def destroyHighlight(self): # pragma: no cover + self.highlight_window.destroy() + self.main_loop.quit() + return False + + +class Lock(object): + + """ + A mutex implementation that uses atomicity of the mkdir operation in UNIX-like + systems. This can be used by scripts to provide for mutual exlusion, either in single + scripts using threads etc. or i.e. to handle sitations of possible collisions among + multiple running scripts. You can choose to make randomized single-script wise locks + or a more general locks if you do not choose to randomize the lockdir name + """ + + def __init__(self, location='/tmp', lockname='dogtail_lockdir_', randomize=True): + """ + You can change the default lockdir location or name. Setting randomize to + False will result in no random string being appened to the lockdir name. + """ + self.lockdir = os.path.join(os.path.normpath(location), lockname) + if randomize: + self.lockdir = "%s%s" % (self.lockdir, self.__getPostfix()) + + def lock(self): + """ + Creates a lockdir based on the settings on Lock() instance creation. + Raises OSError exception of the lock is already present. Should be + atomic on POSIX compliant systems. + """ + locked_msg = 'Dogtail lock: Already locked with the same lock' + if not os.path.exists(self.lockdir): + try: + os.mkdir(self.lockdir) + return self.lockdir + except OSError as e: + if e.errno == errno.EEXIST and os.path.isdir(self.lockdir): + raise OSError(locked_msg) + else: + raise OSError(locked_msg) + + def unlock(self): + """ + Removes a lock. Will raise OSError exception if the lock was not present. + Should be atomic on POSIX compliant systems. + """ + import os # have to import here for situations when executed from __del__ + if os.path.exists(self.lockdir): + try: + os.rmdir(self.lockdir) + except OSError as e: + if e.erron == errno.EEXIST: + raise OSError('Dogtail unlock: lockdir removed elsewhere!') + else: + raise OSError('Dogtail unlock: not locked') + + def __del__(self): + """ + Makes sure lock is removed when the process ends. Although not when killed indeed. + """ + self.unlock() + + def __getPostfix(self): + import random + import string + return ''.join(random.choice(string.letters + string.digits) for x in range(5)) + + +a11yDConfKey = 'org.gnome.desktop.interface' + + +def isA11yEnabled(): + """ + Checks if accessibility is enabled via DConf. + """ + from gi.repository.Gio import Settings + InterfaceSettings = Settings(a11yDConfKey) + dconfEnabled = InterfaceSettings.get_boolean('toolkit-accessibility') + if os.environ.get('GTK_MODULES', '').find('gail:atk-bridge') == -1: + envEnabled = False + else: + envEnabled = True # pragma: no cover + return (dconfEnabled or envEnabled) + + +def bailBecauseA11yIsDisabled(): + if sys.argv[0].endswith("pydoc"): + return # pragma: no cover + try: + if file("/proc/%s/cmdline" % os.getpid()).read().find('epydoc') != -1: + return # pragma: no cover + except: # pragma: no cover + pass # pragma: no cover + logger.log("Dogtail requires that Assistive Technology support be enabled." + "\nYou can enable accessibility with sniff or by running:\n" + "'gsettings set org.gnome.desktop.interface toolkit-accessibility true'\nAborting...") + sys.exit(1) + + +def enableA11y(enable=True): + """ + Enables accessibility via DConf. + """ + from gi.repository.Gio import Settings + InterfaceSettings = Settings(schema=a11yDConfKey) + InterfaceSettings.set_boolean('toolkit-accessibility', enable) + + +def checkForA11y(): + """ + Checks if accessibility is enabled, and halts execution if it is not. + """ + if not isA11yEnabled(): # pragma: no cover + bailBecauseA11yIsDisabled() + + +def checkForA11yInteractively(): # pragma: no cover + """ + Checks if accessibility is enabled, and presents a dialog prompting the + user if it should be enabled if it is not already, then halts execution. + """ + if isA11yEnabled(): + return + from gi.repository import Gtk + dialog = Gtk.Dialog('Enable Assistive Technology Support?', + None, + Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, + (Gtk.STOCK_QUIT, Gtk.ResponseType.CLOSE, + "_Enable", Gtk.ResponseType.ACCEPT)) + question = """Dogtail requires that Assistive Technology Support be enabled for it to function. Would you like to enable Assistive Technology support now? + +Note that you will have to log out for the change to fully take effect. + """.strip() + dialog.set_default_response(Gtk.ResponseType.ACCEPT) + questionLabel = Gtk.Label(label=question) + questionLabel.set_line_wrap(True) + dialog.vbox.pack_start(questionLabel, True, True, 0) + dialog.show_all() + result = dialog.run() + if result == Gtk.ResponseType.ACCEPT: + logger.log("Enabling accessibility...") + enableA11y() + elif result == Gtk.ResponseType.CLOSE: + bailBecauseA11yIsDisabled() + dialog.destroy() + + +class GnomeShell(object): # pragma: no cover + + """ + Utility class to help working with certain atributes of gnome-shell. + Currently that means handling the Application menu available for apps + on the top gnome-shell panel. Searching for the menu and its items is + somewhat tricky due to fuzzy a11y tree of gnome-shell, mainly since the + actual menu is not present as child to the menu-spawning button. Also, + the menus get constructed/destroyed on the fly with application focus + changes. Thus current application name as displayed plus a reference + known menu item (with 'Quit' as default) are required by these methods. + """ + + def __init__(self, classic_mode=False): + from tree import root + self.shell = root.application('gnome-shell') + + def getApplicationMenuList(self, search_by_item='Quit'): + """ + Returns list of all menu item nodes. Searches for the menu by a reference item. + Provide a different item name, if the 'Quit' is not present - but beware picking one + present elsewhere, like 'Lock' or 'Power Off' present under the user menu. + """ + matches = self.shell.findChildren( + predicate.GenericPredicate(name=search_by_item, roleName='label')) + for match in matches: + ancestor = match.parent.parent.parent + if ancestor.roleName == 'panel': + return ancestor.findChildren(predicate.GenericPredicate(roleName='label')) + from tree import SearchError + raise SearchError("Could not find the Application menu based on '%s' item. Please provide an existing reference item" + % search_by_item) + + def getApplicationMenuButton(self, app_name): + """ + Returns the application menu 'button' node as present on the gnome-shell top panel. + """ + try: + return self.shell[0][0][3].child(app_name, roleName='label') + except: + from tree import SearchError + raise SearchError( + "Application menu button of %s could not be found within gnome-shell!" % app_name) + + def getApplicationMenuItem(self, item, search_by_item='Quit'): + """ + Returns a particilar menu item node. Uses a different 'Quit' or custom item name for reference, but also + attempts to use the given item if the general reference fails. + """ + try: + menu_items = self.getApplicationMenuList(search_by_item) + except: + menu_items = self.getApplicationMenuList(item) + for node in menu_items: + if node.name == item: + return node + raise Exception( + 'Could not find the item, did application focus change?') + + def clickApplicationMenuItem(self, app_name, item, search_by_item='Quit'): + """ + Executes the given menu item through opening the menu first followed + by a click at the particular item. The menu search reference 'Quit' + may be customized. Also attempts to use the given item for reference + if search fails with the default/custom one. + """ + self.getApplicationMenuButton(app_name).click() + self.getApplicationMenuItem(item, search_by_item).click() diff --git a/build/lib/dogtail/version.py b/build/lib/dogtail/version.py new file mode 100644 index 00000000000..4d440806155 --- /dev/null +++ b/build/lib/dogtail/version.py @@ -0,0 +1,56 @@ +"""Handles versioning of software packages + +Author: Dave Malcolm <dmalcolm@redhat.com>""" +__author__ = 'Dave Malcolm <dmalcolm@redhat.com>' + + +class Version(object): + + """ + Class representing a version of a software package. + Stored internally as a list of subversions, from major to minor. + Overloaded comparison operators ought to work sanely. + """ + + def __init__(self, versionList): + self.versionList = versionList + + def fromString(versionString): + """ + Parse a string of the form number.number.number + """ + return Version(map(int, versionString.split("."))) + fromString = staticmethod(fromString) + + def __str__(self): + return ".".join(map(str, self.versionList)) + + def __getNum(self): + tmpList = list(self.versionList) + + while len(tmpList) < 5: + tmpList += [0] + + num = 0 + for i in range(len(tmpList)): + num *= 1000 + num += tmpList[i] + return num + + def __lt__(self, other): + return self.__getNum() < other.__getNum() + + def __le__(self, other): + return self.__getNum() <= other.__getNum() + + def __eq__(self, other): + return self.__getNum() == other.__getNum() + + def __ne__(self, other): + return self.__getNum() != other.__getNum() + + def __gt__(self, other): + return self.__getNum() > other.__getNum() + + def __ge__(self, other): + return self.__getNum() >= other.__getNum() diff --git a/build/lib/dogtail/wrapped.py b/build/lib/dogtail/wrapped.py new file mode 100644 index 00000000000..30c0570fa61 --- /dev/null +++ b/build/lib/dogtail/wrapped.py @@ -0,0 +1,33 @@ +""" +Superclasses for application wrappers + +Subclass these classes if you want to create application wrappers, e.g.: +http://svn.gnome.org/viewvc/dogtail-tests/trunk/appwrappers/dogtail/appwrappers/gedit.py?view=markup +""" +__author__ = "Zack Cerza <zcerza@redhat.com>" +import Accessibility + + +def makeWrapperClass(wrappedClass, name): # pragma: no cover + class klass(object): + + def __init__(self, obj): + self.obj = obj + + def __getattr__(self, name): + if name == 'obj': + return self.__dict__['obj'] + return getattr(self.obj, name) + + def __setattr__(self, name, value): + if name == 'obj': + self.__dict__['obj'] = value + else: + return setattr(self.obj, name, value) + + klass.__name__ = name + return klass + +Application = makeWrapperClass(Accessibility.Application, + "WrappedApplication") +Node = makeWrapperClass(Accessibility.Accessible, "WrappedNode") |