Thread: [pyrap-devel] SF.net SVN: pyrap: [2] pyrap/pyrap
Status: Planning
Brought to you by:
jcorbett
From: <jco...@us...> - 2006-02-24 23:18:22
|
Revision: 2 Author: jcorbett Date: 2006-02-24 15:18:15 -0800 (Fri, 24 Feb 2006) ViewCVS: http://svn.sourceforge.net/pyrap/?rev=2&view=rev Log Message: ----------- forgot to add the actual python files ;) Added Paths: ----------- pyrap/pyrap/__init__.py pyrap/pyrap/imagerec.py pyrap/pyrap/project.py pyrap/pyrap/session.py Added: pyrap/pyrap/__init__.py =================================================================== --- pyrap/pyrap/__init__.py (rev 0) +++ pyrap/pyrap/__init__.py 2006-02-24 23:18:15 UTC (rev 2) @@ -0,0 +1,4 @@ + +__all__ = ['project', 'session', 'imagerec'] + +__docformat__ = "javadoc" \ No newline at end of file Property changes on: pyrap/pyrap/__init__.py ___________________________________________________________________ Name: svn:executable + * Name: svn:mime-type + text/plain Name: svn:eol-style + native Added: pyrap/pyrap/imagerec.py =================================================================== --- pyrap/pyrap/imagerec.py (rev 0) +++ pyrap/pyrap/imagerec.py 2006-02-24 23:18:15 UTC (rev 2) @@ -0,0 +1,123 @@ +"""Image Recognition Module. Here you will find functions that will allow you +to find images on the X11 screen. Although the ability to provide this +capability isn't coded here in python, it is no less powerful. visgrep command +from <a href="http://hoopajoo.net/projects/xautomation.html">xautomation +</a> provides the ability to find one image inside another, this module turns +that command line program into an automation framewok.""" + +__docformat__ = "javadoc" + +import subprocess +import gc +import session +import types +import os.path +import tempfile +import atexit +import re + +from time import time + + +class ImageRecognitionException(Exception): + """This class is for errors related to recognizing an image on the X11 Screen. + You might get this exception if you said there should only be one of the image + on the screen, but more than one is detected. Also if any programs that this + module depends on is missing, or if the image your looking for isn't a png (a + major no-no).""" + pass + +class SearchPattern: + """This represents the image your looking for on the X11 display. You will + need to pass in a .png file to the constructor. This is then turned into a + pattern that <a href="http://hoopajoo.net/projects/xautomation.html"> + xautomation's visgrep</a> can use to search for on the screen. + @cvar DEFAULT_TIMEOUT The default amount of time to look for an image on the + screen in seconds. + @type DEFAULT_TIMEOUT int + @cvar _tempdir The directory to store all the .pat images in, cleanup is done + at exit. + @type _tempdir str + @ivar _patfile The pattern file that visgrep uses to search for, created on + initialization of the object. + @type _patfile str""" + + DEFAULT_TIMEOUT = 60 + + _visgrepRE = re.compile(r'^(\d+),(\d+)\s') + + _tempdir = tempfile.mkdtemp("irec") + + def __init__(self, image): + """Create a new image search pattern. Pass in the path to a png file. + @param image The path on the file system to a png image to look for + @type image str + @raise pyrap.imagerec.ImageRecognitionException Raises exception when the + image passed in doesn't exist, + when it isn't a png, or when + there is an error converting + it to a .pat (see visgrep + manpage).""" + assert(isinstance(image, types.StringTypes)) + if not os.path.exists(image): + raise ImageRecognitionException, "Path [%s] does not exist." % (image) + (fileHandle, self._patfile) = tempfile.mkstemp(suffix=".pat", dir=self._tempdir) + os.close(fileHandle) + retcode = subprocess.call(args=["/bin/bash","-c", "png2pat %s >%s" % (image, self._patfile)]) + if retcode != 0: + raise ImageRecognitionException, "png2pat of file [%s] to file [%s] failed!" % (image, self._patfile) + if os.path.getsize(self._patfile) <= 0: + raise ImageRecognitionException, "Size of patfile [%s] is [%d], it came from png file [%s] size [%d]." % (self._patfile, os.path.getsize(self._patfile), image, os.path.getsize(image)) + atexit.register(self._cleanup) + + + def findOnScreen(self, xsession, timeout=DEFAULT_TIMEOUT): + """Find the image on the screen. This method works by searching for the + image on the screen, until timeout occurs. If timeout occurs before a match + is made None is return. However if a match is found within the timeout + period, all the coordinates of the specified instance are returned. + @param xsession The X11Session object to look for the image on. + @type xsession pyrap.session.X11Session + @param timeout The amount in seconds to keep looking for the image. + @type timeout int + @return None (if no matches were found), or a tuple containing the + coordinates (x,y) in the screen for each match. + ie ((10, 589)(895, 1478))""" + assert(isinstance(xsession, session.X11Session)) + assert(isinstance(timeout, types.IntType)) + startTime = time() + endTime = startTime + timeout + coordinates = [] + ssFileName = os.path.join(self._tempdir, "shot.png") + while time() < endTime: + print "Before screen shot.\n" + screenShot = xsession.getScreenShot() + print "After screen shot.\n" + screenShot.save(ssFileName, "png") + print "After screen shot save.\n" + visgrep = subprocess.Popen(["visgrep", ssFileName, self._patfile], stdout=subprocess.PIPE) + retcode = visgrep.wait() + print "After visgrep.\n" + if retcode == 1: + for line in visgrep.stdout: + lineMatch = self._visgrepRE.match(line) + if lineMatch is not None: + coordinates.append((int(lineMatch.group(1)), int(lineMatch.group(2)))) + del lineMatch + if len(coordinates) > 0: + return tuple(coordinates) + print "After line processing.\n" + del screenShot + del visgrep + print "About to collect garbage.\n" + gc.collect() + print "After Garbage collection.\n" + + return None + + def _cleanup(self): + if os.path.exists(self._patfile): + os.unlink(self._patfile) + if len(os.listdir(self._tempdir)) == 0: + os.rmdir(self._tempdir) + Property changes on: pyrap/pyrap/imagerec.py ___________________________________________________________________ Name: svn:executable + * Name: svn:mime-type + text/plain Name: svn:eol-style + native Added: pyrap/pyrap/project.py =================================================================== --- pyrap/pyrap/project.py (rev 0) +++ pyrap/pyrap/project.py 2006-02-24 23:18:15 UTC (rev 2) @@ -0,0 +1,193 @@ +"""Features of pyrap relating to projects, including testcases. You shouldn't +need to create a instance of TestCase directly, instead use the +{@link pyrap.project.load load()} method to create an instance. This makes +working with the image recognition module much easier, and more intuitive. +@author Jason Corbett +@version 1.0""" + +__docformat__ = "javadoc" + +import sys +import os.path +import UserDict +import ConfigParser +import imagerec, session +import logging, logging.config + +def load(directory=os.path.abspath(os.path.dirname(sys.argv[0])), filename="project.ini", runtime="var.ini"): + """Load an instance of {@link pyrap.project.TestCase TestCase} using settings + in a project.ini. By default use the directory of the calling script (argv[0]) + and project.ini as the project file to load. Dynamic variables should be + loaded through var.ini in the same directory. + @param directory The directory that holds the test case files to load. + @type directory str + @param filename The name of the main project ini file (holds project settings) + @type filename str + @param runtime The name of the runtime variables file (additional to project + settings). + @type runtime str + @return Instance of Testcase that has all images registered and is ready to + run, or None on error. + @rtype pyrap.project.TestCase""" + project_file = os.path.join(directory, filename) + runtime_file = os.path.join(directory, runtime) + default_project_configfile = os.path.join(os.path.abspath(os.path.dirname(__file__)), "project-defaults.ini") + default_logging_configfile = os.path.join(os.path.abspath(os.path.dirname(__file__)), "logging-defaults.ini") + + options = ConfigParser.SafeConfigParser() + options.read([default_project_configfile, project_file, runtime_file]) + + custom_logging_configfile = os.path.join(directory, options.get("Test Case", "logging.configfile")) + + if options.get("Test Case", "logging") == "custom" and os.path.exists(custom_logging_configfile): + logging.config.fileConfig(custom_logging_configfile, {"tcroot": directory}) + else: + logging.config.fileConfig(default_logging_configfile, {"tcroot": directory}) + + pyrap_logger = logging.getLogger("pyrap.project.load") + tc_logger = logging.getLogger("testcase.%s" % (options.get("Test Case", "name"))) + + if options.has_option("Test Case", "logging.pyrap.level"): + logging.getLogger("pyrap").setLevel(eval("logging.%s" % (options.get("Test Case", "logging.pyrap.level")))) + + if options.has_option("Test Case", "logging.testcase.level"): + logging.getLogger("testcase").setLevel(eval("logging.%s" % (options.get("Test Case", "logging.testcase.level")))) + + all_requisites_found = True + for requisite in ["Test Case", "X11 Session", "Images"]: + if not options.has_section(requisite): + all_requisites_found = False + tc_logger.critical("Project [%s] does not have section [%s] in the ini file [%s].", options.get("Test Case", "name"), requisite, project_file) + if not all_requisites_found: + return None + + + + + + +class TestCase(UserDict.UserDict): + """Uses parts of {@link pyrap.session.X11Session X11Session} to create + a testcase. Settings should be a ConfigParser object. Possible settings are: + <ul> + <li>Section <b>Test Case</b> + <ul> + <li><code>name = My Test Case</code> + <p>This name is used in logging and the reports generated. It's + important to give a descriptive name, that is also unique (for + clarity).</p> + </li> + <li><code>logging = normal</code> + <p>logging for pyrap. There is a pre-configured logging setup, + putting all logs into a logs/ directory, but you can completely + customize the logging. Valid values are: "normal", "custom", or + "semi normal". "normal" means no changes. "custom" means that + you want to specify a logging config file (in python looging + <a href="http://www.python.org/doc/2.4.1/lib/logging-config-fileformat.html"> + configuration syntax</a>). "semi normal" means that you want the + normal configuration, but with some customizations (options listed + below).</p> + </li> + <li><code>logging.configfile = logging.ini</code> + <p>If and only if you specified "custom" as the type of logging + does this parameter get evaluated. It should point to a file + (relative to the root of the test directory) that contains your + logging configuration.</p> + </li> + <li><code>logging.pyrap.level = DEBUG</code> + <p>If and only if you specified "semi normal" as the type of logging + does this parameter get evaluated. This is the logging level for + pyrap internals, not for the testcase itself. It should be a + valid logging level (such as <code>CRITICAL, ERROR, WARNING, INFO, + DEBUG, NOTSET</code>).</p> + <p>Default is <code>WARNING</code></p> + </li> + <li><code>logging.testcase.level = DEBUG</code> + <p>If and only if you specified "semi normal" as the type of logging + does this parameter get evaluated. This is the logging level for + the testcase, not for the pyrap internals. If you're unsure what + you want, this is probably it, pyrap internals deals with what's + happening in pyrap itself. It should be a valid logging level + (such as <code>CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET</code> + ).</p> + <p>Default is <code>INFO</code></p> + </li> + </ul> + </li> + <li>Section <b>X11 Session</b> + <ul> + <li><code>resolution.x = 1024</code> + <p>The width of the X11 Screen to create, default is + <code>1024</code></p> + </li> + <li><code>resolution.y = 768</code> + <p>The height of the X11 Screen to create, default is + <code>768</code></p> + </li> + <li><code>color depth = 16</code> + <p>The color depth to use for the X Server. <b>IMPORTANT:</b> + please note that you don't want to use a different depth other + than what you used to record your test case. If the png images + you captured were in a different color depth, image recognition + will not work.</p> + <p>Default is <code>16</code></p> + </li> + </ul> + </li> + <li>Section <b>Images</b><br/> + example: <code>name = image file name</code> + <p>This should be a mapping between the names you want to use in your + scripts (for example: "Start Menu") and the actual file names of the + images (for example: "images/start.png", or "images/984810238.png"). + For each image listed here a {@link pyrap.imagerec.SearchPattern + SearchPattern} object will be created on initialization, and held for + use in the test case. Filenames should be relative to the root of the + TestCase directory</p> + </li> + <li>Section <b>Variables</b><br/> + <p>All key value pairs in this section will be put into this classes + dictionary properties. That means that if you have an entry in your + ini file for Variables, and in it you have the entry <code>Foo = Bar</code> + , then you can reference it by <code>tcinstance["Foo"]</code> and + you will get Bar in return.</p> + </li> + </ul>""" + + def __init__(self, settings, rootDirectory): + """Create an instance of TestCase with the settings passed in. + @param settings ConfigParser object containing settings for this Test Case. + @type settings {@link ConfigParser.ConfigParser ConfigParser} + @param rootDirectory The root of the Test Case + @type rootDirectory str""" + UserDict.UserDict.__init__(self) + self.logger = logging.getLogger("pyrap.project.TestCase") + assert(isinstance(settings, ConfigParser.ConfigParser)) + if settings.has_section("Variables"): + self.logger.debug("Going to add all entries from Variables section to project.") + for key, value in settings.items("Variables"): + self.logger.debug("Adding variable entry to project: %s = %s", key, value) + self[key] = value + self.searchPatterns = {} + self.logger.debug("Creating SearchPattern objects from Images section in ini file.") + for name, filename in settings.items("Images"): + try: + if os.path.exists(os.path.join(rootDirectory, filename)): + self.logger.debug("Creating SearchPattern object for entry: %s = %s, fullpath name: %s", name, filename, os.path.join(rootDirectory, filename)) + self.searchPatterns[name] = imagerec.SearchPattern(os.path.join(rootDirectory, filename)) + self.logger.debug("SearchPattern with the name of %s, was successfully created.", name) + else: + self.logger.error("File does not exist for image entry: %s = %s", name, filename) + continue + except imagerec.ImageRecognitionException, error: + self.logger.error("Error creating SearchPattern, will be unavailable, entry: %s = %s, error: ", name, filename, error) + continue + self.logger.debug("Done creating SearchPattern objects.") + + xres = settings.getint("X11 Session", "resolution.x") + yres = settings.getint("X11 Session", "resolution.y") + depth = settings.getint("X11 Session", "depth") + + self.logger.debug("Creating X11 Session with resolution %dx%d and depth %d", xres, yres, depth) + self.xs = session.X11Session(xres, yres, depth) + self.logger.debug("Created X11 Session, DISPLAY=:%d", self.xs.display) + Property changes on: pyrap/pyrap/project.py ___________________________________________________________________ Name: svn:executable + * Name: svn:mime-type + text/plain Name: svn:eol-style + native Added: pyrap/pyrap/session.py =================================================================== --- pyrap/pyrap/session.py (rev 0) +++ pyrap/pyrap/session.py 2006-02-24 23:18:15 UTC (rev 2) @@ -0,0 +1,429 @@ +"""The Session Module contains functions and classes for running a Test Session. +<p>Sessions consist of at least 1 program running in a X11 virtual server (Xvfb). +Contained in this module is variables for key bindings (special keys), and +the TestSession class that has methods relating to the running session.</p> + +@author Jason Corbett +@version 1.0 + +@var SpecialKeys Special Keys holds all the keys that you would want to simulate + pressing in an X11 Session. You can use these keys with the + X11Session Class, or with KeyCombination Class (to pass + eventually to X11Session). Keys available are: + <ul> + <li>Home Key</li> + <li>Arrow Keys (Up, Down, Left, and Right)</li> + <li>PageUp and PageDown Keys</li> + <li>End Key</li> + <li>Return Key (aka Enter)</li> + <li>Tab Key</li> + <li>Escape Key</li> + <li>Delete Key</li> + <li>Modifier Keys like Shift, Control, Alt, and Meta (Windows)</li> + <li>Backspace Key</li> + </ul> + <p>Keys that have Left and Right ones on the keyboard can be + accessed with or without the Left and Right prefix (example: + SpecialKeys.LeftShift = SpecialKeys.Shift). Left and Right + prefixes should come first before the name of the key. You + should be able to acess the keys with the names above, and case + shouldn't matter. If you really care, you can look at + {@link pyrap.session.SpecialKeyClass#specialKeys specialKeys} + variable of the SpecialKeyClass.</p> +@type SpecialKeys pyrap.session.SpecialKeyClass +""" + +import types +import os +import tempfile +import atexit +import subprocess +import signal +import gtk.gdk + +# The following is required for epydoc to parse comments correctly +__docformat__ = "javadoc" + +class MouseClickType: + """This class is pretty basic, however several static methods exist to allow + you to create specific, common used instances of this class. For instance, + you might need to double click. Rather than creating a separate instance of + MouseClickType by doing: <code>MouseClickType(1,2)</code>, which isn't readable + to anyone reading your code, use should use the DoubleClick method which does + the same thing, it should also cache the instance creation so that you don't + have to create multiple instances of the same value. + @ivar commands An array of strings that holds the commands to send to xte. + It is created in the __init__ method. + @type commands list""" + + _cache = {} + + def __init__(self, button, clicks, sleep=100000): + """Create a MouseClickType with the button, number of clicks and sleep value. + This is just a set of commands to xte that say click mouse button (specified + by button), clicks number of times, and sleep between the clicks. + @param button The mouse button number to click. + @type button int + @param clicks The number of sucessive clicks you want to do. + @type clicks int + @param sleep The amount of time to sleep inbetween the clicks. + @type sleep int""" + assert(isinstance(button, types.IntType)) + assert(isinstance(clicks, types.IntType)) + assert(isinstance(sleep, types.IntType)) + + self.commands = [] + for click in range(0, clicks): + if click != 0: + self.commands.append("usleep %d" % (sleep)) + self.commands.append("mouseclick %d" % (button)) + + @staticmethod + def _create_cached(button, clicks, sleep=100000): + """This private static method is used by the other static methods to make + sure we don't create more instances than is needed. This may not be needed, + but just seems to make sense. + @param button {@link pyrap.session.MouseClickType#__init__ See __init__} + @type button int + @param clicks {@link pyrap.session.MouseClickType#__init__ See __init__} + @type clicks int + @param sleep {@link pyrap.session.MouseClickType#__init__ See __init__} + @type sleep int + @return A cached instance of MouseClickType. + @rtype {@link pyrap.session.MouseClickType MouseClickType}""" + key = (button, clicks, sleep) + if not MouseClickType._cache.has_key(key): + MouseClickType._cache[key] = MouseClickType(button, clicks, sleep) + return MouseClickType._cache[key] + + @staticmethod + def SingleClick(): + """Create an instance of MouseClickType that represents a single click. + @return A cached instance of MouseClickType representing a Single Click. + @rtype {@link pyrap.session.MouseClickType MouseClickType}""" + return MouseClickType._create_cached(1, 1) + + @staticmethod + def DoubleClick(): + """Create an instance of MouseClickType that represents a double click. + @return A cached instance of MouseClickType representing a Double Click. + @rtype {@link pyrap.session.MouseClickType MouseClickType}""" + return MouseClickType._create_cached(1, 2) + + @staticmethod + def MiddleClick(): + """Create an instance of MouseClickType that represents a middle mouse + button click. + @return A cached instance of MouseClickType representing a middle mouse + button click. + @rtype {@link pyrap.session.MouseClickType MouseClickType}""" + return MouseClickType._create_cached(3, 1) + + @staticmethod + def RightClick(): + """Create an instance of MouseClickType that represents a right mouse + button click. + @return A cached instance of MouseClickType representing a right mouse + button click. + @rtype {@link pyrap.session.MouseClickType MouseClickType}""" + return MouseClickType._create_cached(2, 1) + + @staticmethod + def ScrollDown(clicks=3): + """Create an instance of MouseClickType that represents someone using the + wheel portion of their mouse to scroll down on a window. You can pass in + how many "clicks" or times the scroll happens, default is 3. Please note + that this doesn't mean it will scroll down 3 lines, that depends on the app, + and how it determines how many "lines" a downward scroll is. + @return A cached instance of MouseClickType representing scrolling down on + the mouse wheel. + @rtype {@link pyrap.session.MouseClickType MouseClickType}""" + return MouseClickType._create_cached(5, clicks) + + @staticmethod + def ScrollUp(clicks=3): + """Create an instance of MouseClickType that represents someone using the + wheel portion of their mouse to scroll up on a window. You can pass in + how many "clicks" or times the scroll happens, default is 3. Please note + that this doesn't mean it will scroll up 3 lines, that depends on the app, + and how it determines how many "lines" a upward scroll is. + @return A cached instance of MouseClickType representing scrolling up on the + mouse wheel. + @rtype {@link pyrap.session.MouseClickType MouseClickType}""" + return MouseClickType._create_cached(4, clicks) + + +class SpecialKeyClass: + """You shouldn't need to use this class directly. Instead an instance of this + class is provided in this module, called SpecialKeys. See the documentation + on the variable for more information. + @ivar specialKeys The master list of all special keys. This is a dict object + that maps all the normal names to internal specific + representations. Look at the source for full list if it's + not in the doc (can't seem to get epydoc to include the full + value list). + + @ivar lowerCaseNames A dict object containing the same names in it as + specialKeys, but this time all of the keys are lower cased + for case-insensitive lookup. The value portion is the real + key of specialKeys. This dict is populated on __init__.""" + specialKeys = { "Home": "Home", + "LeftArrow": "Left", + "UpArrow": "Up", + "RightArrow": "Right", + "DownArrow": "Down", + "Left": "Left", + "Up": "Up", + "Right": "Right", + "Down": "Down", + "PageUp": "Page_Up", + "PageDown": "Page_Down", + "End": "End", + "Return": "Return", + "Enter": "Return", + "Backspace": "Backspace", + "Tab": "Tab", + "Escape": "Escape", + "Delete": "Delete", + "ShiftLeft": "Shift_L", + "Shift": "Shift_L", + "ShiftRight": "Shift_R", + "ControlLeft": "Control_L", + "Control": "Control_L", + "ControlRight": "Control_R", + "WindowsLeft": "Meta_L", + "MetaLeft": "Meta_L", + "Windows": "Meta_L", + "WindowsRight": "Meta_R", + "MetaRight": "Meta_R", + "AltLeft": "Alt_L", + "Alt": "Alt_L", + "AltRight": "Alt_R"} + # Dictionary for case insensitive lookup of key names. Built in the + # constructor. + lowerCaseNames = {} + + + def __init__(self): + """Nothing special about this constructor. + <p>However, we do build the lower case names dictionary for case insensitive + lookups of key names.</p>""" + for key in self.specialKeys.keys(): + self.lowerCaseNames[key.lower()] = key + self.specialKeySet = set(self.specialKeys.values()) + + def __getattr__(self, name): + """Python calls this method when you try to access SpecialKeys.whatever. + <p>In particular you can use SpecialKeys.HOME SpecialKeys.Home or + SpecialKeys.home, they all mean the same thing. Case is in sensitive. This + method shouldn't need to be called from your code.</p>""" + if(self.specialKeys.has_key(name)): + return(self.specialKeys[name]) + elif self.lowerCaseNames.has_key(name.lower()): + return(self.specialKeys[self.lowerCaseNames[name.lower()]]) + else: + raise AttributeError, name + + def AllSpecialKeys(self): + """Get a Set of all special keys. A set is used to eliminate duplicates. + @returns A set of all special keys + @rtype set""" + return(self.specialKeySet) + + +# SpecialKeys holds name to internal mappings of the special keys you may +# need to send to your application. +SpecialKeys = SpecialKeyClass() + +class KeyCombination: + """Represents a set of keys to be pressed, ex: Ctrl-Alt-Delete. + <p>When you need to send a special key combination to the X11Session, you can + create an instance of this class to represent the key combination. """ + # Special keys for use in sending key events + ModifierKeys = frozenset([SpecialKeys.Shift, + SpecialKeys.ShiftLeft, + SpecialKeys.ShiftRight, + SpecialKeys.Control, + SpecialKeys.ControlLeft, + SpecialKeys.ControlRight, + SpecialKeys.Windows, + SpecialKeys.WindowsLeft, + SpecialKeys.WindowsRight]); + + def __init__(self, modifiers, nonmodifier): + """Create a KeyCombination by using one or more modifier keys, and one non + non-modifier. Modifiers can be a list of modifiers, or just one. You can + only have one non modifier. + @param modifiers - Either a list or a single modifier key, should come from + SpecialKeys class. + @param nonmodifier - Can be a special key that is not a modifier, or a + normal character.""" + + # First make sure that everything they passed in for modifiers are in fact + # modifiers + if not isinstance(modifiers, types.ListType): + modifiers = [modifiers,] + modifierSet = set(modifiers) + assert(modifierSet.issubset(self.ModifierKeys)) + self.commands = [] + for modifier in modifiers: + self.commands.append("keydown %s\n" % (modifier)) + + # Second make sure we only have a string representing either a SpecialKey + # or a single character + assert(isinstance(nonmodifier, types.StringTypes)) + assert(nonmodifier in SpecialKeys.AllSpecialKeys() or len(nonmodifier) == 1) + + self.commands.append("keydown %s" % (nonmodifier)) + self.commands.append("keyup %s" % (nonmodifier)) + for modifier in modifiers: + self.commands.append("keyup %s" % (modifier)) + + +class X11Exception(Exception): + """Used for problems relating to starting and running a X11 Session.""" + pass + + +class X11Session: + """X11Session is used to start a virtual X11 session, control it, and get + information from it. This is used in playback mode of pyrap. Some + functionality in this may be wrapped by other classes + @see pyrap.project.TestCase TestCase + @ivar displaynum The display number of the X server + @ivar tempdir The directory where we store the XAuthority file for the X server + @ivar xauth The full path to the XAuthority file + @ivar XServerProcess The subprocess.Popen object for the X Server + @ivar xteProcess The subprocess.Popen object for the xte program""" + + def __init__(self, width, height, depth=16): + """Start a virtual X session (using Xvfb). The virtual X server is started, + and the subprocess.Popen object is saved. Also started is xte, the process + that is used to send mouse and keyboard events to the X server. The startup + process is as follows: + <ol> + <li>Create a temporary directory</li> + <li>Use mcookie to get a hex key for XAuth file</li> + <li>Find open display</li> + <li>Start Xvfb</li> + <li>Start xte</li> + </ol> + @param width The width of the X screen + @param height The height of the X screen + @param depth The color depth of the X screen (default 16bpp) + @type width int + @type height int + @type depth int""" + assert(isinstance(width, types.IntType)) + assert(isinstance(height, types.IntType)) + assert(isinstance(depth, types.IntType)) + self.width = width + self.height = height + self.tempdir = tempfile.mkdtemp("pyrap") + self.xauth = self.tempdir + "/Xauthority" + mcookieproc = subprocess.Popen(args=["mcookie"], stdout=subprocess.PIPE) + retval = mcookieproc.wait() + if retval != 0: + raise X11Exception, "Return value from mcookie was %d." % (retval) + mcookie = mcookieproc.stdout.readline() + + self.displaynum = self._findOpenDisplay() + self.newenviron = dict(os.environ) + self.newenviron['XAUTHORITY'] = self.xauth + self.newenviron['DISPLAY'] = ":%d" % (self.displaynum) + retval = subprocess.call(args=["xauth", "add", self.newenviron['DISPLAY'], ".", mcookie], env=self.newenviron) + if retval != 0: + raise X11Exception, "Return value from mcookie was %d." % (retval) + if os.environ.has_key("XAUTHORITY"): + subprocess.call(args=["xauth", "merge", self.newenviron['XAUTHORITY']]) + + self.XServerProcess = subprocess.Popen(args=["Xvfb", self.newenviron['DISPLAY'], "-screen", "0", "%dx%dx%d" % (width, height, depth)], env=self.newenviron) + self.xteProcess = subprocess.Popen(args=["xte"], env=self.newenviron, stdin=subprocess.PIPE) + self.display = gtk.gdk.Display(self.newenviron['DISPLAY']) + self.default_colormap = self.display.get_default_screen().get_default_colormap() + self.root_window = self.display.get_default_screen().get_root_window() + + atexit.register(self._cleanup) + + def sendString(self, chars): + """Send a string to the X11Session. This is done through the use of xte. + @param chars The string of characters you want to send to the X server + @type chars str""" + assert(isinstance(chars, types.StringTypes)) + self.xteProcess.stdin.write("str %s\n" % (chars)) + + def sendKeyCombination(self, combo): + """Send a key combination, like ctrl-alt-delete, to the X server. + @param combo KeyCombination to send to the X server + @type combo pyrap.session.KeyCombination""" + assert(isinstance(combo, KeyCombination)) + for command in combo.commands: + self.xteProcess.stdin.write(command) + + def pushKey(self, key): + """Send a push and release key event to the X Session. + @param key Can be a single character, or a SpecialKey. + @type key str""" + assert(isinstance(key, types.StringTypes)) + assert(len(key) == 1 or set([key]).issubset(SpecialKeys.specialKeySet)) + self.xteProcess.stdin.write("key %s\n" % (key)) + + def moveMouseTo(self, x, y): + """Move the mouse pointer to the specified coordinates. + @param x the x coordinate to move to, 0 is at the left of the screen + @param y the y coordinate to move to, 0 is at the top of the screen + @type x int + @type y int""" + assert(isinstance(x, types.IntType)) + assert(isinstance(x, types.IntType)) + self.xteProcess.stdin.write("mousemove %d %d\n" % (x, y)) + + def clickMouse(self, mouseclick): + """Click the mouse button. Nothing special, but it does the job. + @param mouseclick An object describing the mouse click and the number of + of times to click it. + @type mouseclick {@link pyrap.session.MouseClickType MouseClickType}""" + assert(isinstance(mouseclick, MouseClickType)) + for xte_command in mouseclick.commands: + self.xteProcess.stdin.write("%s\n" % (xte_command)) + + + def _findOpenDisplay(self, display=50): + """Look for an open X11 Display. This method checks /tmp/.X[display]-lock, + starting at 50. + @param display The display number to start checking for. + @type display int + @returns Open Display Number + @rtype int""" + while os.access("/tmp/.X%d-lock" % (display), os.F_OK): + display += 1 + return display + + def getScreenShot(self): + """Get a screen shot of the X11 Screen. This is needed for several reasons, + most for internal use. The imagerec module needs screen shots to locate + images on the screen. The record application needs constant screen shots to + display what's going on so that the user can make the right actions for + recording. + @return Screen shot of the X11 Session + @rtype gtk.gdk.Pixbuf""" + sspb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.width, self.height) + sspb.get_from_drawable(self.root_window, self.default_colormap, 0, 0, 0, 0, self.width, self.height) + return sspb + + + def _cleanup(self): + """Registered in __init__ as a shutdown hook. X11Session needs to clean up + several things. At this point a lot of processes running, and temp files + have been created.""" + if os.environ.has_key("XAUTHORITY"): + subprocess.call(args=["xauth", "remove", self.newenviron['DISPLAY']]) + if self.xteProcess.poll() is None: + self.xteProcess.stdin.close() + if self.xteProcess.poll() is None: + os.kill(self.xteProcess.pid, signal.SIGTERM) + if self.XServerProcess.poll() is None: + os.kill(self.XServerProcess.pid, signal.SIGTERM) + os.unlink(self.xauth) + os.rmdir(self.tempdir) + Property changes on: pyrap/pyrap/session.py ___________________________________________________________________ Name: svn:executable + * Name: svn:mime-type + text/plain Name: svn:eol-style + native This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |