dogtail-0.9.9/0000775000175000017500000000000012647164550014012 5ustar vhumpavhumpa00000000000000dogtail-0.9.9/dogtail/0000775000175000017500000000000012647164550015435 5ustar vhumpavhumpa00000000000000dogtail-0.9.9/dogtail/__init__.py0000664000175000017500000000124512647161350017543 0ustar vhumpavhumpa00000000000000# -*- coding: UTF-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals """ GUI test tool and automation framework that uses Accessibility (a11y) technologies to communicate with desktop applications. """ __author__ = """Zack Cerza , Ed Rousseau , David Malcolm , Vita Humpa """ __version__ = "0.9.9" __copyright__ = "Copyright © 2005-2014 Red Hat, Inc." __license__ = "GPL" __all__ = ("config", "distro", "dump", "errors", "i18n", "logging", "path", "predicate", "procedural", "rawinput", "sessions", "tc", "tree", "utils", "version", "wrapped") dogtail-0.9.9/dogtail/config.py0000664000175000017500000001540212647161350017251 0ustar vhumpavhumpa00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals import locale import os import sys """ The configuration module. """ __author__ = "Zack Cerza , David Malcolm " 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': from dogtail 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, dictt): """ Loads values from dict, preserving any options already set that are not overridden. """ _Config.options.update(dictt) def reset(self): """ Resets all settings to their defaults. """ _Config.options = {} config = _Config() dogtail-0.9.9/dogtail/distro.py0000664000175000017500000002760712647161350017322 0ustar vhumpavhumpa00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals import os import re from dogtail.version import Version from dogtail.logging import debugLogger as logger from subprocess import check_output """ Handles differences between different distributions """ __author__ = "Dave Malcolm , Zack Cerza " 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.walk(localePrefix, appendIfMoFile, moFiles) return list(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 list(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.Cache() packages = self.cache.packages for package in packages: if package.name == packageName: verString = re.match('.*Ver:\'(.*)-.*\' Section:', str(package.current_ver)).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.Cache() packages = self.cache.packages for package in packages: if package.name == packageName: current = package.current_ver if not current: raise PackageNotFoundError(packageName) depends = current.depends_list list = depends['Depends'] for dependency in list: name = dependency[0].target_pkg.name # Add to the hash using a dummy value result[name] = None return list(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 list(result.keys()) class _ContinuousPackageDb(PackageDb): def __init__(self): PackageDb.__init__(self) def getVersion(self, packageName): return '' def getFiles(self, packageName): return check_output( ["ls -1 /usr/share/locale/*/LC_MESSAGES/%s.mo" % packageName], shell=True).strip().split('\n') def getDependencies(self, packageName): # Simulate a set using a hash (to a dummy value); # sets were only added in Python 2.4 return [] 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() class GnomeContinuous(Distro): # pragma: no cover def __init__(self): self.packageDb = _ContinuousPackageDb() 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 elif os.path.exists("/etc/os-release") and \ re.match(".*GNOME-Continuous", open("/etc/os-release").readline()): # pragma: no cover distro = GnomeContinuous() # pragma: no cover else: raise DistributionNotSupportedError("Unknown") # pragma: no cover logger.log(distro.__class__.__name__) return distro distro = detectDistro() packageDb = distro.packageDb dogtail-0.9.9/dogtail/dump.py0000664000175000017500000000173212647161350016752 0ustar vhumpavhumpa00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals """ Utility functions for 'dumping' trees of Node objects. """ __author__ = "Zack Cerza " 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 list(node.actions.values()): dump(action, depth + 1) for child in node.children: crawl(child, depth + 1) def dumpFile(item, depth): _file.write(str(spacer * depth) + str(item) + str('\n')) def dumpStdOut(item, depth): try: print(spacer * depth + str(item)) except UnicodeDecodeError: print(spacer * depth + str(item).decode('utf8')) if fileName: dump = dumpFile _file = open(fileName, 'w') else: dump = dumpStdOut crawl(node, 0) dogtail-0.9.9/dogtail/errors.py0000664000175000017500000000136612647161350017324 0ustar vhumpavhumpa00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals from dogtail.logging import debugLogger as logger import inspect """ General exceptions; not overly module-specific """ __author__ = "Zack Cerza " 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] != '' and frameRec[1] != '': message = message + ':\n ' + frameRec[4][0] del frameRec logger.log(message) class DependencyNotFoundError(Exception): """ A dependency was not found. """ pass dogtail-0.9.9/dogtail/i18n.py0000664000175000017500000002200512647161350016560 0ustar vhumpavhumpa00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals from dogtail import config import os import re import gettext from dogtail.logging import debugLogger as logger """ Internationalization facilities """ __author__ = """David Malcolm , Zack Cerza """ """ 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): # 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 list(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): results[result] = True # No translations found: if len(results) == 0: if config.config.debugTranslation: logger.log('Translation not found for "%s"' % srcString) return list(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. """ 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). """ 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 + '$' 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 += '"%s", ' % tString result = '"%s" (%s)' % ( self.untranslatedString, translations) return 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): from dogtail 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'. """ from dogtail 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. from dogtail 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) dogtail-0.9.9/dogtail/logging.py0000664000175000017500000001443212647161350017434 0ustar vhumpavhumpa00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals from dogtail.config import config import os import sys import time import traceback """ Logging facilities """ __author__ = """Ed Rousseau , Zack Cerza """ class TimeStamp(object): """ Timestamp class for file logs Generates timestamps tempfiles and log entries """ def __init__(self): self.now = "0" self.timetup = time.localtime() def zeroPad(self, intt, 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 intt < 10 ** width: return ("0" * (width - len(str(intt)))) + str(intt) else: return str(intt) # 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.filee = file # Handle to the logfile if not self.filee: return scriptName = config.scriptName # most probably non-reachable code if not scriptName: # pragma: no cover 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.filee = open(self.fileName, mode='w') self.filee.write("##### " + os.path.basename(self.fileName) + '\n') self.filee.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 to open and write the result to the log file. if isinstance(self.filee, bool) and (force or config.logDebugToFile): self.createFile() if force or config.logDebugToFile: if newline: self.filee.write(message + '\n') else: self.filee.write(message + ' ') self.filee.flush() if self.stdOut and config.logDebugToStdOut: 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 = list(entry.keys()) value = list(entry.values()) key = key[0] value = value[0] entry = str(key) + ": " + str(value) else: print("Method argument requires a 1 {key: value} dict. Supplied argument not one {key: value}") raise ValueError(entry) Logger.log(self, self.stamper.entryStamp() + " " + entry, force=True) debugLogger = Logger('debug', config.logDebugToFile) 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 dogtail-0.9.9/dogtail/path.py0000664000175000017500000000640512647161350016743 0ustar vhumpavhumpa00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals __author__ = """David Malcolm """ 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.lst = [] def __str__(self): result = "{" for (predicate, isRecursive) in self.lst: result += "/(%s,%s)" % (predicate.describeSearchResult(), isRecursive) return result + "}" # We need equality to work so that dicts of these work: def __eq__(self, other): if len(self.lst) != len(other.lst): return False else: for i in range(len(self.lst)): if self.lst[i] != other.lst[i]: return False return True def append(self, predicate, isRecursive): assert predicate self.lst.append((predicate, isRecursive)) def __iter__(self): return iter(self.lst) def length(self): return len(self.lst) 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.lst: # 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. """ i = 0 for i in range(len(self.lst)): if self.lst[i] != other.lst[i]: break if i > 0: # Slice from this point to the end: result = SearchPath() result.lst = other.lst[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.lst.append(self.lst[i]) return result def getPredicate(self, i): (predicate, _) = self.lst[i] return predicate dogtail-0.9.9/dogtail/predicate.py0000664000175000017500000003552012647161350017747 0ustar vhumpavhumpa00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals from dogtail.config import config from dogtail.i18n import TranslatableString from dogtail.logging import debugLogger as logger from gi.repository import GLib import re from time import sleep """ Predicates that can be used when searching for nodes. """ __author__ = 'David Malcolm ' 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 not isinstance(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 re.match(r"name :[0-9]+\.[0-9]+ was not provided", 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 re.match(r"name :[0-9]+\.[0-9]+ was not provided", 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" dogtail-0.9.9/dogtail/procedural.py0000664000175000017500000003142612647161350020150 0ustar vhumpavhumpa00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals from dogtail import errors from dogtail import predicate from dogtail import rawinput from dogtail import tree from dogtail.config import config from dogtail.utils import Lock import os """ Dogtail's procedural UI All the classes here are intended to be single-instance, except for Action. """ __author__ = 'Zack Cerza ' # # # 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. ;) # # # class FocusError(Exception): pass 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 dogtail.utils import run as utilsRun pid = utilsRun(application + ' ' + arguments, appName=appName) focus.application(application) return pid # 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) dogtail-0.9.9/dogtail/rawinput.py0000664000175000017500000001771512647161350017666 0ustar vhumpavhumpa00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals from dogtail.config import config from dogtail.utils import doDelay from dogtail.logging import debugLogger as logger from pyatspi import Registry as registry from pyatspi import (KEY_SYM, KEY_PRESS, KEY_PRESSRELEASE, KEY_RELEASE) import gi gi.require_version('Gtk', '3.0') gi.require_version('Gdk', '3.0') from gi.repository import Gdk """ Handles raw input using AT-SPI event generation. Note: Think of keyvals as keysyms, and keynames as keystrings. """ __author__ = """ David Malcolm , Zack Cerza """ 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, name='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, name='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, name='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, name='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, name='abs') if mouseDelay: doDelay(mouseDelay) else: doDelay() def absoluteMotionWithTrajectory(source_x, source_y, dest_x, dest_y, mouseDelay=None, check=True): """ Synthetize mouse absolute motion with trajectory. The 'trajectory' means that the whole motion is divided into several atomic movements which are synthetized separately. """ if check: checkCoordinates(source_x, source_y) checkCoordinates(dest_x, dest_y) logger.log("Mouse absolute motion with trajectory to (%s,%s)" % (dest_x, dest_y)) dx = float(dest_x - source_x) dy = float(dest_y - source_y) max_len = max(abs(dx), abs(dy)) if max_len == 0: # actually, no motion requested return dx /= max_len dy /= max_len act_x = float(source_x) act_y = float(source_y) for _ in range(0, int(max_len)): act_x += dx act_y += dy if mouseDelay: doDelay(mouseDelay) registry.generateMouseEvent(int(act_x), int(act_y), name='abs') if mouseDelay: doDelay(mouseDelay) else: doDelay() def relativeMotion(x, y, mouseDelay=None): """ Synthetize a relative motion from actual position. Note: Does not check if the end coordinates are positive. """ logger.log("Mouse relative motion of (%s,%s)" % (x, y)) registry.generateMouseEvent(x, y, name='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) (x, y) = toXY absoluteMotion(x, y, check=check) doDelay() release(x, y, button, check) doDelay() def dragWithTrajectory(fromXY, toXY, button=1, check=True): """ Synthetize a mouse press, drag (including move events), and release on the screen """ logger.log("Mouse button %s drag with trajectory from %s to %s" % (button, fromXY, toXY)) (x, y) = fromXY press(x, y, button, check) (x, y) = toXY absoluteMotionWithTrajectory(fromXY[0], fromXY[1], 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. """ 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' } def uniCharToKeySym(uniChar): i = ord(uniChar) keySym = Gdk.unicode_to_keyval(i) return 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. 'p' or 'PageUp' or '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() dogtail-0.9.9/dogtail/sessions.py0000664000175000017500000001563612647161350017663 0ustar vhumpavhumpa00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals import time import os import pwd import errno import re import subprocess 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-