summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorValentin Hangan <valentinx.hangan@intel.com>2016-02-17 12:50:39 +0200
committerValentin Hangan <valentinx.hangan@intel.com>2016-02-17 12:50:39 +0200
commitdce7364ba85c12683234dc4841830d4ce23a488c (patch)
tree0e552e3c422d301fcdbdd5c1f37db4272c956c6a
downloadpoky-contrib-dce7364ba85c12683234dc4841830d4ce23a488c.tar.gz
poky-contrib-dce7364ba85c12683234dc4841830d4ce23a488c.tar.bz2
poky-contrib-dce7364ba85c12683234dc4841830d4ce23a488c.zip
Eclipse framework with dogtail-0.9.0vhangan/eclipse-framework
Framework is found in examples directrory. Added all directory structure for ease of use by GDC team. Signed-off-by Valentin Hangan <valentinx.hangan@intel.com>
-rw-r--r--COPYING340
-rw-r--r--HACKING11
-rw-r--r--INSTALL9
-rw-r--r--MANIFEST146
-rw-r--r--MANIFEST.in12
-rw-r--r--Makefile52
-rw-r--r--NEWS597
-rw-r--r--PKG-INFO13
-rw-r--r--README74
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/__init__.py16
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/config.py219
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/distro.py362
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/dump.py33
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/errors.py28
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/i18n.py281
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/logging.py218
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/path.py113
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/predicate.py443
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/procedural.py455
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/rawinput.py252
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/sessions.py231
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/tc.py230
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/tree.py1318
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/utils.py395
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/version.py56
-rw-r--r--build/lib.linux-x86_64-2.7/dogtail/wrapped.py33
-rw-r--r--build/lib/dogtail/__init__.py16
-rw-r--r--build/lib/dogtail/config.py219
-rw-r--r--build/lib/dogtail/distro.py362
-rw-r--r--build/lib/dogtail/dump.py33
-rw-r--r--build/lib/dogtail/errors.py28
-rw-r--r--build/lib/dogtail/i18n.py281
-rw-r--r--build/lib/dogtail/logging.py218
-rw-r--r--build/lib/dogtail/path.py113
-rw-r--r--build/lib/dogtail/predicate.py443
-rw-r--r--build/lib/dogtail/procedural.py455
-rw-r--r--build/lib/dogtail/rawinput.py252
-rw-r--r--build/lib/dogtail/sessions.py231
-rw-r--r--build/lib/dogtail/tc.py230
-rw-r--r--build/lib/dogtail/tree.py1318
-rw-r--r--build/lib/dogtail/utils.py395
-rw-r--r--build/lib/dogtail/version.py56
-rw-r--r--build/lib/dogtail/wrapped.py33
-rwxr-xr-xbuild/scripts-2.7/dogtail-detect-session66
-rwxr-xr-xbuild/scripts-2.7/dogtail-logout28
-rwxr-xr-xbuild/scripts-2.7/dogtail-run-headless79
-rwxr-xr-xbuild/scripts-2.7/dogtail-run-headless-next319
-rwxr-xr-xbuild/scripts-2.7/sniff798
-rw-r--r--dogtail.spec213
-rwxr-xr-xdogtail/__init__.py16
-rw-r--r--dogtail/__init__.pycbin0 -> 894 bytes
-rwxr-xr-xdogtail/config.py219
-rw-r--r--dogtail/config.pycbin0 -> 7630 bytes
-rwxr-xr-xdogtail/distro.py362
-rwxr-xr-xdogtail/dump.py33
-rwxr-xr-xdogtail/errors.py28
-rwxr-xr-xdogtail/i18n.py281
-rw-r--r--dogtail/i18n.pycbin0 -> 8647 bytes
-rwxr-xr-xdogtail/logging.py218
-rw-r--r--dogtail/logging.pycbin0 -> 6901 bytes
-rwxr-xr-xdogtail/path.py113
-rw-r--r--dogtail/path.pycbin0 -> 4199 bytes
-rwxr-xr-xdogtail/predicate.py443
-rw-r--r--dogtail/predicate.pycbin0 -> 21364 bytes
-rwxr-xr-xdogtail/procedural.py455
-rwxr-xr-xdogtail/rawinput.py252
-rw-r--r--dogtail/rawinput.pycbin0 -> 8943 bytes
-rwxr-xr-xdogtail/sessions.py231
-rwxr-xr-xdogtail/tc.py230
-rwxr-xr-xdogtail/tree.py1318
-rw-r--r--dogtail/tree.pycbin0 -> 47327 bytes
-rwxr-xr-xdogtail/utils.py395
-rw-r--r--dogtail/utils.pycbin0 -> 16856 bytes
-rwxr-xr-xdogtail/version.py56
-rwxr-xr-xdogtail/wrapped.py33
-rw-r--r--examples/README65
-rw-r--r--examples/__init__.py0
-rwxr-xr-xexamples/build_meta_ide.sh7
-rwxr-xr-xexamples/configVariables.py155
-rwxr-xr-xexamples/eclipse_automation_test.py5897
-rwxr-xr-xexamples/iniparser.py44
l---------examples/results-run_eclipsetests.py.log1
-rwxr-xr-xexamples/run_eclipsetests.py136
-rwxr-xr-xexamples/settings-eclipse.ini181
-rw-r--r--icons/dogtail-head-48.pngbin0 -> 2759 bytes
-rw-r--r--icons/dogtail-head.svg595
-rw-r--r--icons/dogtail-tail-48.pngbin0 -> 2462 bytes
-rw-r--r--icons/dogtail-tail.svg336
-rw-r--r--scripts/dogtail-detect-session66
-rw-r--r--scripts/dogtail-logout28
-rw-r--r--scripts/dogtail-run-headless79
-rw-r--r--scripts/dogtail-run-headless-next319
-rwxr-xr-xsetup.py93
-rw-r--r--sniff/icons/button.xpm29
-rw-r--r--sniff/icons/checkbutton.xpm33
-rw-r--r--sniff/icons/checkmenuitem.xpm28
-rw-r--r--sniff/icons/colorselection.xpm35
-rw-r--r--sniff/icons/combo.xpm34
-rw-r--r--sniff/icons/dialog.xpm38
-rw-r--r--sniff/icons/image.xpm39
-rw-r--r--sniff/icons/label.xpm30
-rw-r--r--sniff/icons/menubar.xpm34
-rw-r--r--sniff/icons/menuitem.xpm26
-rw-r--r--sniff/icons/notebook.xpm33
-rw-r--r--sniff/icons/scrolledwindow.xpm28
-rw-r--r--sniff/icons/spinbutton.xpm33
-rw-r--r--sniff/icons/statusbar.xpm34
-rw-r--r--sniff/icons/table.xpm31
-rw-r--r--sniff/icons/text.xpm32
-rw-r--r--sniff/icons/toolbar.xpm33
-rw-r--r--sniff/icons/tree.xpm34
-rw-r--r--sniff/icons/treeitem.xpm35
-rw-r--r--sniff/icons/unknown.xpm33
-rw-r--r--sniff/icons/viewport.xpm34
-rw-r--r--sniff/icons/vscrollbar.xpm33
-rw-r--r--sniff/icons/vseparator.xpm31
-rw-r--r--sniff/icons/window.xpm38
-rwxr-xr-xsniff/sniff798
-rw-r--r--sniff/sniff.desktop14
-rw-r--r--sniff/sniff.glade676
-rw-r--r--sniff/sniff.ui494
-rw-r--r--tests/gtkdemotest.py56
-rw-r--r--tests/test_config.py66
-rw-r--r--tests/test_logging.py77
-rw-r--r--tests/test_node.py570
-rw-r--r--tests/test_predicate.py192
-rw-r--r--tests/test_procedural.py213
-rw-r--r--tests/test_utils.py64
128 files changed, 28816 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000000..3912109b5c
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/HACKING b/HACKING
new file mode 100644
index 0000000000..5aacadf283
--- /dev/null
+++ b/HACKING
@@ -0,0 +1,11 @@
+= Notes on hacking on Dogtail =
+
+(this is a work-in-progress)
+
+
+== Formatting ==
+We use 4 spaces for indentation.
+
+== Python versions ==
+Currently the 0.8.x versions of dogtail target Python 2.7
+We might not be 3.x compatible yet.
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000000..8fb38c5e9f
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,9 @@
+Installation on Red-Hat-like systems:
+ make rpm
+ rpm -Uvh ./dist/dogtail-*.noarch.rpm
+
+Installation on other systems:
+ ./setup.py install --prefix=/usr
+
+ (or /usr/local, $HOME/local, etc...)
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000000..c8696454d5
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,146 @@
+# file GENERATED by distutils, do NOT edit
+COPYING
+HACKING
+INSTALL
+MANIFEST.in
+Makefile
+NEWS
+README
+dogtail.spec
+setup.py
+dogtail/__init__.py
+dogtail/config.py
+dogtail/distro.py
+dogtail/dump.py
+dogtail/errors.py
+dogtail/i18n.py
+dogtail/logging.py
+dogtail/path.py
+dogtail/predicate.py
+dogtail/procedural.py
+dogtail/rawinput.py
+dogtail/sessions.py
+dogtail/tc.py
+dogtail/tree.py
+dogtail/utils.py
+dogtail/version.py
+dogtail/wrapped.py
+examples/buildDirEclipse.py
+examples/commonFunctionEclipse.py
+examples/eclipseCommon.py
+examples/eclipseKeplerCAnsiProject.py
+examples/eclipseKeplerCGtkProject.py
+examples/eclipseKeplerCppAutoProject.py
+examples/eclipseKeplerPreferences.py
+examples/eclipseKeplerPreferences.txt
+examples/eclipseKeplerQEMU.py
+examples/eclipseKeplerSSHConnection.py
+examples/eclipseKeplerYoctoBSP.py
+examples/eclipseKeplerYoctoBSP1.py
+examples/eclipseYoctoBSP.txt
+examples/eclipseYoctoBSP1.txt
+examples/eclipseclick.py
+examples/gedit-test-utf8-procedural-api.py
+examples/gedit-test-utf8-tree-api.py
+examples/iniparser.py
+examples/logg-ubuntu-original.py
+examples/logg.py
+examples/logg.txt
+examples/runEclipse.py
+examples/startEclipse.py
+examples/testgedit.py
+examples/data/10b.png
+examples/data/10w.png
+examples/data/20b.png
+examples/data/20w.png
+examples/data/UTF-8-demo.txt
+examples/data/sample.cfg
+examples/hob/addLayer.py
+examples/hob/add_remove.py
+examples/hob/appstartup.py
+examples/hob/base.py
+examples/hob/baseImageSelection.py
+examples/hob/broughtInBy.py
+examples/hob/buildDir.py
+examples/hob/buildImage.py
+examples/hob/buildStopBuild.py
+examples/hob/buildToolchain.py
+examples/hob/changeDistro.py
+examples/hob/debBuild.py
+examples/hob/emgdDriver.py
+examples/hob/extraParameters.py
+examples/hob/extraParameters2.py
+examples/hob/filechooser-stress-test.py
+examples/hob/finish.py
+examples/hob/finishBuild.py
+examples/hob/i18n-test.py
+examples/hob/imagesShown.py
+examples/hob/ipkBuild.py
+examples/hob/multiplePackageBuild.py
+examples/hob/no-help-at-all.py
+examples/hob/noNative.py
+examples/hob/nonGPLv3Build.py
+examples/hob/nrThreads.py
+examples/hob/openLog.py
+examples/hob/packageSizeShown.py
+examples/hob/pyperclip.py
+examples/hob/recipeReloadBaseImage.py
+examples/hob/recipeReloadMachine.py
+examples/hob/recipesStop.py
+examples/hob/removeExtraParam.py
+examples/hob/rpmBuild.py
+examples/hob/runFullpass.py
+examples/hob/runWeekly.py
+examples/hob/startHob.py
+examples/hob/stopHob.py
+examples/hob/stopHob.txt
+examples/hob/taskReloadBaseImage.py
+examples/hob/test-events.py
+examples/hob/test.py
+examples/hob/testFinish.py
+examples/hob/testStopStart.py
+examples/test/eclipseKeplerYoctoBSP.py
+examples/test/eclipseYoctoBSP.txt
+icons/dogtail-head-48.png
+icons/dogtail-head.svg
+icons/dogtail-tail-48.png
+icons/dogtail-tail.svg
+scripts/dogtail-detect-session
+scripts/dogtail-logout
+scripts/dogtail-run-headless
+scripts/dogtail-run-headless-next
+sniff/sniff
+sniff/sniff.desktop
+sniff/sniff.glade
+sniff/sniff.ui
+sniff/icons/button.xpm
+sniff/icons/checkbutton.xpm
+sniff/icons/checkmenuitem.xpm
+sniff/icons/colorselection.xpm
+sniff/icons/combo.xpm
+sniff/icons/dialog.xpm
+sniff/icons/image.xpm
+sniff/icons/label.xpm
+sniff/icons/menubar.xpm
+sniff/icons/menuitem.xpm
+sniff/icons/notebook.xpm
+sniff/icons/scrolledwindow.xpm
+sniff/icons/spinbutton.xpm
+sniff/icons/statusbar.xpm
+sniff/icons/table.xpm
+sniff/icons/text.xpm
+sniff/icons/toolbar.xpm
+sniff/icons/tree.xpm
+sniff/icons/treeitem.xpm
+sniff/icons/unknown.xpm
+sniff/icons/viewport.xpm
+sniff/icons/vscrollbar.xpm
+sniff/icons/vseparator.xpm
+sniff/icons/window.xpm
+tests/gtkdemotest.py
+tests/test_config.py
+tests/test_logging.py
+tests/test_node.py
+tests/test_predicate.py
+tests/test_procedural.py
+tests/test_utils.py
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000000..45afd7c085
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,12 @@
+include COPYING
+include MANIFEST.in
+include Makefile
+include HACKING
+include INSTALL
+include NEWS
+include dogtail.spec
+recursive-include docs *
+recursive-include scripts *
+include sniff/sniff.glade sniff/sniff.desktop sniff/icons/*.xpm
+recursive-include examples *.py *.txt *.png *.cfg
+include icons/*.svg icons/*.png
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000..f47423bc0e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,52 @@
+# dogtail *development* Makefile
+
+all:
+ python setup.py build
+
+install:
+ python setup.py install --root=$(DESTDIR)
+
+clean:
+ rm -rf api_docs/
+ python setup.py clean
+ rm -f MANIFEST
+ rm -rf build dist
+
+ find . -name '*.pyc' -exec rm {} \;
+
+# Dollar signs must be escaped with dollar signs in variables.
+export camelCAPS='[a-z_][a-zA-Z0-9_]*$$'
+export StudlyCaps='[a-zA-Z_][a-zA-Z0-9_]*$$'
+
+check:
+ pylint --indent-string=" " --class-rgx=${StudlyCaps} --function-rgx=${camelCAPS} --method-rgx=${camelCAPS} --variable-rgx=${camelCAPS} --argument-rgx=${camelCaps} dogtail sniff/sniff examples/*.py recorder/dogtail-recorder scripts/*.py
+
+tarball:
+ python setup.py sdist
+
+rpm: tarball
+ # Build using the custom rpmrc in the rpms/ sub-dir
+ rpmbuild -tb dist/dogtail-*.tar.gz
+ # Move the source and binary RPMs to dist/
+ mv ~/rpmbuild/RPMS/noarch/* dist/
+
+srpm: rpm_prep
+ # Build using the custom rpmrc in the rpms/ sub-dir
+ rpmbuild --rcfile /usr/lib/rpm/rpmrc:/usr/lib/rpm/redhat/rpmrc:`pwd`/rpms/tmp.rpmrc -ts dist/dogtail-*.tar.gz
+ # Move the source and binary RPMs to dist/
+ mv rpms/SRPMS/* dist/
+ rm -rf rpms/
+
+apidocs: apidocs_html apidocs_pdf
+
+apidocs_html:
+ epydoc --html --config epydoc.conf
+
+apidocs_pdf:
+ epydoc --pdf --config epydoc.conf
+ mv api_docs/api.pdf api_docs/dogtail.pdf
+
+update_apidocs: apidocs
+ # Sadly, I'm still the only one who can update the API docs.
+ ssh zmc@fedorapeople.org rm -rf \~/public_html/dogtail/epydoc/*
+ scp api_docs/*.{html,css,png,pdf} zmc@fedorapeople.org:~/public_html/dogtail/epydoc/
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000000..8365b12360
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,597 @@
+dogtail 0.9.0
+=============
+Over the last year we've made a good number of fixes, added some new features
+and got overall stability to the point where we can make a new 'major minor'
+release. With 0.9 we're finally getting only a step away from what we'd like
+to get done with 1.0.
+
+
+ * Added a fix for situations when a previously crashed application
+ still registered with at-spi blocks search ('Broken' session problem)
+ Fixed for root.application(), root.applications() and for whenever
+ GeneralPredicate is used (.child()). Also done for Sniff. RHBZ 972257
+
+ * Added a support for gnome-continuous build-api (#36)
+
+ * Added a direct lambda support in the findChildren method, that allows
+ using non-predicate lambdas to create search properties. These can be
+ all pure dogtail Node properties (i.e. showing), not limiting to just
+ pyatspi ones as is when using Predicate.
+
+ * Added a GnomeShell 'helper' class into utils module allowing to work
+ with the new top-panel application menu.
+
+ * With GTK3.10+ Gdk changes, needed to tune the keyNameToKeyCode function
+ so that it provides valid keySym in as many cases possible - with both
+ pre-GTK3.10 as well as GTK3.11+ having the same behavior in typeText and
+ pressKey
+
+ * Sniff ui update: the info pane now stays down on resizing
+
+ * Got rid of some deprecated GObject calls
+
+ * Several updates to the dogtail-run-headless-next
+ - Any kind of session can now be used in any combination with both gdm/kdm
+ --session can be used to specify any xsession desktop file, 'kde' defaults
+ to 'kde-plasma' too keep pre-updated compatibility; default is now the previous user session
+ --dm - new param to pick either gdm (default) or kdm
+ --session-binary - new param to specify any in-session binary to grab script env from
+ - to be used with non gnome/kde session (lxde, ubuntu)
+ - do not confuse with session execs like gnome-session and starkde, that may
+ actually exit after session loadup; it should be everpresent (kwin, shell...)
+ - Added a --restart switch to force restart the previously running (often left-over) session before proceeding
+ - More logging of script execution (start/stop/PID), and some cleanups
+
+ * Trap GErrors in findAllDescendants and retry if it occurs: This fixes crashes in findChildren if elements are being added/removed quickly
+
+ * Unicode handling updates:
+ - Make sure safeDecode is not skipping any strings
+ - Don't crash when logging message contains unicode
+
+ * Error Handling for Attribute Error
+
+ * Fixed utils.run issue when having whitespace containing binary by
+ using shlex shell-like parsing split.
+
+ * Removed an always true if condition in i18n
+
+ * Log child description when clicking. The log will now print the element description when it will be clicked
+
+ * Trap TypeError in findAllDescendants: Sometimes pytaspi returns None as a Node if children are being removed too fast.
+ We should retry search in this case
+
+ * Extended the default TypingDelay from 0.075 to 0.1 seconds (helps working with certain high unicode charactes)
+
+ * Fixed an bug in sniff discovered by jmolet that made sniff crash when
+ run from /bin/sniff not /usr/bin/sniff
+
+ * Generate a better script method call for labels
+
+ * Fix for failing 'focus.application.node' seting
+
+ * Derive all classes from object explicitely
+
+ * Changed icons used in sniff that got moved to the gnome-icon-theme-legacy
+ recently for the ones from non-legacy package.
+
+ * Unified the 'isSth' vs. 'sth' properties for only 'sth' - added 'selected', but also introduced
+ 'isChecked' property for 0.7.x compatibility
+
+ * Added instructions on how to enable accessibility into utils.bailBecauseA11yIsDisabled()
+
+ * Added a 'retry' option to the tree.root.application()
+
+ * Fixed the concurrent creation of sniff_refresh lock from both tree and procedural
+
+ * Switched to use environ value to get user in config. os.getlogin breaks when run scheduled from a testing system with no terminal.
+
+ * Patched the mouse related rawinput methods to prevent using negative coordinates
+
+ * Updated and added more unittests
+
+dogtail 0.8.2
+=============
+Second update to the 0.8 series containing several fixes and improvements
+
+ * Added the dogtail-run-headless-next to replace the dogtail-run-headless in future.
+ -next, uses a diplay manager (kdm or gdm) for session management, should be used instead of older
+ headless on systemd systems already
+
+ * Unittests vastly improved and updated
+
+ * Fixed a missing reset of FocusWindow to None on application refocus
+
+ * Fixed the dogtail-logout to use gnome-shell instead of old gnome-panel
+
+dogtail 0.8.1
+=============
+A first update to the new GNOME3 / KDE4 compatible release containing several fixes:
+
+ * Sniff's autorefresh made togglable, to help avoid collisions with potential
+ running dogtail scripts. Sniff checks at startup whether some script is running
+ and sets up the autorefresh off if it is.
+
+ * Added a locking mechanism into utils that can be used to solve situations when
+ multiple dogtail processes/thread are running (applied in tree/procedural and sniff
+ already)
+
+ * Removed the deprecated dependency on CORBA
+
+ * Icons no longer use absolute pathing in sniff (thx roignac)
+
+ * Deprecated .has_key() operator replaced with 'in'.
+
+ * Removed .svg inside sniff's .desktop file
+
+dogtail 0.8.0
+=============
+Finally a big release updating dogtail after more than 2 years ! Dogtail
+is developed and maintained now again.
+
+Simply put, ported to be compatible with the new GNOME 3 and to work well in major
+GNOME 3 (GTK3) distributions.
+
+We'd like to make the 0.8.x version onward the GNOME 3 compatible branch usable
+for Fedora, RHEL7 and others, while keeping 0.7.x releases for fixes in older GNOME2
+systems.
+
+--- What was done ---
+
+Notably, Sniff's UI needed to ported completely to GTK3, yet there are
+also several places in dogtail 'itself' that needed to be rewritten to go ahead
+with the new technologies. Those were all the modules where the old pygtk was
+used for various reasons (rawinput, utils, tree...). Incompatibilites were also
+present due to the new version of pyatspi (notably with the doAction method).
+
+Release highlights:
+
+* A great number of fixes everywhere related to GTK/GNOME/At-Spi updates
+
+* The dogtail-recorder was dropped for now, but might return in RC or 0.8.1
+
+* Thanks to the qt-at-spi project, dogtail now works out-of-the-box for QT!
+
+* Headless supports KDE sessions
+
+* Headless working with full 3D-Accelerated GNOME session (no fallback anymore)
+
+* Highlight used in sniff completely re-written and made toggleable (of by default)
+
+* Makefile updated, 'make run' builds packages in homedir/rpmbuld now
+
+* Several examples fixed for updated gedit
+
+* Added tree.isChild() convenience method
+
+* Renamed doAction to doActionNamed due to conflict with doAction inside
+ pyatspi namespace
+
+* Delay made configurable in absoluteMotion and relativeMotion functions
+
+* Changed at-spi activation to use DConf instead of GConf
+
+* A bit of code clean-up and modernization (using @property now etc.)
+
+* More error checking added
+
+* A couple of unit tests for Node added
+
+* Node.findChildren() updated to be faster
+
+
+dogtail 0.7.0
+=============
+
+This release has too many changes to list. Here are some of the major ones:
+
+* Dogtail has been ported to pyatspi. This means:
+ - Faster performance.
+ - Errors will be more clear.
+ - No more segfaulting or myserious crashes.
+
+* There is now useful generated API documentation:
+ http://fedorapeople.org/~zmc/dogtail/epydoc/
+
+* Inline documentation has been greatly improved.
+
+* Headless execution support was completely rewritten.
+
+* Sniff was completely rewritten. It is now:
+ - Faster.
+ - More reliable.
+ - Self-updating via AT-SPI events.
+ - Slightly prettier.
+ - Able to tell you about certain states and relations.
+
+* Image Comparison (TCImage) was rewritten. No more ImageMagick!
+
+Note: tests/Node.py's TestNodeAttributes is failing due to GNOME bugs #498557
+ and #498563
+
+
+dogtail 0.6.1
+=============
+
+Features:
+
+ * dogtail-recorder has a Play button now.
+
+ * dogtail-recorder now records right-clicks. That it didn't previously was an
+ oversight. Note that there is some inconsistency when dealing with context
+ menus, as the events often get fired in the wrong order.
+
+ * New logging mechanism:
+
+ - Debug output is now written to a file. To disable this, set
+ dogtail.config.config.logDebugToFile to False.
+
+ - The procedural API defaults to not raising FocusErrors, and instead
+ issuing warnings via the debug logging mechanism (which prints to
+ standard out along with writing to a file). To re-enable FocusErrors, set
+ dogtail.config.config.fatalErrors to True.
+
+ * Brian Cameron submitted a patch to make dogtail.distro correctly detect that
+ it is running on Solaris.
+
+Bugfixes:
+
+ * dogtail-recorder no longer offers the broken Object-Oriented backend as a
+ choice. If it gets fixed, it will be offered again.
+
+ * dogtail-recorder can be properly exited by clicking on the window manager
+ close button. In fact, the Quit button was removed also.
+
+ * dogtail-recorder's syntax hilighting works again, thanks to a patch from
+ Dave Malcolm.
+
+ * Searching for nodes with parentheses in their names will no longer fail.
+
+ * The various pressKey() functions were blowing up on punctuation and
+ newlines. Fixed.
+
+ * Predicate subclasses in dogtail.predicate which were missing debugNames have
+ gotten them back.
+
+ * Warnings are no longer issued when dogtail encounters invalid Unicode.
+
+ * Where several of our example scripts were broken, Michal Babej noticed and
+ submitted patches fixing them.
+
+ * dogtail's tarball is 90% smaller! Some overly large test images were
+ replaced with very, very small ones that still do the job.
+
+
+dogtail 0.6.0
+=============
+
+Features:
+
+ * Keystroke recording support in dogtail-recorder:
+
+ - Normal keypresses (e.g. typing 'foo bar baz') will be recorded as
+ "type('foo bar baz')" in the procedural API. dogtail.tree.Node has a new
+ method, typeText(), which is called by dogtail.procedural.type().
+
+ - Nonprintable keypresses (e.g. typing Control-Shift-c) will be recorded as
+ "keyCombo('<Control><Shift>c')" in the procedural API. dogtail.tree.Node
+ has a new method, keyCombo(), which is called by
+ dogtail.procedural.keyCombo().
+
+ - The now-unnecessary 'Input Text' button has been removed.
+
+ * dogtail.tree.Node has new attributes and a new method:
+
+ - grabFocus(): Attempts to cause the node to grab the keyboard focus.
+
+ - focusable: Whether the node is able to have keyboard focus.
+
+ - focused: Whether the node currently has keyboard focus.
+
+ - checked: Whether the node is a checkbox that is currently checked.
+
+ * Dogtail now checks if accessibility is enabled before it does anything.
+ sniff and dogtail-recorder will prompt to enable it via a dialog, and
+ scripts themselves will just refuse to run.
+
+ * Dogtail no longer uses the IconLogger (in the notification area) by default.
+ Set dogtail.config.config.useIconLogger to True to reenable it.
+
+ * dogtail.utils.screenshot() was rewritten to use GDK instead of ImageMagick.
+
+Bugfixes:
+
+ * Dogtail is now much more robust in dealing with possibly-invalid Unicode
+ strings passed to it via AT-SPI. (GNOME bug #354515)
+
+ * Dogtail will not crash when it receives an SpiException caused by a CORBA
+ COMM_FAILURE, unless the source of that error is the application under test.
+
+ * While exceptions occuring inside callbacks used in dogtail-recorder will
+ still not properly be raised, they are now printed to standard out.
+
+ * SuSe support in dogtail.distro is fixed. (GNOME bug #353601)
+
+ * sniff and dogtail-recorder will now find their glade files, even if they're
+ installed into a nonstandard prefix. (GNOME bug #353719, #353731)
+
+ * dogtail-run-headless had a few bugs fixed.
+
+
+dogtail 0.5.2
+=============
+
+Features:
+
+ * A much-improved recorder:
+
+ - A procedural script writer, which is now default.
+
+ - An 'Input Text' feature, which allows you to set the text attribute
+ of any visible Node via the recorder. Coming soon is proper keystroke
+ recording.
+
+ - The script view now uses syntax hilighting where available. (GNOME bug
+ #345374)
+
+ * A completely-rewritten dogtail-run-headless, which now offers two session
+ types: minimal GNOME and metacity. The syntax has changed, so use
+ 'dogtail-run-headless --help' to get started. (GNOME bug #320548)
+
+ * All 'name' arguments in dogtail.tree and dogtail.procedural now accept
+ regular expressions.
+
+ * Support for AccessibleSelections, providing a clean way to select page
+ tabs and combo box items. (GNOME bug #336562)
+
+ * Support for building in a jhbuild environment. (GNOME bug #318535)
+
+ * Far more extensive unit tests for dogtail.tree.Node.
+
+Bugfixes:
+
+ * The source is now indented with spaces and not tabs! :) (GNOME bug #318833)
+
+ * Pyspi and dogtail no longer mysteriously stop working. (GNOME bug #321273)
+
+ * Sniff no longer crashes when an application being poked disappears. (GNOME
+ bug #318135)
+
+
+dogtail 0.5.1
+=============
+
+Features:
+
+ * An improved build system for RPMs.
+
+ * dogtail.procedural's FocusErrors are now more informative.
+
+Bugfixes:
+
+ * examples/recorder.py was broken; a bug in dogtail.predicate.makeCamel()
+ was breaking the predicates' makeScriptVariableName() calls. It's fixed.
+
+
+dogtail 0.5.0
+=============
+
+Features:
+
+ * Automatic runtime translation into the current locale using the application
+ under test's own translations. See
+ examples/gedit-test-utf8-procedural-api.py for an example.
+
+ * dogtail.distro.packageDb gained new methods: getFiles(), getMoFiles(), and
+ getDependencies().
+
+ * dogtail.distro.packageDb now has support for JHBuild environments.
+
+ * dogtail.distro has gained exceptions:
+
+ - PackageNotFoundError: raised when the specified package is not found.
+
+ - DistributionNotSupportedError: raised when support for the current
+ distribution is not currently implemented in dogtail.distro. This does
+ not mean that dogtail will not work at all; only that
+ distribution-specific functionality, like automatic runtime translation,
+ will not.
+
+ * dogtail.errors is a new module for very general exceptions, such as
+ DependencyNotFoundError, which is raised when the use of a feature that
+ depends on some missing piece of software is attempted.
+
+ * sniff will now cause the selected node to blink onscreen.
+
+ * dogtail.tree has a new Node subclass, Link. Link nodes have a roleName of
+ 'hyper link' and an action called 'jump'. Their main purpose is to allow
+ us to follow links in web pages.
+
+ * dogtail.tree.Node has new properties and methods:
+
+ - position: the onscreen position of the node, if it wraps an
+ atspi.Component.
+
+ - size: the onscreen size of the node, if it wraps an atspi.Component.
+
+ - blink(): causes the node to blink onscreen.
+
+ - grabFocus(): causes the node to grab the keyboard focus, if it wraps an
+ atspi.Component.
+
+ - rawClick(): synthesizes a raw mouse click on the node, if it wraps an
+ atspi.Component.
+
+ - rawType(): synthesizes raw keyboard events to type text into the node, if
+ it wraps an atspi.Component.
+
+ * dogtail.procedural has new helper methods:
+
+ - menu(): executes the 'menu' action on the focused widget.
+
+ - focus.frame(): a shortcut to focus.widget(roleName='frame' ...)
+
+ - focus.window(): a shortcut to focus.widget(roleName='window' ...)
+
+ * dogtail.procedural.click() can now synthesize raw mouse events to perform
+ "raw" clicks.
+
+ * dogtail.rawinput has gained new methods:
+
+ - pressKey(): synthesizes a raw keyboard event, pressing the key whose
+ name is specified.
+
+ - typeText(): types a specified string, one key at a time, using raw
+ keyboard events.
+
+ * dogtail.config was rewritten.
+
+ * dogtail.config.config has new parameters:
+
+ - ensureSensitivity: controls whether a NotSensitiveError is raised when
+ an attempt is made to execute an action belonging to a Node instance
+ that is not sensitive, or if a warning is simply printed instead (the
+ default).
+
+ - debugTranslation: Controls whether details of the autotranslation
+ process will be outputted to the debug logger.
+
+ * dogtail.config now creates its scratchDir, dataDir, and logDir as soon as
+ the values are set.
+
+ * dogtail.utils.screenshot(), by default, whether using a custom filename or
+ not, now appends a timestamp to the filename. That may be disabled by
+ passing 'timeStamp = False' to the method. For this reason, it also prints
+ the full path to the screenshot.
+
+ * dogtail.logging.TimeStamp.fileStamp()'s format has changed, to
+ filename_YYYYMMDD-hhmmss or just filename_YYYYMMDD, controlled by the new
+ optional argument 'addTime'.
+
+ * dogtail prints the full path to any logfiles it creates.
+
+ * dogtail-run-headless will now turn accessibility on before executing the
+ script under test, and also restore the old value after the the script has
+ finished executing (GNOME bug #320548).
+
+Bugfixes:
+
+ * sniff will correctly show the text of a non-editable text node again
+ (GNOME bug #321564).
+
+ * dogtail.tree.screenshot() was mostly rewritten, and no longer breaks when
+ given a custom filename.
+
+ * dogtail.procedural no longer causes PyDoc to fail miserably (or at all).
+
+ * dogtail will no longer warn about missing wnck bindings, because they're
+ not actually used anyway.
+
+ * dogtail.procedural.focus.dialog() no longer searches recursively (GNOME
+ bug #321624).
+
+ * dogtail.procedural.focus.widget() and its shortcut functions now properly
+ raise a FocusError if their search fails.
+
+ * dogtail.tc.TCString.compare() will now fail properly again, thanks to
+ Muktha Narayan (GNOME bug #321151).
+
+ * dogtail.tc.TCImage.compare() now works with the newest version of
+ ImageMagick. That version had changed its behavior, breaking the function.
+ (GNOME bug #321431).
+
+ * Attempting to instantiate a dogtail.tc.TCImage object while ImageMagick is
+ not installed will now give a comprehensible error message, thanks to the
+ new dogtail.errors.DependencyNotFoundError exception.
+
+ * Calling dogtail.utils.screenshot() while ImageMagick is not installed will
+ also raise a dogtail.errors.DependencyNotFoundError.
+
+ * Attempting to execute an action belonging to a Node instance that isn't
+ sensitive no longer fails by default, but prints a warning. This behavior
+ is controlled by the ensureSensitivity configuration parameter.
+
+ * dogtail-run-headless now correctly returns the exit code of the script
+ being executed (GNOME bug #320535).
+
+ * dogtail.distro's APT support implementation now only calls
+ apt_pkg.GetCache() once, improving performance significantly when multiple
+ calls to packageDb.get{Version,Dependencies}() are needed.
+
+ * Building RPM packages from the supplied dogtail.spec works again on distros
+ that are as old or older than RHEL4.
+
+ * Building RPM packages with 'make rpm' works again without root privileges.
+
+API breaks:
+
+ * Action execution via dogtail.tree.Node instances was forced to change
+ interfaces from the "node.open()" model to a "node.doAction('open')"
+ model. To retain backward compatibility with the application wrappers, the
+ "click" action is still available as "node.click()". The
+ dogtail.procedural interface was able to remain unchanged.
+
+ * dogtail.config was rewritten to be more robust, and its interface was
+ changed slightly. To port, First, remove any instantiations of the old
+ Config class, and then make the following parameter replacements:
+
+ - Config => config
+ - logdir => logDir
+ - scratch => scratchDir
+ - data => dataDir
+
+
+dogtail 0.4.3
+=============
+
+Features:
+ * Headless support has been added. Use dogtail-run-headless to spawn an X
+ server, a minimal "Nat/Jeff" GNOME session, and execute a given dogtail
+ script before exiting. The dogtail-detect-session script detects a running
+ GNOME session, but KDE support can be dropped in once KDE has accessibility
+ support.
+
+ * dogtail now uses a notification icon while it is running. Its tooltip is
+ equal to the last message relayed by the debug logger. Most of the code
+ was taken from jhbuild.
+
+ * Sniff, the AT-SPI browser that uses the dogtail libraries, has gained icons
+ for both applications and specific widgets (based on the role name, taken
+ from at-poke) to make browsing easier.
+
+ * Sniff also gained a proper dogtail icon.
+
+ * The procedural API has been given more convenience functions for 'focusing'
+ different types of widgets.
+
+ * The procedural API has also been given an openItem() method, which works
+ just like click(), for opening icons on nautilus' desktop.
+
+ * dogtail can now detect Gentoo and Conary distributions, thanks to Brent
+ Smith and Tim Gerla, respectively.
+
+ * The Node class in dogtail.tree now exports the 'role' attribute in addition
+ to the 'role name'.
+
+ * Wrappers for gcalctool and yelp were added, thanks to Dave Malcolm.
+
+ * A wrapper for gedit was added, thanks to Paolo Borelli.
+
+ * New examples for Evolution, gcalctool and the GTK file chooser were added,
+ thanks to Dave Malcolm.
+
+ * Dave Malcolm also enhanced the Evolution wrapper, adding support for
+ creating new meetings.
+
+ * A new example that generates fake documentation for a given application has
+ been added, thanks to Dave Malcolm.
+
+ * dogtail.spec has been added, thanks to Jeremy Katz, to improve the quality
+ of the RPM packages.
+
+Bugfixes:
+ * several examples had not been updated to reflect API changes before the
+ last release. This has been corrected.
+
+ * The epiphany wrapper now uses the package name 'epiphany-browser' on
+ Debian-based distributions to detect the package version, thanks to Andrew
+ Beresford.
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000000..d683fe8fbf
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,13 @@
+Metadata-Version: 1.0
+Name: dogtail
+Version: 0.9.0
+Summary: GUI test tool and automation framework that uses Accessibility (a11y) technologies to communicate with desktop applications.
+Home-page: http://dogtail.fedorahosted.org/
+Author: Zack Cerza <zcerza@redhat.com>,
+Ed Rousseau <rousseau@redhat.com>,
+David Malcolm <dmalcolm@redhat.com>
+Vitezslav Humpa <vhumpa@redhat.com>
+Author-email: dogtail-list@gnome.org
+License: UNKNOWN
+Description: UNKNOWN
+Platform: UNKNOWN
diff --git a/README b/README
new file mode 100644
index 0000000000..1eb57cd606
--- /dev/null
+++ b/README
@@ -0,0 +1,74 @@
+dogtail is a GUI test tool and automation framework written in Python. It uses Accessibility (a11y) technologies to communicate with desktop applications. dogtail scripts are written in Python and executed like any other Python program.
+
+
+News
+====
+
+See NEWS file.
+
+
+Installation
+============
+
+See INSTALL file.
+
+
+Dependencies
+============
+
+Python bindings for your distribution, e.g. python-apt or rpm-python
+
+PyGObject and GNOME-Python
+
+Applications to test, e.g. from the GNOME desktop:
+ http://gnome.org/
+
+Xvfb and xinit:
+ http://xorg.freedesktop.org/
+
+Using
+=====
+
+Currently GNOME and GTK+ applications are supported. Thanks to qt-at-spi
+KDE4 and QT applications are now available too.
+
+First, enable accessibility support in your GNOME session with:
+ gsettings set org.gnome.desktop.interface toolkit-accessibility true
+This only affects newly-started applications, so you may want to log out and
+log back in again.
+
+Then, look at some of the example scripts. Run them, tweak them, write your own.
+
+I suggest starting with gedit-test-utf8-procedural-api.py, as it's updated the
+most often.
+
+If you are using KDE instead, install the 'qt-at-spi' QT plugin and make sure
+you QT_ACCESSIBILITY set to 1 throughout your environment (you can put
+'export QT_ACCESSIBILITY=1' to your profile file). QT accessibility should
+be stable from QT 4.8.3 onward.
+
+Bugs
+====
+
+Please report any bugs at:
+ https://fedorahosted.org/dogtail/newticket
+
+
+Contact
+=======
+
+Website:
+ http://dogtail.fedorahosted.org/
+
+API Documentation:
+ http://fedorapeople.org/~vhumpa/dogtail/epydoc/
+
+IRC:
+ #dogtail on irc.freenode.net
+
+Mailing list for users:
+ dogtail-list@gnome.org
+
+Mailing list for developers:
+ dogtail-devel-list@gnome.org
+
diff --git a/build/lib.linux-x86_64-2.7/dogtail/__init__.py b/build/lib.linux-x86_64-2.7/dogtail/__init__.py
new file mode 100644
index 0000000000..76a4a13fcb
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/__init__.py
@@ -0,0 +1,16 @@
+# -*- coding: UTF-8 -*-
+"""
+GUI test tool and automation framework that uses Accessibility (a11y) technologies to communicate with desktop applications.
+
+Authors: Zack Cerza <zcerza@redhat.com>, Ed Rousseau <rousseau@redhat.com>, David Malcolm <dmalcolm@redhat.com>, Vita Humpa <vhumpa@redhat.com>
+"""
+
+__author__ = """Zack Cerza <zcerza@redhat.com>,
+Ed Rousseau <rousseau@redhat.com>,
+David Malcolm <dmalcolm@redhat.com>,
+Vita Humpa <vhumpa@redhat.com>"""
+__version__ = "0.9.0"
+__copyright__ = "Copyright © 2005-2014 Red Hat, Inc."
+__license__ = "GPL"
+__all__ = ("config", "predicate",
+ "procedural", "tc", "tree", "utils", "errors")
diff --git a/build/lib.linux-x86_64-2.7/dogtail/config.py b/build/lib.linux-x86_64-2.7/dogtail/config.py
new file mode 100644
index 0000000000..82e197cf52
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/config.py
@@ -0,0 +1,219 @@
+"""
+The configuration module.
+"""
+__author__ = "Zack Cerza <zcerza@redhat.com>, David Malcolm <dmalcolm@redhat.com>"
+
+import os
+import sys
+import locale
+
+
+def _userTmpDir(baseName):
+ # i.e. /tmp/dogtail-foo
+ return '-'.join(('/'.join(('/tmp', baseName)), os.environ['USER']))
+
+
+class _Config(object):
+
+ """
+ Contains configuration parameters for the dogtail run.
+
+ scratchDir(str):
+ Directory where things like screenshots are stored.
+
+ dataDir(str):
+ Directory where related data files are located.
+
+ logDir(str):
+ Directory where dogtail.tc.TC*-generated logs are stored.
+
+ scriptName(str) [Read-Only]:
+ The name of the script being run.
+
+ encoding(str)
+ The encoding for text, used by dogtail.tc.TCString .
+
+ actionDelay(float):
+ The delay after an action is executed.
+
+ typingDelay(float):
+ The delay after a character is typed on the keyboard.
+
+ runInterval(float):
+ The interval at which dogtail.utils.run() and dogtail.procedural.run()
+ check to see if the application has started up.
+
+ runTimeout(int):
+ The timeout after which dogtail.utils.run() and dogtail.procedural.run()
+ give up on looking for the newly-started application.
+
+ searchBackoffDuration (float):
+ Time in seconds for which to delay when a search fails.
+
+ searchWarningThreshold (int):
+ Number of retries before logging the individual attempts at a search.
+
+ searchCutoffCount (int):
+ Number of times to retry when a search fails.
+
+ defaultDelay (float):
+ Default time in seconds to sleep when delaying.
+
+ childrenLimit (int):
+ When there are a very large number of children of a node, only return
+ this many, starting with the first.
+
+ debugSearching (boolean):
+ Whether to write info on search backoff and retry to the debug log.
+
+ debugSleep (boolean):
+ Whether to log whenever we sleep to the debug log.
+
+ debugSearchPaths (boolean):
+ Whether we should write out debug info when running the SearchPath
+ routines.
+
+ absoluteNodePaths (boolean):
+ Whether we should identify nodes in the logs with long 'abcolute paths', or
+ merely with a short 'relative path'. FIXME: give examples
+
+ ensureSensitivity (boolean):
+ Should we check that ui nodes are sensitive (not 'greyed out') before
+ performing actions on them? If this is True (the default) it will raise
+ an exception if this happens. Can set to False as a workaround for apps
+ and toolkits that don't report sensitivity properly.
+
+ debugTranslation (boolean):
+ Whether we should write out debug information from the translation/i18n
+ subsystem.
+
+ blinkOnActions (boolean):
+ Whether we should blink a rectangle around a Node when an action is
+ performed on it.
+
+ fatalErrors (boolean):
+ Whether errors encountered in dogtail.procedural should be considered
+ fatal. If True, exceptions will be raised. If False, warnings will be
+ passed to the debug logger.
+
+ checkForA11y (boolean):
+ Whether to check if accessibility is enabled. If not, just assume it is
+ (default True).
+
+ logDebugToFile (boolean):
+ Whether to write debug output to a log file.
+
+ logDebugToStdOut (boolean):
+ Whether to print log output to console or not (default True).
+ """
+ @property
+ def scriptName(self):
+ return os.path.basename(sys.argv[0]).replace('.py', '')
+
+ @property
+ def encoding(self):
+ return locale.getpreferredencoding().lower()
+
+ defaults = {
+ # Storage
+ 'scratchDir': '/'.join((_userTmpDir('dogtail'), '')),
+ 'dataDir': '/'.join((_userTmpDir('dogtail'), 'data', '')),
+ 'logDir': '/'.join((_userTmpDir('dogtail'), 'logs', '')),
+ 'scriptName': scriptName.fget(None),
+ 'encoding': encoding.fget(None),
+ 'configFile': None,
+ 'baseFile': None,
+
+ # Timing and Limits
+ 'actionDelay': 1.0,
+ 'typingDelay': 0.1,
+ 'runInterval': 0.5,
+ 'runTimeout': 30,
+ 'searchBackoffDuration': 0.5,
+ 'searchWarningThreshold': 3,
+ 'searchCutoffCount': 20,
+ 'defaultDelay': 0.5,
+ 'childrenLimit': 100,
+
+ # Debug
+ 'debugSearching': False,
+ 'debugSleep': False,
+ 'debugSearchPaths': False,
+ 'logDebugToStdOut': True,
+ 'absoluteNodePaths': False,
+ 'ensureSensitivity': False,
+ 'debugTranslation': False,
+ 'blinkOnActions': False,
+ 'fatalErrors': False,
+ 'checkForA11y': True,
+
+ # Logging
+ 'logDebugToFile': True
+ }
+
+ options = {}
+
+ invalidValue = "__INVALID__"
+
+ def __init__(self):
+ _Config.__createDir(_Config.defaults['scratchDir'])
+ _Config.__createDir(_Config.defaults['logDir'])
+ _Config.__createDir(_Config.defaults['dataDir'])
+
+ def __setattr__(self, name, value):
+ if name not in config.defaults:
+ raise AttributeError(name + " is not a valid option.")
+
+ elif _Config.defaults[name] != value or \
+ _Config.options.get(name, _Config.invalidValue) != value:
+ if 'Dir' in name:
+ _Config.__createDir(value)
+ if value[-1] != os.path.sep:
+ value = value + os.path.sep
+ elif name == 'logDebugToFile':
+ import logging
+ logging.debugLogger = logging.Logger('debug', value)
+ _Config.options[name] = value
+
+ def __getattr__(self, name):
+ try:
+ return _Config.options[name]
+ except KeyError:
+ try:
+ return _Config.defaults[name]
+ except KeyError:
+ raise AttributeError("%s is not a valid option." % name)
+
+ def __createDir(cls, dirName, perms=0o777):
+ """
+ Creates a directory (if it doesn't currently exist), creating any
+ parent directories it needs.
+
+ If perms is None, create with python's default permissions.
+ """
+ dirName = os.path.abspath(dirName)
+ # print "Checking for %s ..." % dirName,
+ if not os.path.isdir(dirName):
+ if perms:
+ umask = os.umask(0)
+ os.makedirs(dirName, perms)
+ os.umask(umask)
+ else:
+ # This is probably a dead code - no other functions call this without the permissions set
+ os.makedirs(dirName) # pragma: no cover
+ __createDir = classmethod(__createDir)
+
+ def load(self, dict):
+ """
+ Loads values from dict, preserving any options already set that are not overridden.
+ """
+ _Config.options.update(dict)
+
+ def reset(self):
+ """
+ Resets all settings to their defaults.
+ """
+ _Config.options = {}
+
+
+config = _Config()
diff --git a/build/lib.linux-x86_64-2.7/dogtail/distro.py b/build/lib.linux-x86_64-2.7/dogtail/distro.py
new file mode 100644
index 0000000000..6f70f19465
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/distro.py
@@ -0,0 +1,362 @@
+"""Handles differences between different distributions
+
+Authors: Dave Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>"""
+__author__ = "Dave Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>"
+
+import os
+import re
+from version import Version
+from logging import debugLogger as logger
+
+
+class DistributionNotSupportedError(Exception): # pragma: no cover
+
+ """
+ This distribution is not supported.
+ """
+ PATCH_MESSAGE = "Please send patches to dogtail-devel-list@gnome.org"
+
+ def __init__(self, distro):
+ self.distro = distro
+
+ def __str__(self):
+ return self.distro + ". " + DistributionNotSupportedError.PATCH_MESSAGE
+
+
+class PackageNotFoundError(Exception):
+
+ """
+ Error finding the requested package.
+ """
+ pass
+
+global packageDb
+global distro
+
+
+class PackageDb(object):
+
+ """
+ Class to abstract the details of whatever software package database is in
+ use (RPM, APT, etc)
+ """
+
+ def __init__(self):
+ self.prefix = '/usr'
+ self.localePrefixes = [self.prefix + '/share/locale']
+
+ def getVersion(self, packageName):
+ """
+ Method to get the version of an installed package as a Version
+ instance (or raise an exception if not found)
+
+ Note: does not know about distributions' internal revision numbers.
+ """
+ raise NotImplementedError
+
+ def getFiles(self, packageName):
+ """
+ Method to get a list of filenames owned by the package, or raise an
+ exception if not found.
+ """
+ raise NotImplementedError
+
+ def getMoFiles(self, locale=None):
+ """
+ Method to get a list of all .mo files on the system, optionally for a
+ specific locale.
+ """
+ moFiles = {}
+
+ def appendIfMoFile(moFiles, dirName, fNames):
+ import re
+ for fName in fNames:
+ if re.match('(.*)\\.mo', fName):
+ moFiles[dirName + '/' + fName] = None
+
+ for localePrefix in self.localePrefixes:
+ if locale:
+ localePrefix = localePrefix + '/' + locale
+ os.path.walk(localePrefix, appendIfMoFile, moFiles)
+
+ return moFiles.keys()
+
+ def getDependencies(self, packageName):
+ """
+ Method to get a list of unique package names that this package
+ is dependent on, or raise an exception if the package is not
+ found.
+ """
+ raise NotImplementedError
+
+
+class _RpmPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+ def getVersion(self, packageName):
+ import rpm
+ ts = rpm.TransactionSet()
+ for header in ts.dbMatch("name", packageName):
+ return Version.fromString(header["version"])
+ raise PackageNotFoundError(packageName)
+
+ def getFiles(self, packageName):
+ import rpm
+ ts = rpm.TransactionSet()
+ for header in ts.dbMatch("name", packageName):
+ return header["filenames"]
+ raise PackageNotFoundError(packageName)
+
+ def getDependencies(self, packageName):
+ import rpm
+ ts = rpm.TransactionSet()
+ for header in ts.dbMatch("name", packageName):
+ # Simulate a set using a hash (to a dummy value);
+ # sets were only added in Python 2.4
+ result = {}
+
+ # Get the list of requirements; these are
+ # sometimes package names, but can also be
+ # so-names of libraries, and invented virtual
+ # ids
+ for requirement in header[rpm.RPMTAG_REQUIRES]:
+ # Get the name of the package providing
+ # this requirement:
+ for depPackageHeader in ts.dbMatch("provides", requirement):
+ depName = depPackageHeader['name']
+ if depName != packageName:
+ # Add to the Hash with a dummy value
+ result[depName] = None
+ return result.keys()
+ raise PackageNotFoundError(packageName)
+
+
+class _AptPackageDb(PackageDb):
+
+ def __init__(self):
+ PackageDb.__init__(self)
+ self.cache = None
+
+ def getVersion(self, packageName):
+ if not self.cache:
+ import apt_pkg
+ apt_pkg.init()
+ self.cache = apt_pkg.GetCache()
+ packages = self.cache.Packages
+ for package in packages:
+ if package.Name == packageName:
+ verString = re.match(
+ '.*Ver:\'(.*)-.*\' Section:', str(package.CurrentVer)).group(1)
+ return Version.fromString(verString)
+ raise PackageNotFoundError(packageName)
+
+ def getFiles(self, packageName):
+ files = []
+ list = os.popen('dpkg -L %s' % packageName).readlines()
+ if not list:
+ raise PackageNotFoundError(packageName)
+ else:
+ for line in list:
+ file = line.strip()
+ if file:
+ files.append(file)
+ return files
+
+ def getDependencies(self, packageName):
+ # Simulate a set using a hash (to a dummy value);
+ # sets were only added in Python 2.4
+ result = {}
+ if not self.cache:
+ import apt_pkg
+ apt_pkg.init()
+ self.cache = apt_pkg.GetCache()
+ packages = self.cache.Packages
+ for package in packages:
+ if package.Name == packageName:
+ current = package.CurrentVer
+ if not current:
+ raise PackageNotFoundError(packageName)
+ depends = current.DependsList
+ list = depends['Depends']
+ for dependency in list:
+ name = dependency[0].TargetPkg.Name
+ # Add to the hash using a dummy value
+ result[name] = None
+ return result.keys()
+
+
+class _UbuntuAptPackageDb(_AptPackageDb):
+
+ def __init__(self):
+ _AptPackageDb.__init__(self)
+ self.localePrefixes.append(self.prefix + '/share/locale-langpack')
+
+
+class _PortagePackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+ def getVersion(self, packageName):
+ # the portage utilities are almost always going to be in
+ # /usr/lib/portage/pym
+ import sys
+ sys.path.append('/usr/lib/portage/pym')
+ import portage
+ # FIXME: this takes the first package returned in the list, in the
+ # case that there are slotted packages, and removes the leading
+ # category such as 'sys-apps'
+ gentooPackageName = portage.db["/"][
+ "vartree"].dbapi.match(packageName)[0].split('/')[1]
+ # this removes the distribution specific versioning returning only the
+ # upstream version
+ upstreamVersion = portage.pkgsplit(gentooPackageName)[1]
+ # print "Version of package is: " + upstreamVersion
+ return Version.fromString(upstreamVersion)
+
+
+class _ConaryPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+ def getVersion(self, packageName):
+ from conaryclient import ConaryClient
+ client = ConaryClient()
+ dbVersions = client.db.getTroveVersionList(packageName)
+ if not len(dbVersions):
+ raise PackageNotFoundError(packageName)
+ return dbVersions[0].trailingRevision().asString().split("-")[0]
+
+# getVersion not implemented because on Solaris multiple modules are installed
+# in single packages, so it is hard to tell what version number of a specific
+# module.
+
+
+class _SolarisPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+
+class JhBuildPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+ prefixes = []
+ prefixes.append(os.environ['LD_LIBRARY_PATH'])
+ prefixes.append(os.environ['XDG_CONFIG_DIRS'])
+ prefixes.append(os.environ['PKG_CONFIG_PATH'])
+ self.prefix = os.path.commonprefix(prefixes)
+ self.localePrefixes.append(self.prefix + '/share/locale')
+
+ def getDependencies(self, packageName):
+ result = {}
+ lines = os.popen('jhbuild list ' + packageName).readlines()
+ for line in lines:
+ if line:
+ result[line.strip()] = None
+ return result.keys()
+
+
+class Distro(object):
+
+ """
+ Class representing a distribution.
+
+ Scripts may want to do arbitrary logic based on whichever distro is in use
+ (e.g. handling differences in names of packages, distribution-specific
+ patches, etc.)
+
+ We can either create methods in the Distro class to handle these, or we
+ can use constructs like isinstance(distro, Ubuntu) to handle this. We can
+ even create hierarchies of distro subclasses to handle this kind of thing
+ (could get messy fast though)
+ """
+
+
+class Fedora(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _RpmPackageDb()
+
+
+class RHEL(Fedora): # pragma: no cover
+ pass
+
+
+class Debian(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _AptPackageDb()
+
+
+class Ubuntu(Debian):
+
+ def __init__(self):
+ self.packageDb = _UbuntuAptPackageDb()
+
+
+class Suse(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _RpmPackageDb()
+
+
+class Gentoo(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _PortagePackageDb()
+
+
+class Conary(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _ConaryPackageDb()
+
+
+class Solaris(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _SolarisPackageDb()
+
+
+class JHBuild(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = JhBuildPackageDb()
+
+
+def detectDistro():
+ logger.log("Detecting distribution:", newline=False)
+
+ if os.environ.get("CERTIFIED_GNOMIE", "no") == "yes":
+ distro = JHBuild() # pragma: no cover
+ elif os.path.exists("/etc/SuSE-release"):
+ distro = Suse() # pragma: no cover
+ elif os.path.exists("/etc/fedora-release"):
+ distro = Fedora() # pragma: no cover
+ elif os.path.exists("/etc/redhat-release"):
+ distro = RHEL() # pragma: no cover
+ elif os.path.exists("/usr/share/doc/ubuntu-minimal"):
+ distro = Ubuntu()
+ elif os.path.exists("/etc/debian_version"): # pragma: no cover
+ distro = Debian() # pragma: no cover
+ elif os.path.exists("/etc/gentoo-release"): # pragma: no cover
+ distro = Gentoo() # pragma: no cover
+ elif os.path.exists("/etc/slackware-version"): # pragma: no cover
+ raise DistributionNotSupportedError("Slackware") # pragma: no cover
+ elif os.path.exists("/var/lib/conarydb/conarydb"): # pragma: no cover
+ distro = Conary() # pragma: no cover
+ elif os.path.exists("/etc/release") and \
+ re.match(".*Solaris", open("/etc/release").readline()): # pragma: no cover
+ distro = Solaris() # pragma: no cover
+ else:
+ raise DistributionNotSupportedError("Unknown") # pragma: no cover
+ logger.log(distro.__class__.__name__)
+ return distro
+
+distro = detectDistro()
+packageDb = distro.packageDb
diff --git a/build/lib.linux-x86_64-2.7/dogtail/dump.py b/build/lib.linux-x86_64-2.7/dogtail/dump.py
new file mode 100644
index 0000000000..3756820a51
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/dump.py
@@ -0,0 +1,33 @@
+"""Utility functions for 'dumping' trees of Node objects.
+
+Author: Zack Cerza <zcerza@redhat.com>"""
+__author__ = "Zack Cerza <zcerza@redhat.com>"
+
+from __builtin__ import file
+
+spacer = ' '
+
+
+def plain(node, fileName=None):
+ """
+ Plain-text dump. The hierarchy is represented through indentation.
+ """
+ def crawl(node, depth):
+ dump(node, depth)
+ for action in node.actions.values():
+ dump(action, depth + 1)
+ for child in node.children:
+ crawl(child, depth + 1)
+
+ def dumpFile(item, depth):
+ _file.write(spacer * depth + str(item) + '\n')
+
+ def dumpStdOut(item, depth):
+ print(spacer * depth + str(item))
+ if fileName:
+ dump = dumpFile
+ _file = file(fileName, 'w')
+ else:
+ dump = dumpStdOut
+
+ crawl(node, 0)
diff --git a/build/lib.linux-x86_64-2.7/dogtail/errors.py b/build/lib.linux-x86_64-2.7/dogtail/errors.py
new file mode 100644
index 0000000000..648af9d598
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/errors.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+"""
+General exceptions; not overly module-specific
+"""
+__author__ = "Zack Cerza <zcerza@redhat.com>"
+from logging import debugLogger as logger
+
+import inspect
+
+
+def warn(message, caller=True):
+ """
+ Generate a warning, and pass it to the debug logger.
+ """
+ frameRec = inspect.stack()[-1]
+ message = "Warning: %s:%s: %s" % (frameRec[1], frameRec[2], message)
+ if caller and frameRec[1] != '<stdin>' and frameRec[1] != '<string>':
+ message = message + ':\n ' + frameRec[4][0]
+ del frameRec
+ logger.log(message)
+
+
+class DependencyNotFoundError(Exception):
+
+ """
+ A dependency was not found.
+ """
+ pass
diff --git a/build/lib.linux-x86_64-2.7/dogtail/i18n.py b/build/lib.linux-x86_64-2.7/dogtail/i18n.py
new file mode 100644
index 0000000000..8117f8d82c
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/i18n.py
@@ -0,0 +1,281 @@
+# -*- coding: utf-8 -*-
+"""
+Internationalization facilities
+
+Authors: David Malcolm <dmalcolm@redhat.com>
+"""
+
+__author__ = """David Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>"""
+
+import config
+
+import os
+import re
+import gettext
+
+from logging import debugLogger as logger
+from __builtin__ import unicode
+
+
+def safeDecode(string):
+ try:
+ string = string.decode('utf-8', 'replace')
+ except UnicodeEncodeError:
+ string = string.encode('utf-8', 'replace')
+ return string
+
+
+def safeEncode(string):
+ pass
+
+
+"""
+Singleton list of TranslationDb instances, to be initialized by the script with
+whatever translation databases it wants.
+"""
+translationDbs = []
+
+
+class TranslationDb(object):
+
+ """
+ Abstract base class representing a database of translations
+ """
+
+ def getTranslationsOf(self, srcName):
+ """
+ Pure virtual method to look up the translation of a string.
+ Returns a list of candidate strings (the translation), empty if not found.
+
+ Note that a source string can map to multiple translated strings. For
+ example, in the French translation of Evolution, the string "Forward" can
+ translate to both
+ (i) "Faire suivre" for forwarding an email, and
+ (ii) "Suivant" for the next page in a wizard.
+ """
+ raise NotImplementedError
+
+
+class GettextTranslationDb(TranslationDb):
+
+ """
+ Implementation of TranslationDb which leverages gettext, using a single
+ translation mo-file.
+ """
+
+ def __init__(self, moFile):
+ self.__moFile = moFile
+ self.__gnutranslations = gettext.GNUTranslations(open(moFile))
+
+ def getTranslationsOf(self, srcName):
+ srcName = safeDecode(srcName)
+ # print "searching for translations of %s"%srcName
+ # Use a dict to get uniqueness:
+ results = {}
+ result = self.__gnutranslations.ugettext(srcName)
+ if result != srcName:
+ results[result] = None
+
+ # Hack alert:
+ #
+ # Note that typical UI definition in GTK etc contains strings with
+ # underscores to denote accelerators.
+ # For example, the stock GTK "Add" item has text "_Add" which e.g.
+ # translates to "A_jouter" in French
+ #
+ # Since these underscores have been stripped out before we see these strings,
+ # we are looking for a translation of "Add" into "Ajouter" in this case, so
+ # we need to fake it, by looking up the string multiple times, with underscores
+ # inserted in all possible positions, stripping underscores out of the result.
+ # Ugly, but it works.
+
+ for index in range(len(srcName)):
+ candidate = srcName[:index] + "_" + srcName[index:]
+ result = self.__gnutranslations.ugettext(candidate)
+ if result != candidate:
+ # Strip out the underscore, and add to the result:
+ results[result.replace('_', '')] = True
+
+ return results.keys()
+
+
+def translate(srcString):
+ """
+ Look up srcString in the various translation databases (if any), returning
+ a list of all matches found (potentially the empty list)
+ """
+ # Use a dict to get uniqueness:
+ results = {}
+ # Try to translate the string:
+ for translationDb in translationDbs:
+ for result in translationDb.getTranslationsOf(srcString):
+ result = safeDecode(result)
+ results[result] = True
+
+ # No translations found:
+ if len(results) == 0:
+ if config.config.debugTranslation:
+ logger.log('Translation not found for "%s"' % srcString)
+ return results.keys()
+
+
+class TranslatableString(object):
+
+ """
+ Class representing a string that we want to match strings against, handling
+ translation for us, by looking it up once at construction time.
+ """
+
+ def __init__(self, untranslatedString):
+ """
+ Constructor looks up the string in all of the translation databases, storing
+ the various translations it finds.
+ """
+ untranslatedString = safeDecode(untranslatedString)
+ self.untranslatedString = untranslatedString
+ self.translatedStrings = translate(untranslatedString)
+
+ def matchedBy(self, string):
+ """
+ Compare the test string against either the translation of the original
+ string (or simply the original string, if no translation was found).
+ """
+ # print "comparing %s against %s"%(string, self)
+ def stringsMatch(inS, outS):
+ """
+ Compares a regular expression to a string
+
+ inS: the regular expression (or normal string)
+ outS: the normal string to be compared against
+ """
+ inString = str(inS)
+ outString = outS
+ if inString == outString:
+ return True
+ inString = inString + '$'
+ inString = safeDecode(inString)
+ outString = safeDecode(outString)
+ if inString[0] == '*':
+ inString = "\\" + inString
+ # Escape all parentheses, since grouping will never be needed here
+ inString = re.sub('([\(\)])', r'\\\1', inString)
+ match = re.match(inString, outString)
+ matched = match is not None
+ return matched
+
+ matched = False
+ # the 'ts' variable keeps track of whether we're working with
+ # translated strings. it's only used for debugging purposes.
+ #ts = 0
+ # print string, str(self)
+ for translatedString in self.translatedStrings:
+ #ts = ts + 1
+ matched = stringsMatch(translatedString, string)
+ if not matched:
+ matched = translatedString == string
+ if matched:
+ return matched
+ # ts=0
+ return stringsMatch(self.untranslatedString, string)
+
+ def __str__(self):
+ """
+ Provide a meaningful debug version of the string (and the translation in
+ use)
+ """
+ if len(self.translatedStrings) > 0:
+ # build an output string, with commas in the correct places
+ translations = ""
+ for tString in self.translatedStrings:
+ translations += u'"%s", ' % safeDecode(tString)
+ result = u'"%s" (%s)' % (
+ safeDecode(self.untranslatedString), translations)
+ return safeDecode(result)
+ else:
+ return '"%s"' % (self.untranslatedString)
+
+
+def isMoFile(filename, language=''):
+ """
+ Does the given filename look like a gettext mo file?
+
+ Optionally: Does the file also contain translations for a certain language,
+ for example 'ja'?
+ """
+ if re.match('(.*)\\.mo$', filename):
+ if not language:
+ return True
+ elif re.match('/usr/share/locale(.*)/%s(.*)/LC_MESSAGES/(.*)\\.mo$' %
+ language, filename):
+ return True
+ else:
+ return False
+ else:
+ return False
+
+
+def loadAllTranslationsForLanguage(language):
+ import distro
+ for moFile in distro.packageDb.getMoFiles(language):
+ translationDbs.append(GettextTranslationDb(moFile))
+
+
+def getMoFilesForPackage(packageName, language='', getDependencies=True):
+ """
+ Look up the named package and find all gettext mo files within it and its
+ dependencies. It is possible to restrict the results to those of a certain
+ language, for example 'ja'.
+ """
+ import distro
+
+ result = []
+ for filename in distro.packageDb.getFiles(packageName):
+ if isMoFile(filename, language):
+ result.append(filename)
+
+ if getDependencies:
+ # Recurse:
+ for dep in distro.packageDb.getDependencies(packageName):
+ # We pass False to the inner call because getDependencies has already
+ # walked the full tree
+ result.extend(getMoFilesForPackage(dep, language, False))
+
+ return result
+
+
+def loadTranslationsFromPackageMoFiles(packageName, getDependencies=True):
+ """
+ Helper function which appends all of the gettext translation mo-files used by
+ the package (and its dependencies) to the translation database list.
+ """
+ # Keep a list of mo-files that are already in use to avoid duplicates.
+ moFiles = {}
+
+ def load(packageName, language='', getDependencies=True):
+ for moFile in getMoFilesForPackage(packageName, language, getDependencies):
+ # Searching the popt mo-files for translations makes gettext bail out,
+ # so we ignore them here. This is
+ # https://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=172155 .
+ if not('popt.mo' in moFile or moFile in moFiles):
+ try:
+ translationDbs.append(GettextTranslationDb(moFile))
+ moFiles[moFile] = None
+ except (AttributeError, IndexError):
+ if config.config.debugTranslation:
+ #import traceback
+ # logger.log(traceback.format_exc())
+ logger.log(
+ "Warning: Failed to load mo-file for translation: " + moFile)
+
+ # Hack alert:
+ #
+ # The following special-case is necessary for Ubuntu, since their
+ # translations are shipped in a single huge package. The downside to
+ # this special case, aside from the simple fact that there is one,
+ # is that it makes automatic translations much slower.
+
+ import distro
+ language = os.environ.get('LANGUAGE', os.environ['LANG'])[0:2]
+ if isinstance(distro.distro, distro.Ubuntu):
+ load('language-pack-gnome-%s' % language, language)
+ load(packageName, language, getDependencies)
diff --git a/build/lib.linux-x86_64-2.7/dogtail/logging.py b/build/lib.linux-x86_64-2.7/dogtail/logging.py
new file mode 100644
index 0000000000..7e73f163b8
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/logging.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+"""
+Logging facilities
+
+Authors: Ed Rousseau <rousseau@redhat.com>, Zack Cerza <zcerza@redhat.com, David Malcolm <dmalcolm@redhat.com>
+"""
+
+__author__ = """Ed Rousseau <rousseau@redhat.com>,
+Zack Cerza <zcerza@redhat.com,
+David Malcolm <dmalcolm@redhat.com>
+"""
+import os
+import sys
+import time
+from config import config
+import codecs
+
+# Timestamp class for file logs
+
+
+class TimeStamp(object):
+
+ """
+ Generates timestamps tempfiles and log entries
+ """
+
+ def __init__(self):
+ self.now = "0"
+ self.timetup = time.localtime()
+
+ def zeroPad(self, int, width=2):
+ """
+ Pads an integer 'int' with zeroes, up to width 'width'.
+
+ Returns a string.
+
+ It will not truncate. If you call zeroPad(100, 2), '100' will be returned.
+ """
+ if int < 10 ** width:
+ return ("0" * (width - len(str(int)))) + str(int)
+ else:
+ return str(int)
+
+ # file stamper
+ def fileStamp(self, filename, addTime=True):
+ """
+ Generates a filename stamp in the format of filename_YYYYMMDD-hhmmss.
+ A format of filename_YYYYMMDD can be used instead by specifying addTime = False.
+ """
+ self.now = filename.strip() + "_"
+ self.timetup = time.localtime()
+
+ # Should produce rel-eng style filestamps
+ # format it all pretty by chopping the tuple
+ fieldCount = 3
+ if addTime:
+ fieldCount = fieldCount + 3
+ for i in range(fieldCount):
+ if i == 3:
+ self.now = self.now + '-'
+ self.now = self.now + self.zeroPad(self.timetup[i])
+ return self.now
+
+ # Log entry stamper
+ def entryStamp(self):
+ """
+ Generates a logfile entry stamp of YYYY.MM.DD HH:MM:SS
+ """
+ self.timetup = time.localtime()
+
+ # This will return a log entry formatted string in YYYY.MM.DD HH:MM:SS
+ for i in range(6):
+ # put in the year
+ if i == 0:
+ self.now = str(self.timetup[i])
+ # Format Month and Day
+ elif i == 1 or i == 2:
+ self.now = self.now + "." + self.zeroPad(self.timetup[i])
+ else:
+ # make the " " between Day and Hour and put in the hour
+ if i == 3:
+ self.now = self.now + " " + self.zeroPad(self.timetup[i])
+ # Otherwise Use the ":" divider
+ else:
+ self.now = self.now + ":" + self.zeroPad(self.timetup[i])
+ return self.now
+
+
+class Logger(object):
+
+ """
+ Writes entries to standard out.
+ """
+ stamper = TimeStamp()
+
+ def __init__(self, logName, file=False, stdOut=True):
+ """
+ name: the name of the log
+ file: The file object to log to.
+ stdOut: Whether to log to standard out.
+ """
+ self.logName = logName
+ self.stdOut = stdOut
+ self.file = file # Handle to the logfile
+ if not self.file:
+ return
+
+ scriptName = config.scriptName
+ if not scriptName:
+ scriptName = 'log'
+ self.fileName = scriptName
+
+ # check to see if we can write to the logDir
+ if os.path.isdir(config.logDir):
+ self.findUniqueName()
+ else:
+ # If path doesn't exist, raise an exception
+ raise IOError(
+ "Log path %s does not exist or is not a directory" % config.logDir)
+
+ def findUniqueName(self):
+ # generate a logfile name and check if it already exists
+ self.fileName = config.logDir + self.stamper.fileStamp(self.fileName) \
+ + '_' + self.logName
+ i = 0
+ while os.path.exists(self.fileName):
+ # Append the pathname
+ if i == 0:
+ self.fileName = self.fileName + "." + str(i)
+ else:
+ logsplit = self.fileName.split(".")
+ logsplit[-1] = str(i)
+ self.fileName = ".".join(logsplit)
+ i += 1
+
+ def createFile(self):
+ # Try to create the file and write the header info
+ print("Creating logfile at %s ..." % self.fileName)
+ self.file = codecs.open(self.fileName, mode='wb', encoding=
+ 'utf-8')
+ self.file.write("##### " + os.path.basename(self.fileName) + '\n')
+ self.file.flush()
+
+ def log(self, message, newline=True, force=False):
+ """
+ Hook used for logging messages. Might eventually be a virtual
+ function, but nice and simple for now.
+
+ If force is True, log to a file irrespective of config.logDebugToFile.
+ """
+ try:
+ message = message.decode('utf-8', 'replace')
+ except UnicodeEncodeError:
+ pass
+
+
+ # Try to open and write the result to the log file.
+ if isinstance(self.file, bool) and (force or config.logDebugToFile):
+ self.createFile()
+
+ if force or config.logDebugToFile:
+ if newline:
+ self.file.write(message + '\n')
+ else:
+ self.file.write(message + ' ')
+ self.file.flush()
+
+ if self.stdOut and config.logDebugToStdOut:
+ if newline:
+ print(message)
+ else:
+ print(message)
+
+
+class ResultsLogger(Logger):
+
+ """
+ Writes entries into the Dogtail log
+ """
+
+ def __init__(self, stdOut=True):
+ Logger.__init__(self, 'results', file=True, stdOut=stdOut)
+
+ # Writes the result of a test case comparison to the log
+ def log(self, entry):
+ """
+ Writes the log entry. Requires a 1 {key: value} pair dict for an argument or else it will throw an exception.
+ """
+ # We require a 1 key: value dict
+ # Strip all leading and trailing witespace from entry dict and convert
+ # to string for writing
+
+ if len(entry) == 1:
+ key = entry.keys()
+ value = entry.values()
+ key = key[0]
+ value = value[0]
+ entry = str(key) + ": " + str(value)
+ else:
+ raise ValueError(entry)
+ print(
+ "Method argument requires a 1 {key: value} dict. Supplied argument not one {key: value}")
+
+ Logger.log(self, self.stamper.entryStamp() + " " + entry,
+ force=True)
+
+debugLogger = Logger('debug', config.logDebugToFile)
+
+import traceback
+
+
+def exceptionHook(exc, value, tb): # pragma: no cover
+ tbStringList = traceback.format_exception(exc, value, tb)
+ tbString = ''.join(tbStringList)
+ debugLogger.log(tbString)
+ sys.exc_clear()
+
+sys.excepthook = exceptionHook
diff --git a/build/lib.linux-x86_64-2.7/dogtail/path.py b/build/lib.linux-x86_64-2.7/dogtail/path.py
new file mode 100644
index 0000000000..5742cfb03e
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/path.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+"""
+Author: David Malcolm <dmalcolm@redhat.com>
+"""
+__author__ = """David Malcolm <dmalcolm@redhat.com>"""
+
+
+class SearchPath(object):
+
+ """
+ Class used by the recording framework (and for more verbose script
+ logging) for identifying nodes in a persistent way, independent of the
+ style of script being written.
+
+ Implemented as a list of (predicate, isRecursive) pairs, giving the
+ 'best' way to find the Accessible wrapped by a Node, starting at the
+ root and applying each search in turn.
+
+ This is somewhat analagous to an absolute path in a filesystem, except
+ that some of searches may be recursive, rather than just searching
+ direct children.
+
+ FIXME: try to ensure uniqueness
+ FIXME: need some heuristics to get 'good' searches, whatever
+ that means
+ """
+
+ def __init__(self):
+ self.__list = []
+
+ def __str__(self):
+ result = "{"
+ for (predicate, isRecursive) in self.__list:
+ result += "/(%s,%s)" % (
+ predicate.describeSearchResult(), isRecursive)
+ return result + "}"
+
+ # We need equality to work so that dicts of these work:
+ def __eq__(self, other):
+ # print "eq: self:%s"%self
+ # print " other:%s"%other
+ if len(self.__list) != len(other.__list):
+ # print "nonequal length"
+ return False
+ else:
+ for i in range(len(self.__list)):
+ if self.__list[i] != other.__list[i]:
+ return False
+ # print True
+ return True
+
+ def append(self, predicate, isRecursive):
+ assert predicate
+ self.__list.append((predicate, isRecursive))
+
+ def __iter__(self):
+ return iter(self.__list)
+
+ def length(self):
+ return len(self.__list)
+
+ def makeScriptMethodCall(self):
+ """
+ Used by the recording system.
+
+ Generate the Python source code that will carry out this search.
+ """
+ result = ""
+ for (predicate, isRecursive) in self.__list:
+ # print predicate
+ # print self.generateVariableName(predicate)
+ result += "." + predicate.makeScriptMethodCall(isRecursive)
+ return result
+
+ def getRelativePath(self, other):
+ """
+ Given another SearchPath instance, if the other is 'below' this
+ one, return a SearchPath that describes how to reach it relative
+ to this one (a copy of the second part of the list). Otherwise
+ return None.
+ """
+ for i in range(len(self.__list)):
+ if self.__list[i] != other.__list[i]:
+ break
+ if i > 0:
+ # Slice from this point to the end:
+ result = SearchPath()
+ result.__list = other.__list[i + 1:]
+
+ if False:
+ print("....................")
+ print("from %s" % self)
+ print("to %s" % other)
+ print("i=%s" % i)
+ print("relative path %s" % result)
+ print("....................")
+
+ return result
+ else:
+ return None
+
+ def getPrefix(self, n):
+ """
+ Get the first n components of this instance as a new instance
+ """
+ result = SearchPath()
+ for i in range(n):
+ result.__list.append(self.__list[i])
+ return result
+
+ def getPredicate(self, i):
+ (predicate, isRecursive) = self.__list[i]
+ return predicate
diff --git a/build/lib.linux-x86_64-2.7/dogtail/predicate.py b/build/lib.linux-x86_64-2.7/dogtail/predicate.py
new file mode 100644
index 0000000000..aa100e1f05
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/predicate.py
@@ -0,0 +1,443 @@
+"""Predicates that can be used when searching for nodes.
+
+Author: David Malcolm <dmalcolm@redhat.com>"""
+__author__ = 'David Malcolm <dmalcolm@redhat.com>'
+
+from i18n import TranslatableString
+from gi.repository import GLib
+from time import sleep
+from logging import debugLogger as logger
+from config import config
+
+def stringMatches(scriptName, reportedName):
+ assert isinstance(scriptName, TranslatableString)
+
+ return scriptName.matchedBy(reportedName)
+
+
+def makeScriptRecursiveArgument(isRecursive, defaultValue):
+ if isRecursive == defaultValue:
+ return ""
+ else:
+ return ", recursive=%s" % isRecursive
+
+
+def makeCamel(string):
+ """
+ Convert string to camelCaps
+ """
+ string = str(string)
+ # FIXME: this function is probably really fragile, lots of difficult cases
+ # here
+
+ # Sanitize string, replacing bad characters with spaces:
+ for char in ":;!@#$%^&*()-+=_~`\\/?|[]{}<>,.\t\n\r\"'":
+ string = string.replace(char, " ")
+ words = string.strip().split(" ")
+ for word in words:
+ word.strip
+ result = ""
+ firstWord = True
+ for word in words:
+ lowercaseWord = word.lower()
+ if firstWord:
+ result += lowercaseWord
+ firstWord = False
+ else:
+ result += lowercaseWord.capitalize()
+ return result
+
+
+class Predicate(object):
+
+ """Abstract base class representing a predicate function on nodes.
+
+ It's more than just a function in that it has data and can describe itself"""
+
+ def satisfiedByNode(self, node):
+ """Pure virtual method returning a boolean if the predicate is satisfied by the node"""
+ raise NotImplementedError
+
+ def describeSearchResult(self, node):
+ raise NotImplementedError
+
+ def makeScriptMethodCall(self, isRecursive):
+ """
+ Method to generate a string containing a (hopefully) readable search
+ method call on a node (to be used when generating Python source code in
+ the event recorder)
+ """
+ raise NotImplementedError
+
+ def makeScriptVariableName(self):
+ """
+ Method to generate a string containing a (hopefully) readable name
+ for a Node instance variable that would be the result of a search on
+ this predicate (to be used when generating Python source code in the
+ event recorder).
+ """
+ raise NotImplementedError
+
+ def __eq__(self, other):
+ """
+ Predicates are considered equal if they are of the same subclass and
+ have the same data
+ """
+ # print "predeq: self:%s"%self
+ # print " other:%s"%other
+ # print "predeq: selfdict:%s"%self.__dict__
+ # print " otherdict:%s"%other.__dict__
+
+ if type(self) != type(other):
+ return False
+ else:
+ return self.__dict__ == other.__dict__
+
+
+class IsAnApplicationNamed(Predicate):
+
+ """Search subclass that looks for an application by name"""
+
+ def __init__(self, appName):
+ self.appName = TranslatableString(appName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ try:
+ return node.roleName == 'application' and stringMatches(self.appName, node.name)
+ except GLib.GError as e:
+ if 'name :1.0 was not provided' in e.message:
+ logger.log("Dogtail: warning: omiting possibly broken at-spi application record")
+ return False
+ else:
+ try:
+ sleep(config.defaults['searchWarningThreshold'])
+ return node.roleName == 'application' and stringMatches(self.appName, node.name)
+ except GLib.GError:
+ logger.log("Dogtail: warning: application may be hanging")
+ return False
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return '%s application' % self.appName
+
+ def makeScriptMethodCall(self, isRecursive):
+ # ignores the isRecursive parameter
+ return "application(%s)" % self.appName
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.appName) + "App"
+
+
+class GenericPredicate(Predicate):
+
+ """SubtreePredicate subclass that takes various optional search fields"""
+
+ def __init__(self, name=None, roleName=None, description=None, label=None, debugName=None):
+ if name:
+ self.name = TranslatableString(name)
+ else:
+ self.name = None
+ self.roleName = roleName
+ self.description = description
+ if label:
+ self.label = TranslatableString(label)
+ else:
+ self.label = None
+
+ if debugName:
+ self.debugName = debugName
+ else:
+ if label:
+ self.debugName = "labelled '%s'" % self.label
+ else:
+ self.debugName = "child with"
+ if name:
+ self.debugName += " name=%s" % self.name
+ if roleName:
+ self.debugName += " roleName='%s'" % roleName
+ if description:
+ self.debugName += " description='%s'" % description
+ assert self.debugName
+
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ # labelled nodes are handled specially:
+ if self.label:
+ # this reverses the search; we're looking for a node with LABELLED_BY
+ # and then checking the label, rather than looking for a label and
+ # then returning whatever LABEL_FOR targets
+ if node.labeller:
+ return stringMatches(self.label, node.labeller.name)
+ else:
+ return False
+ else:
+ # Ensure the node matches any criteria that were set:
+ try:
+ if self.name:
+ if not stringMatches(self.name, node.name):
+ return False
+ if self.roleName:
+ if self.roleName != node.roleName:
+ return False
+ if self.description:
+ if self.description != node.description:
+ return False
+ except GLib.GError as e:
+ if 'name :1.0 was not provided' in e.message:
+ logger.log("Dogtail: warning: omiting possibly broken at-spi application record")
+ return False
+ else:
+ raise e
+ return True
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return self.debugName
+
+ def makeScriptMethodCall(self, isRecursive):
+ if self.label:
+ args = "label=%s" % self.label
+ else:
+ args = ""
+ if self.name:
+ print(self.name)
+ args += " name=%s" % self.name
+ if self.roleName:
+ args += " roleName='%s'" % self.roleName
+ if self.description:
+ args += " description='%s'" % self.description
+ return "child(%s%s)" % (args, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ if self.label:
+ return makeCamel(self.label) + "Node"
+ else:
+ if self.name:
+ return makeCamel(self.name) + "Node"
+ if self.roleName:
+ return makeCamel(self.roleName) + "Node"
+ if self.description:
+ return makeCamel(self.description) + "Node"
+
+
+class IsNamed(Predicate):
+
+ """Predicate subclass that looks simply by name"""
+
+ def __init__(self, name):
+ self.name = TranslatableString(name)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ return stringMatches(self.name, node.name)
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return "named %s" % self.name
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "child(name=%s%s)" % (self.name, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.name) + "Node"
+
+
+class IsAWindowNamed(Predicate):
+
+ """Predicate subclass that looks for a top-level window by name"""
+
+ def __init__(self, windowName):
+ self.windowName = TranslatableString(windowName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ return node.roleName == 'frame' and stringMatches(self.windowName, node.name)
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return "%s window" % self.windowName
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "window(%s%s)" % (self.windowName, makeScriptRecursiveArgument(isRecursive, False))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.windowName) + "Win"
+
+
+class IsAWindow(Predicate):
+
+ """Predicate subclass that looks for top-level windows"""
+
+ def __init__(self):
+ self.satisfiedByNode = lambda node: node.roleName == 'frame'
+
+ def describeSearchResult(self):
+ return "window"
+
+
+class IsADialogNamed(Predicate):
+
+ """Predicate subclass that looks for a top-level dialog by name"""
+
+ def __init__(self, dialogName):
+ self.dialogName = TranslatableString(dialogName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ return node.roleName == 'dialog' and stringMatches(self.dialogName, node.name)
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return '%s dialog' % self.dialogName
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "dialog(%s%s)" % (self.dialogName, makeScriptRecursiveArgument(isRecursive, False))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.dialogName) + "Dlg"
+
+
+class IsLabelledBy(Predicate):
+
+ """Predicate: is this node labelled by another node"""
+ pass
+
+
+class IsLabelledAs(Predicate):
+
+ """Predicate: is this node labelled with the text string (i.e. by another node with that as a name)"""
+
+ def __init__(self, labelText):
+ self.labelText = TranslatableString(labelText)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ # FIXME
+ if node.labeller:
+ return stringMatches(self.labelText, node.labeller.name)
+ else:
+ return False
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return 'labelled %s' % self.labelText
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "child(label=%s%s)" % (self.labelText, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.labelText) + "Node"
+
+
+class IsAMenuNamed(Predicate):
+
+ """Predicate subclass that looks for a menu by name"""
+
+ def __init__(self, menuName):
+ self.menuName = TranslatableString(menuName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'menu' and \
+ stringMatches(self.menuName, node.name)
+
+ def describeSearchResult(self):
+ return '%s menu' % (self.menuName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "menu(%s%s)" % (self.menuName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.menuName) + "Menu"
+
+
+class IsAMenuItemNamed(Predicate):
+
+ """Predicate subclass that looks for a menu item by name"""
+
+ def __init__(self, menuItemName):
+ self.menuItemName = TranslatableString(menuItemName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: \
+ node.roleName.endswith('menu item') and \
+ stringMatches(self.menuItemName, node.name)
+
+ def describeSearchResult(self):
+ return '%s menuitem' % (self.menuItemName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "menuItem(%s%s)" % (self.menuItemName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.menuItemName) + "MenuItem"
+
+
+class IsATextEntryNamed(Predicate):
+
+ """Predicate subclass that looks for a text entry by name"""
+
+ def __init__(self, textEntryName):
+ self.textEntryName = TranslatableString(textEntryName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'text' and \
+ stringMatches(self.textEntryName, node.name)
+
+ def describeSearchResult(self):
+ return '%s textentry' % (self.textEntryName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "textentry(%s%s)" % (self.textEntryName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.textEntryName) + "Entry"
+
+
+class IsAButtonNamed(Predicate):
+
+ """Predicate subclass that looks for a button by name"""
+
+ def __init__(self, buttonName):
+ self.buttonName = TranslatableString(buttonName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'push button' \
+ and stringMatches(self.buttonName, node.name)
+
+ def describeSearchResult(self):
+ return '%s button' % (self.buttonName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "button(%s%s)" % (self.buttonName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.buttonName) + "Button"
+
+
+class IsATabNamed(Predicate):
+
+ """Predicate subclass that looks for a tab by name"""
+
+ def __init__(self, tabName):
+ self.tabName = TranslatableString(tabName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'page tab' and \
+ stringMatches(self.tabName, node.name)
+
+ def describeSearchResult(self):
+ return '%s tab' % (self.tabName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "tab(%s%s)" % (self.tabName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.tabName) + "Tab"
diff --git a/build/lib.linux-x86_64-2.7/dogtail/procedural.py b/build/lib.linux-x86_64-2.7/dogtail/procedural.py
new file mode 100644
index 0000000000..c87ae86181
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/procedural.py
@@ -0,0 +1,455 @@
+"""
+Dogtail's procedural UI
+All the classes here are intended to be single-instance, except for Action.
+"""
+__author__ = 'Zack Cerza <zcerza@redhat.com>'
+#
+#
+# WARNING: Here There Be Dragons (TM) #
+#
+# If you don't understand how to use this API, you almost certainly don't #
+# want to read the code first. We make use of some very non-intuitive #
+# features of Python in order to make the API very simplistic. Therefore, #
+# you should probably only read this code if you're already familiar with #
+# some of Python's advanced features. You have been warned. ;) #
+#
+#
+
+import tree
+import predicate
+from config import config
+from utils import Lock
+import rawinput
+
+#FocusError = "FocusError: %s not found"
+
+
+class FocusError(Exception):
+ pass
+
+import errors
+
+
+def focusFailed(pred):
+ errors.warn('The requested widget could not be focused: %s' %
+ pred.debugName)
+
+ENOARGS = "At least one argument is needed"
+
+
+class FocusBase(object):
+
+ """
+ The base for every class in the module. Does nothing special, really.
+ """
+ node = None
+
+ def __getattr__(self, name):
+ # Fold all the Node's AT-SPI properties into the Focus object.
+ try:
+ return getattr(self.node, name)
+ except AttributeError:
+ raise AttributeError(name)
+
+ def __setattr__(self, name, value):
+ # Fold all the Node's AT-SPI properties into the Focus object.
+ if name == 'node':
+ setattr(self.__class__, name, value)
+ else:
+ try:
+ setattr(self.node, name, value)
+ except AttributeError:
+ raise AttributeError(name)
+
+
+class FocusApplication (FocusBase):
+
+ """
+ Keeps track of which application is currently focused.
+ """
+ desktop = tree.root
+
+ def __call__(self, name):
+ """
+ Search for an application that matches and refocus on the given name.
+ """
+ try:
+ pred = predicate.IsAnApplicationNamed(name)
+ app = self.desktop.findChild(
+ pred, recursive=False, retry=False)
+ except tree.SearchError:
+ if config.fatalErrors:
+ raise FocusError(name)
+ else:
+ focusFailed(pred)
+ return False
+ if app:
+ FocusApplication.node = app
+ FocusDialog.node = None
+ FocusWindow.node = None
+ FocusWidget.node = None
+ return True
+
+
+class FocusDesktop (FocusBase):
+
+ """
+ This isn't used yet, and may never be used.
+ """
+ pass
+
+
+class FocusWindow (FocusBase):
+
+ """
+ Keeps track of which window is currently focused.
+ """
+
+ def __call__(self, name):
+ """
+ Search for a dialog that matches the given name and refocus on it.
+ """
+ result = None
+ pred = predicate.IsAWindowNamed(name)
+ try:
+ result = FocusApplication.node.findChild(
+ pred, requireResult=False, recursive=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWindow.node = result
+ FocusDialog.node = None
+ FocusWidget.node = None
+ else:
+ if config.fatalErrors:
+ raise FocusError(pred.debugName)
+ else:
+ focusFailed(pred)
+ return False
+ return True
+
+
+class FocusDialog (FocusBase):
+
+ """
+ Keeps track of which dialog is currently focused.
+ """
+
+ def __call__(self, name):
+ """
+ Search for a dialog that matches the given name and refocus on it.
+ """
+ result = None
+ pred = predicate.IsADialogNamed(name)
+ try:
+ result = FocusApplication.node.findChild(
+ pred, requireResult=False, recursive=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusDialog.node = result
+ FocusWidget.node = None
+ else:
+ if config.fatalErrors:
+ raise FocusError(pred.debugName)
+ else:
+ focusFailed(pred)
+ return False
+ return True
+
+
+class FocusWidget (FocusBase):
+
+ """
+ Keeps track of which widget is currently focused.
+ """
+
+ def findByPredicate(self, pred):
+ result = None
+ try:
+ result = FocusWidget.node.findChild(
+ pred, requireResult=False, retry=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWidget.node = result
+ else:
+ try:
+ result = FocusDialog.node.findChild(
+ pred, requireResult=False, retry=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWidget.node = result
+ else:
+ try:
+ result = FocusWindow.node.findChild(
+ pred, requireResult=False, retry=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWidget.node = result
+ else:
+ try:
+ result = FocusApplication.node.findChild(
+ pred, requireResult=False, retry=False)
+ if result:
+ FocusWidget.node = result
+ except AttributeError:
+ if config.fatalErrors:
+ raise FocusError(pred)
+ else:
+ focusFailed(pred)
+ return False
+
+ if result is None:
+ FocusWidget.node = result
+ if config.fatalErrors:
+ raise FocusError(pred.debugName)
+ else:
+ focusFailed(pred)
+ return False
+ return True
+
+ def __call__(self, name='', roleName='', description=''):
+ """
+ If name, roleName or description are specified, search for a widget that matches and refocus on it.
+ """
+ if not name and not roleName and not description:
+ raise TypeError(ENOARGS)
+
+ # search for a widget.
+ pred = predicate.GenericPredicate(name=name,
+ roleName=roleName, description=description)
+ return self.findByPredicate(pred)
+
+
+class Focus (FocusBase):
+
+ """
+ The container class for the focused application, dialog and widget.
+ """
+
+ def __getattr__(self, name):
+ raise AttributeError(name)
+
+ def __setattr__(self, name, value):
+ if name in ('application', 'dialog', 'widget', 'window'):
+ self.__dict__[name] = value
+ else:
+ raise AttributeError(name)
+
+ desktop = tree.root
+ application = FocusApplication()
+ app = application # shortcut :)
+ dialog = FocusDialog()
+ window = FocusWindow()
+ frame = window
+ widget = FocusWidget()
+
+ def button(self, name):
+ """
+ A shortcut to self.widget.findByPredicate(predicate.IsAButtonNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsAButtonNamed(name))
+
+ def icon(self, name):
+ """
+ A shortcut to self.widget(name, roleName = 'icon')
+ """
+ return self.widget(name=name, roleName='icon')
+
+ def menu(self, name):
+ """
+ A shortcut to self.widget.findByPredicate(predicate.IsAMenuNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsAMenuNamed(name))
+
+ def menuItem(self, name):
+ """
+ A shortcut to self.widget.findByPredicate(predicate.IsAMenuItemNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsAMenuItemNamed(name))
+
+ def table(self, name=''):
+ """
+ A shortcut to self.widget(name, roleName 'table')
+ """
+ return self.widget(name=name, roleName='table')
+
+ def tableCell(self, name=''):
+ """
+ A shortcut to self.widget(name, roleName 'table cell')
+ """
+ return self.widget(name=name, roleName='table cell')
+
+ def text(self, name=''):
+ """
+ A shortcut to self.widget.findByPredicate(IsATextEntryNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsATextEntryNamed(name))
+
+
+class Action (FocusWidget):
+
+ """
+ Aids in executing AT-SPI actions, refocusing the widget if necessary.
+ """
+
+ def __init__(self, action):
+ """
+ action is a string with the same name as the AT-SPI action you wish to execute using this class.
+ """
+ self.action = action
+
+ def __call__(self, name='', roleName='', description='', delay=config.actionDelay):
+ """
+ If name, roleName or description are specified, first search for a widget that matches and refocus on it.
+ Then execute the action.
+ """
+ if name or roleName or description:
+ FocusWidget.__call__(
+ self, name=name, roleName=roleName, description=description)
+ self.node.doActionNamed(self.action)
+
+ def __getattr__(self, attr):
+ return getattr(FocusWidget.node, attr)
+
+ def __setattr__(self, attr, value):
+ if attr == 'action':
+ self.__dict__[attr] = value
+ else:
+ setattr(FocusWidget, attr, value)
+
+ def button(self, name):
+ """
+ A shortcut to self(name, roleName = 'push button')
+ """
+ self.__call__(name=name, roleName='push button')
+
+ def menu(self, name):
+ """
+ A shortcut to self(name, roleName = 'menu')
+ """
+ self.__call__(name=name, roleName='menu')
+
+ def menuItem(self, name):
+ """
+ A shortcut to self(name, roleName = 'menu item')
+ """
+ self.__call__(name=name, roleName='menu item')
+
+ def table(self, name=''):
+ """
+ A shortcut to self(name, roleName 'table')
+ """
+ self.__call__(name=name, roleName='table')
+
+ def tableCell(self, name=''):
+ """
+ A shortcut to self(name, roleName 'table cell')
+ """
+ self.__call__(name=name, roleName='table cell')
+
+ def text(self, name=''):
+ """
+ A shortcut to self(name, roleName = 'text')
+ """
+ self.__call__(name=name, roleName='text')
+
+
+class Click (Action):
+
+ """
+ A special case of Action, Click will eventually handle raw mouse events.
+ """
+ primary = 1
+ middle = 2
+ secondary = 3
+
+ def __init__(self):
+ Action.__init__(self, 'click')
+
+ def __call__(self, name='', roleName='', description='', raw=True, button=primary, delay=config.actionDelay):
+ """
+ By default, execute a raw mouse event.
+ If raw is False or if button evaluates to False, just pass the rest of
+ the arguments to Action.
+ """
+ if name or roleName or description:
+ FocusWidget.__call__(
+ self, name=name, roleName=roleName, description=description)
+ if raw and button:
+ # We're doing a raw mouse click
+ Click.node.click(button)
+ else:
+ Action.__call__(
+ self, name=name, roleName=roleName, description=description, delay=delay)
+
+
+class Select (Action):
+
+ """
+ Aids in selecting and deselecting widgets, i.e. page tabs
+ """
+ select = 'select'
+ deselect = 'deselect'
+
+ def __init__(self, action):
+ """
+ action must be 'select' or 'deselect'.
+ """
+ if action not in (self.select, self.deselect):
+ raise ValueError(action)
+ Action.__init__(self, action)
+
+ def __call__(self, name='', roleName='', description='', delay=config.actionDelay):
+ """
+ If name, roleName or description are specified, first search for a widget that matches and refocus on it.
+ Then execute the action.
+ """
+ if name or roleName or description:
+ FocusWidget.__call__(
+ self, name=name, roleName=roleName, description=description)
+ func = getattr(self.node, self.action)
+ func()
+
+
+def type(text):
+ if focus.widget.node:
+ focus.widget.node.typeText(text)
+ else:
+ rawinput.typeText(text)
+
+
+def keyCombo(combo):
+ if focus.widget.node:
+ focus.widget.node.keyCombo(combo)
+ else:
+ rawinput.keyCombo(combo)
+
+
+def run(application, arguments='', appName=''):
+ from utils import run as utilsRun
+ pid = utilsRun(application + ' ' + arguments, appName=appName)
+ focus.application(application)
+ return pid
+
+import os
+# tell sniff not to use auto-refresh while script using this module is running
+# may have already been locked by dogtail.tree
+if not os.path.exists('/tmp/sniff_refresh.lock'): # pragma: no cover
+ sniff_lock = Lock(lockname='sniff_refresh.lock', randomize=False)
+ try:
+ sniff_lock.lock()
+ except OSError:
+ pass # lock was already present from other script instance or leftover from killed instance
+ # lock should unlock automatically on script exit.
+
+focus = Focus()
+click = Click()
+activate = Action('activate')
+openItem = Action('open')
+menu = Action('menu')
+select = Select(Select.select)
+deselect = Select(Select.deselect)
diff --git a/build/lib.linux-x86_64-2.7/dogtail/rawinput.py b/build/lib.linux-x86_64-2.7/dogtail/rawinput.py
new file mode 100644
index 0000000000..7100b29bfa
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/rawinput.py
@@ -0,0 +1,252 @@
+# -*- coding: utf-8 -*-
+"""
+Handles raw input using AT-SPI event generation.
+
+Note: Think of keyvals as keysyms, and keynames as keystrings.
+
+Authors: David Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>
+"""
+
+__author__ = """
+David Malcolm <dmalcolm@redhat.com>,
+Zack Cerza <zcerza@redhat.com>
+"""
+import gi
+
+gi.require_version('Gtk', '3.0')
+gi.require_version('Gdk', '3.0')
+
+from gi.repository import Gdk
+from config import config
+from utils import doDelay
+from logging import debugLogger as logger
+from pyatspi import Registry as registry
+from pyatspi import (KEY_SYM, KEY_PRESS, KEY_PRESSRELEASE, KEY_RELEASE)
+from exceptions import ValueError
+from __builtin__ import unicode, unichr
+
+
+def doTypingDelay():
+ doDelay(config.typingDelay)
+
+
+def checkCoordinates(x, y):
+ if x < 0 or y < 0:
+ raise ValueError(
+ "Attempting to generate a mouse event at negative coordinates: (%s,%s)" % (x, y))
+
+
+def click(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button click at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s click at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sc' % button)
+ doDelay(config.actionDelay)
+
+
+def doubleClick(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button double-click at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s doubleclick at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sd' % button)
+ doDelay()
+
+
+def press(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button press at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s press at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sp' % button)
+ doDelay()
+
+
+def release(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button release at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s release at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sr' % button)
+ doDelay()
+
+
+def absoluteMotion(x, y, mouseDelay=None, check=True):
+ """
+ Synthesize mouse absolute motion to (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse absolute motion to (%s,%s)" % (x, y))
+ registry.generateMouseEvent(x, y, 'abs')
+ if mouseDelay:
+ doDelay(mouseDelay)
+ else:
+ doDelay()
+
+
+def relativeMotion(x, y, mouseDelay=None):
+ logger.log("Mouse relative motion of (%s,%s)" % (x, y))
+ registry.generateMouseEvent(x, y, 'rel')
+ if mouseDelay:
+ doDelay(mouseDelay)
+ else:
+ doDelay()
+
+
+def drag(fromXY, toXY, button=1, check=True):
+ """
+ Synthesize a mouse press, drag, and release on the screen.
+ """
+ logger.log("Mouse button %s drag from %s to %s" % (button, fromXY, toXY))
+
+ (x, y) = fromXY
+ press(x, y, button, check)
+ # doDelay()
+
+ (x, y) = toXY
+ absoluteMotion(x, y, check=check)
+ doDelay()
+
+ release(x, y, button, check)
+ doDelay()
+
+
+def typeText(string):
+ """
+ Types the specified string, one character at a time.
+ Please note, you may have to set a higher typing delay,
+ if your machine misses/switches the characters typed.
+ Needed sometimes on slow setups/VMs typing non-ASCII utf8 chars.
+ """
+ if not isinstance(string, unicode):
+ string = string.decode('utf-8')
+ for char in string:
+ pressKey(char)
+
+keyNameAliases = {
+ 'enter': 'Return',
+ 'esc': 'Escape',
+ 'alt': 'Alt_L',
+ 'control': 'Control_L',
+ 'ctrl': 'Control_L',
+ 'shift': 'Shift_L',
+ 'del': 'Delete',
+ 'ins': 'Insert',
+ 'pageup': 'Page_Up',
+ 'pagedown': 'Page_Down',
+ ' ': 'space',
+ '\t': 'Tab',
+ '\n': 'Return'
+}
+
+
+# TODO: Dead code
+def keySymToUniChar(keySym): # pragma: no cover
+ i = Gdk.keyval_to_unicode(keySym)
+ if i:
+ UniChar = unichr(i)
+ else:
+ UniChar = ''
+ return UniChar
+
+
+def uniCharToKeySym(uniChar):
+ # OK, if it's not actually unicode we can fix that, right?
+ if not isinstance(uniChar, unicode):
+ uniChar = unicode(uniChar, 'utf-8')
+ i = ord(uniChar)
+ keySym = Gdk.unicode_to_keyval(i)
+ return keySym
+
+
+# dead code
+def keySymToKeyName(keySym): # pragma: no cover
+ return Gdk.keyval_name(keySym)
+
+
+def keyNameToKeySym(keyName):
+ keyName = keyNameAliases.get(keyName.lower(), keyName)
+ keySym = Gdk.keyval_from_name(keyName)
+ # various error 'codes' returned for non-recognized chars in versions of GTK3.X
+ if keySym == 0xffffff or keySym == 0x0 or keySym is None:
+ try:
+ keySym = uniCharToKeySym(keyName)
+ except: # not even valid utf-8 char
+ try: # Last attempt run at a keyName ('Meta_L', 'Dash' ...)
+ keySym = getattr(Gdk, 'KEY_' + keyName)
+ except AttributeError:
+ raise KeyError(keyName)
+ return keySym
+
+
+def keyNameToKeyCode(keyName):
+ """
+ Use GDK to get the keycode for a given keystring.
+
+ Note that the keycode returned by this function is often incorrect when
+ the requested keystring is obtained by holding down the Shift key.
+
+ Generally you should use uniCharToKeySym() and should only need this
+ function for nonprintable keys anyway.
+ """
+ keymap = Gdk.Keymap.get_for_display(Gdk.Display.get_default())
+ entries = keymap.get_entries_for_keyval(
+ Gdk.keyval_from_name(keyName))
+ try:
+ return entries[1][0].keycode
+ except TypeError:
+ pass
+
+
+def pressKey(keyName):
+ """
+ Presses (and releases) the key specified by keyName.
+ keyName is the English name of the key as seen on the keyboard. Ex: 'enter'
+ Names are looked up in Gdk.KEY_ If they are not found there, they are
+ looked up by uniCharToKeySym().
+ """
+ keySym = keyNameToKeySym(keyName)
+ registry.generateKeyboardEvent(keySym, None, KEY_SYM)
+ doTypingDelay()
+
+
+def keyCombo(comboString):
+ """
+ Generates the appropriate keyboard events to simulate a user pressing the
+ specified key combination.
+
+ comboString is the representation of the key combo to be generated.
+ e.g. '<Control><Alt>p' or '<Control><Shift>PageUp' or '<Control>q'
+ """
+ strings = []
+ for s in comboString.split('<'):
+ if s:
+ for S in s.split('>'):
+ if S:
+ S = keyNameAliases.get(S.lower(), S)
+ strings.append(S)
+ for s in strings:
+ if not hasattr(Gdk, s):
+ if not hasattr(Gdk, 'KEY_' + s):
+ raise ValueError("Cannot find key %s" % s)
+ modifiers = strings[:-1]
+ finalKey = strings[-1]
+ for modifier in modifiers:
+ code = keyNameToKeyCode(modifier)
+ registry.generateKeyboardEvent(code, None, KEY_PRESS)
+ code = keyNameToKeyCode(finalKey)
+ registry.generateKeyboardEvent(code, None, KEY_PRESSRELEASE)
+ for modifier in modifiers:
+ code = keyNameToKeyCode(modifier)
+ registry.generateKeyboardEvent(code, None, KEY_RELEASE)
+ doDelay()
diff --git a/build/lib.linux-x86_64-2.7/dogtail/sessions.py b/build/lib.linux-x86_64-2.7/dogtail/sessions.py
new file mode 100644
index 0000000000..8d4bcce6a3
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/sessions.py
@@ -0,0 +1,231 @@
+import time
+import os
+import pwd
+import errno
+import re
+import subprocess
+import signal
+import tempfile
+import random
+import glob
+from dogtail.config import config
+
+
+def scratchFile(label): # pragma: no cover
+ """Uses tempfile.NamedTemporaryFile() to create a unique tempfile in
+ config.scratchDir, with a filename like:
+ dogtail-headless-<label>.<random junk>"""
+ prefix = "dogtail-headless-"
+ return tempfile.NamedTemporaryFile(prefix="%s%s." % (prefix, label),
+ dir=config.scratchDir)
+
+
+def testBinary(path): # pragma: no cover
+ if (path.startswith(os.path.sep) or
+ path.startswith(os.path.join('.', '')) or
+ path.startswith(os.path.join('..', ''))):
+ if not os.path.exists(path):
+ raise IOError(errno.ENOENT, "No such file", path)
+ if not os.access(path, os.X_OK):
+ raise IOError(errno.ENOEXEC, "Permission denied", path)
+ return True
+
+
+def get_username(): # pragma: no cover
+ return pwd.getpwuid(os.getuid())[0]
+
+
+class Subprocess(object): # pragma: no cover
+
+ def __init__(self, cmdList, environ=None):
+ testBinary(cmdList[0])
+ self.cmdList = cmdList
+ self.environ = environ
+ self._exitCode = None
+
+ def start(self):
+ if self.environ is None:
+ self.environ = os.environ
+ self.popen = subprocess.Popen(
+ self.cmdList, env=self.environ) # , stdout = subprocess.PIPE,
+ # stderr = subprocess.STDOUT, close_fds = True)
+ return self.popen.pid
+
+ def wait(self):
+ return self.popen.wait()
+
+ def stop(self):
+ # The following doesn't exist in python < 2.6, if you can believe it.
+ # self.popen.terminate()
+ os.kill(self.popen.pid, signal.SIGTERM)
+
+ @property
+ def exitCode(self):
+ if self._exitCode is None:
+ self._exitCode = self.wait()
+ return self._exitCode
+
+
+class XServer(Subprocess): # pragma: no cover
+
+ def __init__(self, server="/usr/bin/Xorg",
+ xinitrc="/etc/X11/xinit/Xclients",
+ resolution="1024x768x16"):
+ """resolution is only used with Xvfb."""
+ testBinary(server)
+ self.server = server
+ self._exitCode = None
+ self.xinit = "/usr/bin/xinit"
+ self.display = None
+ self.xinitrc = xinitrc
+ self.resolution = resolution
+
+ @staticmethod
+ def findFreeDisplay():
+ tmp = os.listdir('/tmp')
+ pattern = re.compile('\.X([0-9]+)-lock')
+ usedDisplays = []
+ for file in tmp:
+ match = re.match(pattern, file)
+ if match:
+ usedDisplays.append(int(match.groups()[0]))
+ if not usedDisplays:
+ return ':0'
+ usedDisplays.sort()
+ return ':' + str(usedDisplays[-1] + 1)
+
+ @property
+ def cmdList(self):
+ self.display = self.findFreeDisplay()
+ cmd = []
+ if self.xinit:
+ cmd.append(self.xinit)
+ if self.xinitrc:
+ cmd.append(self.xinitrc)
+ cmd.append('--')
+ cmd.append(self.server)
+ cmd.append(self.display)
+ cmd.extend(['-ac', '-noreset'])
+ if self.server.endswith('Xvfb'):
+ cmd.extend(['-screen', '0', self.resolution])
+ cmd.append('-shmem')
+ return cmd
+
+ def start(self):
+ print(' '.join(self.cmdList))
+ self.popen = subprocess.Popen(self.cmdList)
+ return self.popen.pid
+
+
+class Script(Subprocess): # pragma: no cover
+ pass
+
+
+class Session(object): # pragma: no cover
+
+ cookieName = "DOGTAIL_SESSION_COOKIE"
+
+ def __init__(self, sessionBinary, scriptCmdList=[], scriptDelay=20, logout=True):
+ testBinary(sessionBinary)
+ self.sessionBinary = sessionBinary
+ self.script = Script(scriptCmdList)
+ self.scriptDelay = scriptDelay
+ self.logout = logout
+ self.xserver = XServer()
+ self._cookie = None
+ self._environment = None
+
+ def start(self):
+ self.xinitrcFileObj = scratchFile('xinitrc')
+ self.xserver.xinitrc = self.xinitrcFileObj.name
+ self._buildXInitRC(self.xinitrcFileObj)
+ xServerPid = self.xserver.start()
+ time.sleep(self.scriptDelay)
+ self.script.environ = self.environment
+ scriptPid = self.script.start()
+ return (xServerPid, scriptPid)
+
+ @property
+ def environment(self):
+ def isSessionProcess(fileName):
+ try:
+ if os.path.realpath(path + 'exe') != ('/usr/bin/plasma-desktop'
+ if self.sessionBinary.split('/')[-1] == 'startkde'
+ else self.sessionBinary):
+ return False
+ except OSError:
+ return False
+ pid = fileName.split('/')[2]
+ if pid == 'self' or pid == str(os.getpid()):
+ return False
+ return True
+
+ def getEnvDict(fileName):
+ try:
+ envString = open(fileName, 'r').read()
+ except IOError:
+ return {}
+ envItems = envString.split('\x00')
+ envDict = {}
+ for item in envItems:
+ if not '=' in item:
+ continue
+ k, v = item.split('=', 1)
+ envDict[k] = v
+ return envDict
+
+ def isSessionEnv(envDict):
+ if not envDict:
+ return False
+ if envDict.get(self.cookieName, 'notacookie') == self.cookie:
+ return True
+ return False
+
+ for path in glob.glob('/proc/*/'):
+ if not isSessionProcess(path):
+ continue
+ envFile = path + 'environ'
+ envDict = getEnvDict(envFile)
+ if isSessionEnv(envDict):
+ # print path
+ # print envDict
+ self._environment = envDict
+ if not self._environment:
+ raise RuntimeError("Can't find our environment!")
+ return self._environment
+
+ def wait(self):
+ self.script.wait()
+ return self.xserver.wait()
+
+ def stop(self):
+ try:
+ self.script.stop()
+ except OSError:
+ pass
+ self.xserver.stop()
+
+ def attemptLogout(self):
+ logoutScript = Script('dogtail-logout',
+ environ=self.environment)
+ logoutScript.start()
+ logoutScript.wait()
+
+ @property
+ def cookie(self):
+ if not self._cookie:
+ self._cookie = "%X" % random.getrandbits(16)
+ return self._cookie
+
+ def _buildXInitRC(self, fileObj):
+ lines = [
+ "export %s=%s" % (self.cookieName, self.cookie),
+ "gsettings set org.gnome.desktop.interface toolkit-accessibility true",
+ ". /etc/X11/xinit/xinitrc-common",
+ "export %s" % self.cookieName,
+ "exec -l $SHELL -c \"$CK_XINIT_SESSION $SSH_AGENT %s\"" %
+ (self.sessionBinary),
+ ""]
+
+ fileObj.write('\n'.join(lines).strip())
+ fileObj.flush()
diff --git a/build/lib.linux-x86_64-2.7/dogtail/tc.py b/build/lib.linux-x86_64-2.7/dogtail/tc.py
new file mode 100644
index 0000000000..c7ef9f5bc2
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/tc.py
@@ -0,0 +1,230 @@
+# -*- coding: utf-8 -*-
+"""Test Case magic
+
+Author: Ed Rousseau <rousseau@redhat.com>"""
+__author__ = "Ed Rousseau <rousseau@redhat.com>"
+
+import os
+import os.path
+from config import config
+from logging import ResultsLogger, TimeStamp
+from PIL import Image, ImageChops, ImageStat
+from __builtin__ import unicode, long
+
+
+class TC(object): # pragma: no cover
+
+ """
+ The Test Case Superclass
+ """
+ logger = ResultsLogger()
+
+ def __init__(self):
+ self.encoding = config.encoding
+ # ascii + unicode. 8 bit extended char has been ripped out
+ self.supportedtypes = (
+ "ascii", "utf-8", "utf-16", "utf-16-be", "utf-16-le", "unicode-escape", "raw-unicode-escape",
+ "big5", "gb18030", "eucJP", "eucKR", "shiftJIS")
+
+ # String comparison function
+ def compare(self, label, baseline, undertest, encoding=config.encoding):
+ """
+ Compares 2 strings to see if they are the same. The user may specify
+ the encoding to which the two strings are to be normalized for the
+ comparison. Default encoding is the default system encoding.
+ Normalization to extended 8 bit charactersets is not supported.
+
+ When the origin of either baseline or undertest is a text file whose
+ encoding is something other than ASCII, it is necessary to use
+ codecs.open() instead of open(), so the file's encoding may be
+ specified.
+ """
+ self.label = label.strip()
+ self.baseline = baseline
+ self.undertest = undertest
+ for string in [self.baseline, self.undertest]:
+ try:
+ string = unicode(string, 'utf-8')
+ except TypeError:
+ pass
+ self.encoding = encoding
+
+ # Normalize the encoding type for the comparaison based on
+ # self.encoding
+ if self.encoding in self.supportedtypes:
+ self.baseline = (self.baseline).encode(self.encoding)
+ self.undertest = (self.undertest).encode(self.encoding)
+ # Compare the strings
+ if self.baseline == self.undertest:
+ self.result = {self.label: "Passed"}
+ else:
+ self.result = {self.label: "Failed - " + self.encoding +
+ " strings do not match. " + self.baseline + " expected: Got " + self.undertest}
+ # Pass the test result to the ResultsLogger for writing
+ TC.logger.log(self.result)
+ return self.result
+
+ else:
+ # We should probably raise an exception here
+ self.result = {
+ self.label: "ERROR - " + self.encoding + " is not a supported encoding type"}
+ TC.logger.log(self.result)
+ return self.result
+
+
+# String Test Case subclass
+class TCString(TC): # pragma: no cover
+
+ """
+ String Test Case Class
+ """
+
+ def __init__(self):
+ TC.__init__(self)
+
+# Image test case subclass
+
+
+class TCImage(TC): # pragma: no cover
+
+ """
+ Image Test Case Class.
+ """
+
+ def compare(self, label, baseline, undertest):
+ for _file in (baseline, undertest):
+ if type(_file) is not unicode and type(_file) is not str:
+ raise TypeError("Need filenames!")
+ self.label = label.strip()
+ self.baseline = baseline.strip()
+ self.undertest = undertest.strip()
+ diffName = TimeStamp().fileStamp("diff") + ".png"
+ self.diff = os.path.normpath(
+ os.path.sep.join((config.scratchDir, diffName)))
+
+ self.baseImage = Image.open(self.baseline)
+ self.testImage = Image.open(self.undertest)
+ try:
+ if self.baseImage.size != self.testImage.size:
+ self.result = {
+ self.label: "Failed - images are different sizes"}
+ raise StopIteration
+
+ self.diffImage = ImageChops.difference(self.baseImage,
+ self.testImage)
+ self.diffImage.save(self.diff)
+ result = False
+ for stat in ('stddev', 'mean', 'sum2'):
+ for item in getattr(ImageStat.Stat(self.diffImage), stat):
+ if item:
+ self.result = {self.label: "Failed - see %s" %
+ self.diff}
+ raise StopIteration
+ else:
+ result = True
+ except StopIteration:
+ result = False
+
+ if result:
+ self.result = {self.label: "Passed"}
+
+ TC.logger.log(self.result)
+ return self.result
+
+
+class TCNumber(TC): # pragma: no cover
+
+ """
+ Number Comparaison Test Case Class
+ """
+
+ def __init__(self):
+ TC.__init__(self)
+ self.supportedtypes = ("int", "long", "float", "complex", "oct", "hex")
+
+ # Compare 2 numbers by the type provided in the type arg
+ def compare(self, label, baseline, undertest, type):
+ """
+ Compares 2 numbers to see if they are the same. The user may specify
+ how to normalize mixed type comparisons via the type argument.
+ """
+ self.label = label.strip()
+ self.baseline = baseline
+ self.undertest = undertest
+ self.type = type.strip()
+
+ # If we get a valid type, convert to that type and compare
+ if self.type in self.supportedtypes:
+ # Normalize for comparison
+ if self.type == "int":
+ self.baseline = int(self.baseline)
+ self.undertest = int(self.undertest)
+ elif self.type == "long":
+ self.baseline = long(self.baseline)
+ self.undertest = long(self.undertest)
+ elif self.type == "float":
+ self.baseline = float(self.baseline)
+ self.undertest = float(self.undertest)
+ else:
+ self.baseline = complex(self.baseline)
+ self.undertest = complex(self.undertest)
+
+ # compare
+ if self.baseline == self.undertest:
+ self.result = {self.label: "Passed - numbers are the same"}
+ else:
+ self.result = {self.label: "Failed - " + str(
+ self.baseline) + " expected: Got " + str(self.undertest)}
+ TC.logger.log(self.result)
+ return self.result
+ else:
+ self.result = {
+ self.label: "Failed - " + self.type + " is not in list of supported types"}
+ TC.logger.log(self.result)
+ return self.result
+
+
+class TCBool(TC): # pragma: no cover
+
+ def __init__(self):
+ pass
+
+ def compare(self, label, _bool):
+ """
+ If _bool is True, pass.
+ If _bool is False, fail.
+ """
+ if type(_bool) is not bool:
+ raise TypeError
+ if _bool:
+ result = {label: "Passed"}
+ else:
+ result = {label: "Failed"}
+ TC.logger.log(result)
+
+from tree import Node
+
+
+class TCNode(TC): # pragma: no cover
+
+ def __init__(self):
+ pass
+
+ def compare(self, label, baseline, undertest):
+ """
+ If baseline is None, simply check that undertest is a Node.
+ If baseline is a Node, check that it is equal to undertest.
+ """
+ if baseline is not None and not isinstance(baseline, Node):
+ raise TypeError
+
+ if not isinstance(undertest, Node):
+ result = {label: "Failed - %s is not a Node" % undertest}
+ elif baseline is None:
+ result = {label: "Passed - %s is a Node" % undertest}
+ elif isinstance(baseline, Node):
+ if baseline == undertest:
+ result = {label: "Passed - %s == %s" % (baseline, undertest)}
+ else:
+ result = {label: "Failed - %s != %s" % (baseline, undertest)}
+ TC.logger.log(result)
diff --git a/build/lib.linux-x86_64-2.7/dogtail/tree.py b/build/lib.linux-x86_64-2.7/dogtail/tree.py
new file mode 100644
index 0000000000..e6050bbb98
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/tree.py
@@ -0,0 +1,1318 @@
+"""Makes some sense of the AT-SPI API
+
+The tree API handles various things for you:
+ - fixes most timing issues
+ - can automatically generate (hopefully) highly-readable logs of what the
+script is doing
+ - traps various UI malfunctions, raising exceptions for them (again,
+hopefully improving the logs)
+
+The most important class is Node. Each Node is an element of the desktop UI.
+There is a tree of nodes, starting at 'root', with applications as its
+children, with the top-level windows and dialogs as their children. The various
+widgets that make up the UI appear as descendents in this tree. All of these
+elements (root, the applications, the windows, and the widgets) are represented
+as instances of Node in a tree (provided that the program of interest is
+correctly exporting its user-interface to the accessibility system). The Node
+class is a mixin for Accessible and the various Accessible interfaces.
+
+The Action class represents an action that the accessibility layer exports as
+performable on a specific node, such as clicking on it. It's a wrapper around
+Accessibility.Action.
+
+We often want to look for a node, based on some criteria, and this is provided
+by the Predicate class.
+
+Dogtail implements a high-level searching system, for finding a node (or
+nodes) satisfying whatever criteria you are interested in. It does this with
+a 'backoff and retry' algorithm. This fixes most timing problems e.g. when a
+dialog is in the process of opening but hasn't yet done so.
+
+If a search fails, it waits 'config.searchBackoffDuration' seconds, and then
+tries again, repeatedly. After several failed attempts (determined by
+config.searchWarningThreshold) it will start sending warnings about the search
+to the debug log. If it still can't succeed after 'config.searchCutoffCount'
+attempts, it raises an exception containing details of the search. You can see
+all of this process in the debug log by setting 'config.debugSearching' to True
+
+We also automatically add a short delay after each action
+('config.defaultDelay' gives the time in seconds). We'd hoped that the search
+backoff and retry code would eliminate the need for this, but unfortunately we
+still run into timing issues. For example, Evolution (and probably most
+other apps) set things up on new dialogs and wizard pages as they appear, and
+we can run into 'setting wars' where the app resets the widgetry to defaults
+after our script has already filled out the desired values, and so we lose our
+values. So we give the app time to set the widgetry up before the rest of the
+script runs.
+
+The classes trap various UI malfunctions and raise exceptions that better
+describe what went wrong. For example, they detects attempts to click on an
+insensitive UI element and raise a specific exception for this.
+
+Unfortunately, some applications do not set up the 'sensitive' state
+correctly on their buttons (e.g. Epiphany on form buttons in a web page). The
+current workaround for this is to set config.ensureSensitivity=False, which
+disables the sensitivity testing.
+
+Authors: Zack Cerza <zcerza@redhat.com>, David Malcolm <dmalcolm@redhat.com>
+"""
+__author__ = """Zack Cerza <zcerza@redhat.com>,
+David Malcolm <dmalcolm@redhat.com>
+"""
+
+from config import config
+if config.checkForA11y:
+ from utils import checkForA11y
+ checkForA11y()
+
+import predicate
+from time import sleep
+from utils import doDelay
+from utils import Blinker
+from utils import Lock
+import rawinput
+import path
+from __builtin__ import xrange
+
+from logging import debugLogger as logger
+
+try:
+ import pyatspi
+ import Accessibility
+except ImportError: # pragma: no cover
+ raise ImportError("Error importing the AT-SPI bindings")
+
+# We optionally import the bindings for libWnck.
+try:
+ from gi.repository import Wnck
+ gotWnck = True # pragma: no cover
+except ImportError:
+ # Skip this warning, since the functionality is almost entirely nonworking anyway.
+ # print "Warning: Dogtail could not import the Python bindings for
+ # libwnck. Window-manager manipulation will not be available."
+ gotWnck = False
+
+from gi.repository import GLib
+
+haveWarnedAboutChildrenLimit = False
+
+
+class SearchError(Exception):
+ pass
+
+
+class NotSensitiveError(Exception):
+
+ """
+ The widget is not sensitive.
+ """
+ message = "Cannot %s %s. It is not sensitive."
+
+ def __init__(self, action):
+ self.action = action
+
+ def __str__(self):
+ return self.message % (self.action.name, self.action.node.getLogString())
+
+
+class ActionNotSupported(Exception):
+
+ """
+ The widget does not support the requested action.
+ """
+ message = "Cannot do '%s' action on %s"
+
+ def __init__(self, actionName, node):
+ self.actionName = actionName
+ self.node = node
+
+ def __str__(self):
+ return self.message % (self.actionName, self.node.getLogString())
+
+
+class Action(object):
+
+ """
+ Class representing an action that can be performed on a specific node
+ """
+ # Valid types of actions we know about. Feel free to add any you see.
+ types = ('click',
+ 'press',
+ 'release',
+ 'activate',
+ 'jump',
+ 'check',
+ 'dock',
+ 'undock',
+ 'open',
+ 'menu')
+
+ def __init__(self, node, action, index):
+ self.node = node
+ self.__action = action
+ self.__index = index
+
+ @property
+ def name(self):
+ return self.__action.getName(self.__index)
+
+ @property
+ def description(self):
+ return self.__action.getDescription(self.__index)
+
+ @property
+ def keyBinding(self):
+ return self.__action.getKeyBinding(self.__index)
+
+ def __str__(self):
+ return "[action | %s | %s ]" % \
+ (self.name, self.keyBinding)
+
+ def do(self):
+ """
+ Performs the given tree.Action, with appropriate delays and logging.
+ """
+ logger.log("%s on %s" % (self.name, self.node.getLogString()))
+ if not self.node.sensitive:
+ if config.ensureSensitivity:
+ raise NotSensitiveError(self)
+ else:
+ nSE = NotSensitiveError(self)
+ logger.log("Warning: " + str(nSE))
+ if config.blinkOnActions:
+ self.node.blink()
+ result = self.__action.doAction(self.__index)
+ doDelay(config.actionDelay)
+ return result
+
+
+class Node(object):
+
+ """
+ A node in the tree of UI elements. This class is mixed in with
+ Accessibility.Accessible to both make it easier to use and to add
+ additional functionality. It also has a debugName which is set up
+ automatically when doing searches.
+ """
+
+ def __setupUserData(self):
+ try:
+ len(self.user_data)
+ except (AttributeError, TypeError):
+ self.user_data = {}
+
+ def debugName():
+ doc = "debug name assigned during search operations"
+
+ def fget(self):
+ self.__setupUserData()
+ return self.user_data.get('debugName', None)
+
+ def fset(self, debugName):
+ self.__setupUserData()
+ self.user_data['debugName'] = debugName
+
+ return property(**locals())
+ debugName = debugName()
+ #
+ # Accessible
+ #
+
+ @property
+ def dead(self):
+ """Is the node dead (defunct) ?"""
+ try:
+ if self.roleName == 'invalid':
+ return True
+ self.role
+ self.name
+ if len(self) > 0:
+ self[0]
+ except:
+ return True
+ return False
+
+ @property
+ def children(self):
+ """a list of this Accessible's children"""
+ if self.parent and self.parent.roleName == 'hyper link':
+ print(self.parent.role)
+ return []
+ children = []
+ childCount = self.childCount
+ if childCount > config.childrenLimit:
+ global haveWarnedAboutChildrenLimit
+ if not haveWarnedAboutChildrenLimit:
+ logger.log("Only returning %s children. You may change "
+ "config.childrenLimit if you wish. This message will only"
+ " be printed once." % str(config.childrenLimit))
+ haveWarnedAboutChildrenLimit = True
+ childCount = config.childrenLimit
+ for i in range(childCount):
+ # Workaround for GNOME bug #465103
+ # also solution for GNOME bug #321273
+ try:
+ child = self[i]
+ except LookupError:
+ child = None
+ if child:
+ children.append(child)
+
+ invalidChildren = childCount - len(children)
+ if invalidChildren and config.debugSearching:
+ logger.log("Skipped %s invalid children of %s" %
+ (invalidChildren, str(self)))
+ try:
+ ht = self.queryHypertext()
+ for li in range(ht.getNLinks()):
+ link = ht.getLink(li)
+ for ai in range(link.nAnchors):
+ child = link.getObject(ai)
+ child.__setupUserData()
+ child.user_data['linkAnchor'] = \
+ LinkAnchor(node=child,
+ hypertext=ht,
+ linkIndex=li,
+ anchorIndex=ai)
+ children.append(child)
+ except (NotImplementedError, AttributeError):
+ pass
+
+ return children
+
+ roleName = property(Accessibility.Accessible.getRoleName)
+
+ role = property(Accessibility.Accessible.getRole)
+
+ indexInParent = property(Accessibility.Accessible.getIndexInParent)
+
+ #
+ # Action
+ #
+
+ # Needed to be renamed from doAction due to conflicts
+ # with 'Accessibility.Accessible.doAction' in gtk3 branch
+ def doActionNamed(self, name):
+ """
+ Perform the action with the specified name. For a list of actions
+ supported by this instance, check the 'actions' property.
+ """
+ actions = self.actions
+ if name in actions:
+ return actions[name].do()
+ raise ActionNotSupported(name, self)
+
+ @property
+ def actions(self):
+ """
+ A dictionary of supported action names as keys, with Action objects as
+ values. Common action names include:
+
+ 'click' 'press' 'release' 'activate' 'jump' 'check' 'dock' 'undock'
+ 'open' 'menu'
+ """
+ actions = {}
+ try:
+ action = self.queryAction()
+ for i in range(action.nActions):
+ a = Action(self, action, i)
+ actions[action.getName(i)] = a
+ finally:
+ return actions
+
+ def combovalue():
+ doc = "The value (as a string) currently selected in the combo box."
+
+ def fget(self):
+ return self.name
+
+ def fset(self, value):
+ logger.log("Setting combobox %s to '%s'" % (self.getLogString(),
+ value))
+ self.childNamed(childName=value).doActionNamed('click')
+ doDelay()
+
+ return property(**locals())
+ combovalue = combovalue()
+ #
+ # Hypertext and Hyperlink
+ #
+
+ @property
+ def URI(self):
+ try:
+ return self.user_data['linkAnchor'].URI
+ except (KeyError, AttributeError):
+ raise NotImplementedError
+
+ #
+ # Text and EditableText
+ #
+ def text():
+ doc = """For instances with an AccessibleText interface, the text as a
+ string. This is read-only, unless the instance also has an
+ AccessibleEditableText interface. In this case, you can write values
+ to the attribute. This will get logged in the debug log, and a delay
+ will be added.
+
+ If this instance corresponds to a password entry, use the passwordText
+ property instead."""
+
+ def fget(self):
+ try:
+ return self.queryText().getText(0, -1)
+ except NotImplementedError:
+ return None
+
+ def fset(self, text):
+ try:
+ if config.debugSearching:
+ msg = "Setting text of %s to %s"
+ # Let's not get too crazy if 'text' is really large...
+ # FIXME: Sometimes the next line screws up Unicode strings.
+ if len(text) > 140:
+ txt = text[:134] + " [...]"
+ else:
+ txt = text
+ logger.log(msg % (self.getLogString(), "'%s'" % txt))
+ self.queryEditableText().setTextContents(text)
+ except NotImplementedError:
+ raise AttributeError("can't set attribute")
+
+ return property(**locals())
+ text = text()
+
+ def caretOffset():
+
+ def fget(self):
+ """For instances with an AccessibleText interface, the caret
+ offset as an integer."""
+ return self.queryText().caretOffset
+
+ def fset(self, offset):
+ return self.queryText().setCaretOffset(offset)
+
+ return property(**locals())
+ caretOffset = caretOffset()
+
+ #
+ # Component
+ #
+
+ @property
+ def position(self):
+ """A tuple containing the position of the Accessible: (x, y)"""
+ return self.queryComponent().getPosition(pyatspi.DESKTOP_COORDS)
+
+ @property
+ def size(self):
+ """A tuple containing the size of the Accessible: (w, h)"""
+ return self.queryComponent().getSize()
+
+ @property
+ def extents(self):
+ """A tuple containing the location and size of the Accessible:
+ (x, y, w, h)"""
+ try:
+ ex = self.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
+ return (ex.x, ex.y, ex.width, ex.height)
+ except NotImplementedError:
+ return None
+
+ def contains(self, x, y):
+ try:
+ return self.queryComponent().contains(x, y, pyatspi.DESKTOP_COORDS)
+ except NotImplementedError:
+ return False
+
+ def getChildAtPoint(self, x, y):
+ node = self
+ while True:
+ try:
+ child = node.queryComponent().getAccessibleAtPoint(x, y,
+ pyatspi.DESKTOP_COORDS)
+ if child and child.contains(x, y):
+ node = child
+ else:
+ break
+ except NotImplementedError:
+ break
+ if node and node.contains(x, y):
+ return node
+ else:
+ return None
+
+ def grabFocus(self):
+ "Attempts to set the keyboard focus to this Accessible."
+ return self.queryComponent().grabFocus()
+
+ # def blink(self, count=2):
+ #"""
+ # Blink, baby!
+ #"""
+ # if not self.extents: return False
+ # else:
+ #(x, y, w, h) = self.extents
+ #from utils import Blinker
+ #blinkData = Blinker(x, y, w, h, count)
+ # return True
+
+ def click(self, button=1):
+ """
+ Generates a raw mouse click event, using the specified button.
+ - 1 is left,
+ - 2 is middle,
+ - 3 is right.
+ """
+ logger.log("Clicking on %s" % self.getLogString())
+ clickX = self.position[0] + self.size[0] / 2
+ clickY = self.position[1] + self.size[1] / 2
+ if config.debugSearching:
+ logger.log("raw click on %s %s at (%s,%s)" %
+ (self.name, self.getLogString(), str(clickX), str(clickY)))
+ rawinput.click(clickX, clickY, button)
+
+ def doubleClick(self, button=1):
+ """
+ Generates a raw mouse double-click event, using the specified button.
+ """
+ clickX = self.position[0] + self.size[0] / 2
+ clickY = self.position[1] + self.size[1] / 2
+ if config.debugSearching:
+ logger.log("raw click on %s %s at (%s,%s)" %
+ (self.name, self.getLogString(), str(clickX), str(clickY)))
+ rawinput.doubleClick(clickX, clickY, button)
+
+ def point(self, mouseDelay=None):
+ """
+ Move mouse cursor to the center of the widget.
+ """
+ pointX = self.position[0] + self.size[0] / 2
+ pointY = self.position[1] + self.size[1] / 2
+ logger.log("Pointing on %s %s at (%s,%s)" %
+ (self.name, self.getLogString(), str(pointX), str(pointY)))
+ rawinput.registry.generateMouseEvent(pointX, pointY, 'abs')
+ if mouseDelay:
+ doDelay(mouseDelay)
+ else:
+ doDelay()
+
+ #
+ # RelationSet
+ #
+ @property
+ def labeler(self):
+ """'labeller' (read-only list of Node instances):
+ The node(s) that is/are a label for this node. Generated from
+ 'relations'.
+ """
+ relationSet = self.getRelationSet()
+ for relation in relationSet:
+ if relation.getRelationType() == pyatspi.RELATION_LABELLED_BY:
+ if relation.getNTargets() == 1:
+ return relation.getTarget(0)
+ targets = []
+ for i in range(relation.getNTargets()):
+ targets.append(relation.getTarget(i))
+ return targets
+ labeller = labeler
+
+ @property
+ def labelee(self):
+ """'labellee' (read-only list of Node instances):
+ The node(s) that this node is a label for. Generated from 'relations'.
+ """
+ relationSet = self.getRelationSet()
+ for relation in relationSet:
+ if relation.getRelationType() == pyatspi.RELATION_LABEL_FOR:
+ if relation.getNTargets() == 1:
+ return relation.getTarget(0)
+ targets = []
+ for i in range(relation.getNTargets()):
+ targets.append(relation.getTarget(i))
+ return targets
+ labellee = labelee
+
+ #
+ # StateSet
+ #
+ @property
+ def sensitive(self):
+ """Is the Accessible sensitive (i.e. not greyed out)?"""
+ return self.getState().contains(pyatspi.STATE_SENSITIVE)
+
+ @property
+ def showing(self):
+ return self.getState().contains(pyatspi.STATE_SHOWING)
+
+ @property
+ def focusable(self):
+ """Is the Accessible capable of having keyboard focus?"""
+ return self.getState().contains(pyatspi.STATE_FOCUSABLE)
+
+ @property
+ def focused(self):
+ """Does the Accessible have keyboard focus?"""
+ return self.getState().contains(pyatspi.STATE_FOCUSED)
+
+ @property
+ def checked(self):
+ """Is the Accessible a checked checkbox?"""
+ return self.getState().contains(pyatspi.STATE_CHECKED)
+
+ @property
+ def isChecked(self):
+ """Is the Accessible a checked checkbox? Compatibility property, same as Node.checked."""
+ return self.checked
+
+ #
+ # Selection
+ #
+
+ def selectAll(self):
+ """Selects all children."""
+ result = self.querySelection().selectAll()
+ doDelay()
+ return result
+
+ def deselectAll(self):
+ """Deselects all selected children."""
+ result = self.querySelection().clearSelection()
+ doDelay()
+ return result
+
+ def select(self):
+ """Selects the Accessible."""
+ try:
+ parent = self.parent
+ except AttributeError:
+ raise NotImplementedError
+ result = parent.querySelection().selectChild(self.indexInParent)
+ doDelay()
+ return result
+
+ def deselect(self):
+ """Deselects the Accessible."""
+ try:
+ parent = self.parent
+ except AttributeError:
+ raise NotImplementedError
+ result = parent.querySelection().deselectChild(self.indexInParent)
+ doDelay()
+ return result
+
+ @property
+ def isSelected(self):
+ """Is the Accessible selected? Compatibility property, same as Node.selected."""
+ try:
+ parent = self.parent
+ except AttributeError:
+ raise NotImplementedError
+ return parent.querySelection().isChildSelected(self.indexInParent)
+
+ @property
+ def selected(self):
+ """Is the Accessible selected?"""
+ return self.isSelected
+
+ @property
+ def selectedChildren(self):
+ """Returns a list of children that are selected."""
+ # TODO: hideChildren for Hyperlinks?
+ selection = self.querySelection()
+ selectedChildren = []
+ for i in xrange(selection.nSelectedChildren):
+ selectedChildren.append(selection.getSelectedChild(i))
+
+ #
+ # Value
+ #
+
+ def value():
+ doc = "The value contained by the AccessibleValue interface."
+
+ def fget(self):
+ try:
+ return self.queryValue().currentValue
+ except NotImplementedError:
+ pass
+
+ def fset(self, value):
+ self.queryValue().currentValue = value
+
+ return property(**locals())
+ value = value()
+
+ @property
+ def minValue(self):
+ """The minimum value of self.value"""
+ try:
+ return self.queryValue().minimumValue
+ except NotImplementedError:
+ pass
+
+ @property
+ def minValueIncrement(self):
+ """The minimum value increment of self.value"""
+ try:
+ return self.queryValue().minimumIncrement
+ except NotImplementedError:
+ pass
+
+ @property
+ def maxValue(self):
+ """The maximum value of self.value"""
+ try:
+ return self.queryValue().maximumValue
+ except NotImplementedError:
+ pass
+
+ def typeText(self, string):
+ """
+ Type the given text into the node, with appropriate delays and
+ logging.
+ """
+ logger.log("Typing text into %s: '%s'" % (self.getLogString(), string))
+
+ if self.focusable:
+ if not self.focused:
+ try:
+ self.grabFocus()
+ except Exception:
+ logger.log("Node is focusable but I can't grabFocus!")
+ rawinput.typeText(string)
+ else:
+ logger.log("Node is not focusable; falling back to inserting text")
+ et = self.queryEditableText()
+ et.insertText(self.caretOffset, string, len(string))
+ self.caretOffset += len(string)
+ doDelay()
+
+ def keyCombo(self, comboString):
+ if config.debugSearching:
+ logger.log("Pressing keys '%s' into %s" %
+ (comboString, self.getLogString()))
+ if self.focusable:
+ if not self.focused:
+ try:
+ self.grabFocus()
+ except Exception:
+ logger.log("Node is focusable but I can't grabFocus!")
+ else:
+ logger.log("Node is not focusable; trying key combo anyway")
+ rawinput.keyCombo(comboString)
+
+ def getLogString(self):
+ """
+ Get a string describing this node for the logs,
+ respecting the config.absoluteNodePaths boolean.
+ """
+ if config.absoluteNodePaths:
+ return self.getAbsoluteSearchPath()
+ else:
+ return str(self)
+
+ def satisfies(self, pred):
+ """
+ Does this node satisfy the given predicate?
+ """
+ # the logic is handled by the predicate:
+ assert isinstance(pred, predicate.Predicate)
+ return pred.satisfiedByNode(self)
+
+ def dump(self, type='plain', fileName=None):
+ import dump
+ dumper = getattr(dump, type)
+ dumper(self, fileName)
+
+ def getAbsoluteSearchPath(self):
+ """
+ FIXME: this needs rewriting...
+ Generate a SearchPath instance giving the 'best'
+ way to find the Accessible wrapped by this node again, starting
+ at the root and applying each search in turn.
+
+ This is somewhat analagous to an absolute path in a filesystem,
+ except that some of searches may be recursive, rather than just
+ searching direct children.
+
+ Used by the recording framework for identifying nodes in a
+ persistent way, independent of the style of script being
+ written.
+
+ FIXME: try to ensure uniqueness
+ FIXME: need some heuristics to get 'good' searches, whatever
+ that means
+ """
+ if config.debugSearchPaths:
+ logger.log("getAbsoluteSearchPath(%s)" % self)
+
+ if self.roleName == 'application':
+ result = path.SearchPath()
+ result.append(predicate.IsAnApplicationNamed(self.name), False)
+ return result
+ else:
+ if self.parent:
+ (ancestor, pred, isRecursive) = self.getRelativeSearch()
+ if config.debugSearchPaths:
+ logger.log("got ancestor: %s" % ancestor)
+
+ ancestorPath = ancestor.getAbsoluteSearchPath()
+ ancestorPath.append(pred, isRecursive)
+ return ancestorPath
+ else:
+ # This should be the root node:
+ return path.SearchPath()
+
+ def getRelativeSearch(self):
+ """
+ Get a (ancestorNode, predicate, isRecursive) triple that identifies the
+ best way to find this Node uniquely.
+ FIXME: or None if no such search exists?
+ FIXME: may need to make this more robust
+ FIXME: should this be private?
+ """
+ if config.debugSearchPaths:
+ logger.log("getRelativeSearchPath(%s)" % self)
+
+ assert self
+ assert self.parent
+
+ isRecursive = False
+ ancestor = self.parent
+
+ # iterate up ancestors until you reach an identifiable one,
+ # setting the search to be isRecursive if need be:
+ while not self.__nodeIsIdentifiable(ancestor):
+ ancestor = ancestor.parent
+ isRecursive = True
+
+ # Pick the most appropriate predicate for finding this node:
+ if self.labellee:
+ if self.labellee.name:
+ return (ancestor, predicate.IsLabelledAs(self.labellee.name), isRecursive)
+
+ if self.roleName == 'menu':
+ return (ancestor, predicate.IsAMenuNamed(self.name), isRecursive)
+ elif self.roleName == 'menu item' or self.roleName == 'check menu item':
+ return (ancestor, predicate.IsAMenuItemNamed(self.name), isRecursive)
+ elif self.roleName == 'text':
+ return (ancestor, predicate.IsATextEntryNamed(self.name), isRecursive)
+ elif self.roleName == 'push button':
+ return (ancestor, predicate.IsAButtonNamed(self.name), isRecursive)
+ elif self.roleName == 'frame':
+ return (ancestor, predicate.IsAWindowNamed(self.name), isRecursive)
+ elif self.roleName == 'dialog':
+ return (ancestor, predicate.IsADialogNamed(self.name), isRecursive)
+ else:
+ pred = predicate.GenericPredicate(
+ name=self.name, roleName=self.roleName)
+ return (ancestor, pred, isRecursive)
+
+ def __nodeIsIdentifiable(self, ancestor):
+ if ancestor.labellee:
+ return True
+ elif ancestor.name:
+ return True
+ elif not ancestor.parent:
+ return True
+ else:
+ return False
+
+ def _fastFindChild(self, pred, recursive=True):
+ """
+ Searches for an Accessible using methods from pyatspi.utils
+ """
+ if isinstance(pred, predicate.Predicate):
+ pred = pred.satisfiedByNode
+ if not recursive:
+ cIter = iter(self)
+ while True:
+ try:
+ child = cIter.next()
+ except StopIteration:
+ break
+ if child is not None:
+ if pred(child):
+ return child
+ else:
+ return pyatspi.utils.findDescendant(self, pred)
+
+ def findChild(self, pred, recursive=True, debugName=None,
+ retry=True, requireResult=True):
+ """
+ Search for a node satisyfing the predicate, returning a Node.
+
+ If retry is True (the default), it makes multiple attempts,
+ backing off and retrying on failure, and eventually raises a
+ descriptive exception if the search fails.
+
+ If retry is False, it gives up after one attempt.
+
+ If requireResult is True (the default), an exception is raised after all
+ attempts have failed. If it is false, the function simply returns None.
+ """
+ def describeSearch(parent, pred, recursive, debugName):
+ """
+ Internal helper function
+ """
+ if recursive:
+ noun = "descendent"
+ else:
+ noun = "child"
+ if debugName is None:
+ debugName = pred.describeSearchResult()
+ return "%s of %s: %s" % (noun, parent.getLogString(), debugName)
+
+ assert isinstance(pred, predicate.Predicate)
+ numAttempts = 0
+ while numAttempts < config.searchCutoffCount:
+ if numAttempts >= config.searchWarningThreshold or config.debugSearching:
+ logger.log("searching for %s (attempt %i)" %
+ (describeSearch(self, pred, recursive, debugName), numAttempts))
+
+ result = self._fastFindChild(pred.satisfiedByNode, recursive)
+ if result:
+ assert isinstance(result, Node)
+ if debugName:
+ result.debugName = debugName
+ else:
+ result.debugName = pred.describeSearchResult()
+ return result
+ else:
+ if not retry:
+ break
+ numAttempts += 1
+ if config.debugSearching or config.debugSleep:
+ logger.log("sleeping for %f" %
+ config.searchBackoffDuration)
+ sleep(config.searchBackoffDuration)
+ if requireResult:
+ raise SearchError(describeSearch(self, pred, recursive, debugName))
+
+ # The canonical "search for multiple" method:
+ def findChildren(self, pred, recursive=True, isLambda=False):
+ """
+ Find all children/descendents satisfying the predicate.
+ """
+ if isLambda is True:
+ nodes = self.findChildren(predicate.GenericPredicate(), recursive=recursive)
+ result = []
+ for node in nodes:
+ try:
+ if pred(node):
+ result.append(node)
+ except:
+ pass
+ return result
+ if isinstance(pred, predicate.Predicate):
+ pred = pred.satisfiedByNode
+ if not recursive:
+ cIter = iter(self)
+ result = []
+ while True:
+ try:
+ child = cIter.next()
+ except StopIteration:
+ break
+ if child is not None and pred(child):
+ result.append(child)
+ return result
+ else:
+ descendants = []
+ while True:
+ try:
+ descendants = pyatspi.utils.findAllDescendants(self, pred)
+ break
+ except (GLib.GError, TypeError):
+ continue
+ return descendants
+
+ # The canonical "search above this node" method:
+ def findAncestor(self, pred):
+ """
+ Search up the ancestry of this node, returning the first Node
+ satisfying the predicate, or None.
+ """
+ assert isinstance(pred, predicate.Predicate)
+ candidate = self.parent
+ while candidate is not None:
+ if candidate.satisfies(pred):
+ return candidate
+ else:
+ candidate = candidate.parent
+ # Not found:
+ return None
+
+ # Various wrapper/helper search methods:
+ def child(self, name='', roleName='', description='', label='', recursive=True, retry=True, debugName=None):
+ """
+ Finds a child satisying the given criteria.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.GenericPredicate(name=name, roleName=roleName, description=description, label=label), recursive=recursive, retry=retry, debugName=debugName)
+
+ def isChild(self, name='', roleName='', description='', label='', recursive=True, retry=False, debugName=None):
+ """
+ Determines whether a child satisying the given criteria exists.
+
+ This is implemented using findChild, but will not automatically retry
+ if no such child is found. To make the function retry multiple times set retry to True.
+ Returns a boolean value depending on whether the child was eventually found. Similar to
+ 'child', yet it catches SearchError exception to provide for False results, will raise
+ any other exceptions. It also logs the search.
+ """
+ found = True
+ try:
+ self.findChild(
+ predicate.GenericPredicate(
+ name=name, roleName=roleName, description=description, label=label),
+ recursive=recursive, retry=retry, debugName=debugName)
+ except SearchError:
+ found = False
+ return found
+
+ def menu(self, menuName, recursive=True):
+ """
+ Search below this node for a menu with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsAMenuNamed(menuName=menuName), recursive)
+
+ def menuItem(self, menuItemName, recursive=True):
+ """
+ Search below this node for a menu item with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsAMenuItemNamed(menuItemName=menuItemName), recursive)
+
+ def textentry(self, textEntryName, recursive=True):
+ """
+ Search below this node for a text entry with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsATextEntryNamed(textEntryName=textEntryName), recursive)
+
+ def button(self, buttonName, recursive=True):
+ """
+ Search below this node for a button with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsAButtonNamed(buttonName=buttonName), recursive)
+
+ def childLabelled(self, labelText, recursive=True):
+ """
+ Search below this node for a child labelled with the given text.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsLabelledAs(labelText), recursive)
+
+ def childNamed(self, childName, recursive=True):
+ """
+ Search below this node for a child with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsNamed(childName), recursive)
+
+ def tab(self, tabName, recursive=True):
+ """
+ Search below this node for a tab with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsATabNamed(tabName=tabName), recursive)
+
+ def getUserVisibleStrings(self):
+ """
+ Get all user-visible strings in this node and its descendents.
+
+ (Could be implemented as an attribute)
+ """
+ result = []
+ if self.name:
+ result.append(self.name)
+ if self.description:
+ result.append(self.description)
+ try:
+ children = self.children
+ except Exception:
+ return result
+ for child in children:
+ result.extend(child.getUserVisibleStrings())
+ return result
+
+ def blink(self):
+ """
+ Blink, baby!
+ """
+ if not self.extents:
+ return False
+ else:
+ (x, y, w, h) = self.extents
+ Blinker(x, y, w, h)
+ return True
+
+
+class LinkAnchor(object):
+
+ """
+ Class storing info about an anchor within an Accessibility.Hyperlink, which
+ is in turn stored within an Accessibility.Hypertext.
+ """
+
+ def __init__(self, node, hypertext, linkIndex, anchorIndex):
+ self.node = node
+ self.hypertext = hypertext
+ self.linkIndex = linkIndex
+ self.anchorIndex = anchorIndex
+
+ @property
+ def link(self):
+ return self.hypertext.getLink(self.linkIndex)
+
+ @property
+ def URI(self):
+ return self.link.getURI(self.anchorIndex)
+
+
+class Root (Node):
+
+ """
+ FIXME:
+ """
+
+ def applications(self):
+ """
+ Get all applications.
+ """
+ return root.findChildren(predicate.GenericPredicate(
+ roleName="application"), recursive=False)
+
+ def application(self, appName, retry=True):
+ """
+ Gets an application by name, returning an Application instance
+ or raising an exception.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return root.findChild(predicate.IsAnApplicationNamed(appName), recursive=False, retry=retry)
+
+
+class Application (Node):
+
+ def dialog(self, dialogName, recursive=False):
+ """
+ Search below this node for a dialog with the given name,
+ returning a Window instance.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+
+ FIXME: should this method activate the dialog?
+ """
+ return self.findChild(predicate.IsADialogNamed(dialogName=dialogName), recursive)
+
+ def window(self, windowName, recursive=False):
+ """
+ Search below this node for a window with the given name,
+ returning a Window instance.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+
+ FIXME: this bit isn't true:
+ The window will be automatically activated (raised and focused
+ by the window manager) if wnck bindings are available.
+ """
+ result = self.findChild(
+ predicate.IsAWindowNamed(windowName=windowName), recursive)
+ # FIXME: activate the WnckWindow ?
+ # if gotWnck:
+ # result.activate()
+ return result
+
+ def getWnckApplication(self): # pragma: no cover
+ """
+ Get the wnck.Application instance for this application, or None
+
+ Currently implemented via a hack: requires the app to have a
+ window, and looks up the application of that window
+
+ wnck.Application can give you the pid, the icon, etc
+
+ FIXME: untested
+ """
+ window = self.child(roleName='frame')
+ if window:
+ wnckWindow = window.getWnckWindow()
+ return wnckWindow.get_application()
+
+
+class Window (Node):
+
+ def getWnckWindow(self): # pragma: no cover
+ """
+ Get the wnck.Window instance for this window, or None
+ """
+ # FIXME: this probably needs rewriting:
+ screen = Wnck.screen_get_default()
+
+ # You have to force an update before any of the wnck methods
+ # do anything:
+ screen.force_update()
+
+ for wnckWindow in screen.get_windows():
+ # FIXME: a dubious hack: search by window title:
+ if wnckWindow.get_name() == self.name:
+ return wnckWindow
+
+ def activate(self): # pragma: no cover
+ """
+ Activates the wnck.Window associated with this Window.
+
+ FIXME: doesn't yet work
+ """
+ wnckWindow = self.getWnckWindow()
+ # Activate it with a timestamp of 0; this may confuse
+ # alt-tabbing through windows etc:
+ # FIXME: is there a better way of getting a timestamp?
+ # gdk_x11_get_server_time (), with a dummy window
+ wnckWindow.activate(0)
+
+
+class Wizard (Window):
+
+ """
+ Note that the buttons of a GnomeDruid were not accessible until
+ recent versions of libgnomeui. This is
+ http://bugzilla.gnome.org/show_bug.cgi?id=157936
+ and is fixed in gnome-2.10 and gnome-2.12 (in CVS libgnomeui);
+ there's a patch attached to that bug.
+
+ This bug is known to affect FC3; fixed in FC5
+ """
+
+ def __init__(self, node, debugName=None):
+ Node.__init__(self, node)
+ if debugName:
+ self.debugName = debugName
+ logger.log("%s is on '%s' page" % (self, self.getPageTitle()))
+
+ def currentPage(self):
+ """
+ Get the current page of this wizard
+
+ FIXME: this is currently a hack, supporting only GnomeDruid
+ """
+ pageHolder = self.child(roleName='panel')
+ for child in pageHolder.children:
+ # current child has SHOWING state set, we hope:
+ # print child
+ # print child.showing
+ if child.showing:
+ return child
+ raise "Unable to determine current page of %s" % self
+
+ def getPageTitle(self):
+ """
+ Get the string title of the current page of this wizard
+
+ FIXME: this is currently a total hack, supporting only GnomeDruid
+ """
+ currentPage = self.currentPage()
+ return currentPage.child(roleName='panel').child(roleName='panel').child(roleName='label', recursive=False).text
+
+ def clickForward(self):
+ """
+ Click on the 'Forward' button to advance to next page of wizard.
+
+ It will log the title of the new page that is reached.
+
+ FIXME: what if it's Next rather than Forward ???
+
+ This will only work if your libgnomeui has accessible buttons;
+ see above.
+ """
+ fwd = self.child("Forward")
+ fwd.click()
+
+ # Log the new wizard page; it's helpful when debugging scripts
+ logger.log("%s is now on '%s' page" % (self, self.getPageTitle()))
+ # FIXME disabled for now (can't get valid page titles)
+
+ def clickApply(self):
+ """
+ Click on the 'Apply' button to advance to next page of wizard.
+ FIXME: what if it's Finish rather than Apply ???
+
+ This will only work if your libgnomeui has accessible buttons;
+ see above.
+ """
+ fwd = self.child("Apply")
+ fwd.click()
+
+ # FIXME: debug logging?
+
+Accessibility.Accessible.__bases__ = (
+ Application, Root, Node,) + Accessibility.Accessible.__bases__
+
+try:
+ root = pyatspi.Registry.getDesktop(0)
+ root.debugName = 'root'
+except Exception: # pragma: no cover
+ # Warn if AT-SPI's desktop object doesn't show up.
+ logger.log(
+ "Error: AT-SPI's desktop is not visible. Do you have accessibility enabled?")
+
+# Check that there are applications running. Warn if none are.
+children = root.children
+if not children: # pragma: no cover
+ logger.log(
+ "Warning: AT-SPI's desktop is visible but it has no children. Are you running any AT-SPI-aware applications?")
+del children
+
+import os
+# sniff also imports from tree and we don't want to run this code from
+# sniff itself
+if not os.path.exists('/tmp/sniff_running.lock'):
+ if not os.path.exists('/tmp/sniff_refresh.lock'): # may have already been locked by dogtail.procedural
+ # tell sniff not to use auto-refresh while script using this module is
+ # running
+ sniff_lock = Lock(lockname='sniff_refresh.lock', randomize=False)
+ try:
+ sniff_lock.lock()
+ except OSError: # pragma: no cover
+ pass # lock was already present from other script instance or leftover from killed instance
+ # lock should unlock automatically on script exit.
+
+# Convenient place to set some debug variables:
+#config.debugSearching = True
+#config.absoluteNodePaths = True
+#config.logDebugToFile = False
diff --git a/build/lib.linux-x86_64-2.7/dogtail/utils.py b/build/lib.linux-x86_64-2.7/dogtail/utils.py
new file mode 100644
index 0000000000..be3c78adec
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/utils.py
@@ -0,0 +1,395 @@
+# -*- coding: utf-8 -*-
+"""
+Various utilities
+
+Authors: Ed Rousseau <rousseau@redhat.com>, Zack Cerza <zcerza@redhat.com, David Malcolm <dmalcolm@redhat.com>
+"""
+
+__author__ = """Ed Rousseau <rousseau@redhat.com>,
+Zack Cerza <zcerza@redhat.com,
+David Malcolm <dmalcolm@redhat.com>
+"""
+
+import os
+import sys
+import subprocess
+import cairo
+import predicate
+import errno
+import shlex
+
+import gi
+gi.require_version('Gtk', '3.0')
+gi.require_version('Gdk', '3.0')
+
+from gi.repository import Gtk
+from gi.repository import GLib
+from config import config
+from time import sleep
+from logging import debugLogger as logger
+from logging import TimeStamp
+from __builtin__ import file
+
+
+def screenshot(file='screenshot.png', timeStamp=True):
+ """
+ This function wraps the ImageMagick import command to take a screenshot.
+
+ The file argument may be specified as 'foo', 'foo.png', or using any other
+ extension that ImageMagick supports. PNG is the default.
+
+ By default, screenshot filenames are in the format of foo_YYYYMMDD-hhmmss.png .
+ The timeStamp argument may be set to False to name the file foo.png.
+ """
+ if not isinstance(timeStamp, bool):
+ raise TypeError("timeStampt must be True or False")
+ # config is supposed to create this for us. If it's not there, bail.
+ assert os.path.isdir(config.scratchDir)
+
+ baseName = ''.join(file.split('.')[0:-1])
+ fileExt = file.split('.')[-1].lower()
+ if not baseName:
+ baseName = file
+ fileExt = 'png'
+
+ if timeStamp:
+ ts = TimeStamp()
+ newFile = ts.fileStamp(baseName) + '.' + fileExt
+ path = config.scratchDir + newFile
+ else:
+ newFile = baseName + '.' + fileExt
+ path = config.scratchDir + newFile
+
+ from gi.repository import Gdk
+ from gi.repository import GLib
+ from gi.repository import GdkPixbuf
+ rootWindow = Gdk.get_default_root_window()
+ geometry = rootWindow.get_geometry()
+ pixbuf = GdkPixbuf.Pixbuf(colorspace=GdkPixbuf.Colorspace.RGB,
+ has_alpha=False,
+ bits_per_sample=8,
+ width=geometry[2],
+ height=geometry[3])
+
+ pixbuf = Gdk.pixbuf_get_from_window(rootWindow, 0, 0,
+ geometry[2], geometry[3])
+ # GdkPixbuf.Pixbuf.save() needs 'jpeg' and not 'jpg'
+ if fileExt == 'jpg':
+ fileExt = 'jpeg'
+ try:
+ pixbuf.savev(path, fileExt, [], [])
+ except GLib.GError:
+ raise ValueError("Failed to save screenshot in %s format" % fileExt)
+ assert os.path.exists(path)
+ logger.log("Screenshot taken: " + path)
+ return path
+
+
+def run(string, timeout=config.runTimeout, interval=config.runInterval, desktop=None, dumb=False, appName=''):
+ """
+ Runs an application. [For simple command execution such as 'rm *', use os.popen() or os.system()]
+ If dumb is omitted or is False, polls at interval seconds until the application is finished starting, or until timeout is reached.
+ If dumb is True, returns when timeout is reached.
+ """
+ if not desktop:
+ from tree import root as desktop
+ args = shlex.split(string)
+ os.environ['GTK_MODULES'] = 'gail:atk-bridge'
+ pid = subprocess.Popen(args, env=os.environ).pid
+
+ if not appName:
+ appName = args[0]
+
+ if dumb:
+ # We're starting a non-AT-SPI-aware application. Disable startup
+ # detection.
+ doDelay(timeout)
+ else:
+ # Startup detection code
+ # The timing here is not totally precise, but it's good enough for now.
+ time = 0
+ while time < timeout:
+ time = time + interval
+ try:
+ for child in desktop.children[::-1]:
+ if child.name == appName:
+ for grandchild in child.children:
+ if grandchild.roleName == 'frame':
+ from procedural import focus
+ focus.application.node = child
+ doDelay(interval)
+ return pid
+ except AttributeError: # pragma: no cover
+ pass
+ doDelay(interval)
+ return pid
+
+
+def doDelay(delay=None):
+ """
+ Utility function to insert a delay (with logging and a configurable
+ default delay)
+ """
+ if delay is None:
+ delay = config.defaultDelay
+ if config.debugSleep:
+ logger.log("sleeping for %f" % delay)
+ sleep(delay)
+
+
+class Highlight (Gtk.Window): # pragma: no cover
+
+ def __init__(self, x, y, w, h): # pragma: no cover
+ super(Highlight, self).__init__()
+ self.set_decorated(False)
+ self.set_has_resize_grip(False)
+ self.set_default_size(w, h)
+ self.screen = self.get_screen()
+ self.visual = self.screen.get_rgba_visual()
+ if self.visual is not None and self.screen.is_composited():
+ self.set_visual(self.visual)
+ self.set_app_paintable(True)
+ self.connect("draw", self.area_draw)
+ self.show_all()
+ self.move(x, y)
+
+ def area_draw(self, widget, cr): # pragma: no cover
+ cr.set_source_rgba(.0, .0, .0, 0.0)
+ cr.set_operator(cairo.OPERATOR_SOURCE)
+ cr.paint()
+ cr.set_operator(cairo.OPERATOR_OVER)
+ cr.set_source_rgb(0.9, 0.1, 0.1)
+ cr.set_line_width(6)
+ cr.rectangle(0, 0, self.get_size()[0], self.get_size()[1])
+ cr.stroke()
+
+
+class Blinker(object): # pragma: no cover
+ INTERVAL_MS = 1000
+ main_loop = GLib.MainLoop()
+
+ def __init__(self, x, y, w, h): # pragma: no cover
+ self.highlight_window = Highlight(x, y, w, h)
+ if self.highlight_window.screen.is_composited() is not False:
+ self.timeout_handler_id = GLib.timeout_add(
+ Blinker.INTERVAL_MS, self.destroyHighlight)
+ self.main_loop.run()
+ else:
+ self.highlight_window.destroy()
+
+ def destroyHighlight(self): # pragma: no cover
+ self.highlight_window.destroy()
+ self.main_loop.quit()
+ return False
+
+
+class Lock(object):
+
+ """
+ A mutex implementation that uses atomicity of the mkdir operation in UNIX-like
+ systems. This can be used by scripts to provide for mutual exlusion, either in single
+ scripts using threads etc. or i.e. to handle sitations of possible collisions among
+ multiple running scripts. You can choose to make randomized single-script wise locks
+ or a more general locks if you do not choose to randomize the lockdir name
+ """
+
+ def __init__(self, location='/tmp', lockname='dogtail_lockdir_', randomize=True):
+ """
+ You can change the default lockdir location or name. Setting randomize to
+ False will result in no random string being appened to the lockdir name.
+ """
+ self.lockdir = os.path.join(os.path.normpath(location), lockname)
+ if randomize:
+ self.lockdir = "%s%s" % (self.lockdir, self.__getPostfix())
+
+ def lock(self):
+ """
+ Creates a lockdir based on the settings on Lock() instance creation.
+ Raises OSError exception of the lock is already present. Should be
+ atomic on POSIX compliant systems.
+ """
+ locked_msg = 'Dogtail lock: Already locked with the same lock'
+ if not os.path.exists(self.lockdir):
+ try:
+ os.mkdir(self.lockdir)
+ return self.lockdir
+ except OSError as e:
+ if e.errno == errno.EEXIST and os.path.isdir(self.lockdir):
+ raise OSError(locked_msg)
+ else:
+ raise OSError(locked_msg)
+
+ def unlock(self):
+ """
+ Removes a lock. Will raise OSError exception if the lock was not present.
+ Should be atomic on POSIX compliant systems.
+ """
+ import os # have to import here for situations when executed from __del__
+ if os.path.exists(self.lockdir):
+ try:
+ os.rmdir(self.lockdir)
+ except OSError as e:
+ if e.erron == errno.EEXIST:
+ raise OSError('Dogtail unlock: lockdir removed elsewhere!')
+ else:
+ raise OSError('Dogtail unlock: not locked')
+
+ def __del__(self):
+ """
+ Makes sure lock is removed when the process ends. Although not when killed indeed.
+ """
+ self.unlock()
+
+ def __getPostfix(self):
+ import random
+ import string
+ return ''.join(random.choice(string.letters + string.digits) for x in range(5))
+
+
+a11yDConfKey = 'org.gnome.desktop.interface'
+
+
+def isA11yEnabled():
+ """
+ Checks if accessibility is enabled via DConf.
+ """
+ from gi.repository.Gio import Settings
+ InterfaceSettings = Settings(a11yDConfKey)
+ dconfEnabled = InterfaceSettings.get_boolean('toolkit-accessibility')
+ if os.environ.get('GTK_MODULES', '').find('gail:atk-bridge') == -1:
+ envEnabled = False
+ else:
+ envEnabled = True # pragma: no cover
+ return (dconfEnabled or envEnabled)
+
+
+def bailBecauseA11yIsDisabled():
+ if sys.argv[0].endswith("pydoc"):
+ return # pragma: no cover
+ try:
+ if file("/proc/%s/cmdline" % os.getpid()).read().find('epydoc') != -1:
+ return # pragma: no cover
+ except: # pragma: no cover
+ pass # pragma: no cover
+ logger.log("Dogtail requires that Assistive Technology support be enabled."
+ "\nYou can enable accessibility with sniff or by running:\n"
+ "'gsettings set org.gnome.desktop.interface toolkit-accessibility true'\nAborting...")
+ sys.exit(1)
+
+
+def enableA11y(enable=True):
+ """
+ Enables accessibility via DConf.
+ """
+ from gi.repository.Gio import Settings
+ InterfaceSettings = Settings(schema=a11yDConfKey)
+ InterfaceSettings.set_boolean('toolkit-accessibility', enable)
+
+
+def checkForA11y():
+ """
+ Checks if accessibility is enabled, and halts execution if it is not.
+ """
+ if not isA11yEnabled(): # pragma: no cover
+ bailBecauseA11yIsDisabled()
+
+
+def checkForA11yInteractively(): # pragma: no cover
+ """
+ Checks if accessibility is enabled, and presents a dialog prompting the
+ user if it should be enabled if it is not already, then halts execution.
+ """
+ if isA11yEnabled():
+ return
+ from gi.repository import Gtk
+ dialog = Gtk.Dialog('Enable Assistive Technology Support?',
+ None,
+ Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
+ (Gtk.STOCK_QUIT, Gtk.ResponseType.CLOSE,
+ "_Enable", Gtk.ResponseType.ACCEPT))
+ question = """Dogtail requires that Assistive Technology Support be enabled for it to function. Would you like to enable Assistive Technology support now?
+
+Note that you will have to log out for the change to fully take effect.
+ """.strip()
+ dialog.set_default_response(Gtk.ResponseType.ACCEPT)
+ questionLabel = Gtk.Label(label=question)
+ questionLabel.set_line_wrap(True)
+ dialog.vbox.pack_start(questionLabel, True, True, 0)
+ dialog.show_all()
+ result = dialog.run()
+ if result == Gtk.ResponseType.ACCEPT:
+ logger.log("Enabling accessibility...")
+ enableA11y()
+ elif result == Gtk.ResponseType.CLOSE:
+ bailBecauseA11yIsDisabled()
+ dialog.destroy()
+
+
+class GnomeShell(object): # pragma: no cover
+
+ """
+ Utility class to help working with certain atributes of gnome-shell.
+ Currently that means handling the Application menu available for apps
+ on the top gnome-shell panel. Searching for the menu and its items is
+ somewhat tricky due to fuzzy a11y tree of gnome-shell, mainly since the
+ actual menu is not present as child to the menu-spawning button. Also,
+ the menus get constructed/destroyed on the fly with application focus
+ changes. Thus current application name as displayed plus a reference
+ known menu item (with 'Quit' as default) are required by these methods.
+ """
+
+ def __init__(self, classic_mode=False):
+ from tree import root
+ self.shell = root.application('gnome-shell')
+
+ def getApplicationMenuList(self, search_by_item='Quit'):
+ """
+ Returns list of all menu item nodes. Searches for the menu by a reference item.
+ Provide a different item name, if the 'Quit' is not present - but beware picking one
+ present elsewhere, like 'Lock' or 'Power Off' present under the user menu.
+ """
+ matches = self.shell.findChildren(
+ predicate.GenericPredicate(name=search_by_item, roleName='label'))
+ for match in matches:
+ ancestor = match.parent.parent.parent
+ if ancestor.roleName == 'panel':
+ return ancestor.findChildren(predicate.GenericPredicate(roleName='label'))
+ from tree import SearchError
+ raise SearchError("Could not find the Application menu based on '%s' item. Please provide an existing reference item"
+ % search_by_item)
+
+ def getApplicationMenuButton(self, app_name):
+ """
+ Returns the application menu 'button' node as present on the gnome-shell top panel.
+ """
+ try:
+ return self.shell[0][0][3].child(app_name, roleName='label')
+ except:
+ from tree import SearchError
+ raise SearchError(
+ "Application menu button of %s could not be found within gnome-shell!" % app_name)
+
+ def getApplicationMenuItem(self, item, search_by_item='Quit'):
+ """
+ Returns a particilar menu item node. Uses a different 'Quit' or custom item name for reference, but also
+ attempts to use the given item if the general reference fails.
+ """
+ try:
+ menu_items = self.getApplicationMenuList(search_by_item)
+ except:
+ menu_items = self.getApplicationMenuList(item)
+ for node in menu_items:
+ if node.name == item:
+ return node
+ raise Exception(
+ 'Could not find the item, did application focus change?')
+
+ def clickApplicationMenuItem(self, app_name, item, search_by_item='Quit'):
+ """
+ Executes the given menu item through opening the menu first followed
+ by a click at the particular item. The menu search reference 'Quit'
+ may be customized. Also attempts to use the given item for reference
+ if search fails with the default/custom one.
+ """
+ self.getApplicationMenuButton(app_name).click()
+ self.getApplicationMenuItem(item, search_by_item).click()
diff --git a/build/lib.linux-x86_64-2.7/dogtail/version.py b/build/lib.linux-x86_64-2.7/dogtail/version.py
new file mode 100644
index 0000000000..4d44080615
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/version.py
@@ -0,0 +1,56 @@
+"""Handles versioning of software packages
+
+Author: Dave Malcolm <dmalcolm@redhat.com>"""
+__author__ = 'Dave Malcolm <dmalcolm@redhat.com>'
+
+
+class Version(object):
+
+ """
+ Class representing a version of a software package.
+ Stored internally as a list of subversions, from major to minor.
+ Overloaded comparison operators ought to work sanely.
+ """
+
+ def __init__(self, versionList):
+ self.versionList = versionList
+
+ def fromString(versionString):
+ """
+ Parse a string of the form number.number.number
+ """
+ return Version(map(int, versionString.split(".")))
+ fromString = staticmethod(fromString)
+
+ def __str__(self):
+ return ".".join(map(str, self.versionList))
+
+ def __getNum(self):
+ tmpList = list(self.versionList)
+
+ while len(tmpList) < 5:
+ tmpList += [0]
+
+ num = 0
+ for i in range(len(tmpList)):
+ num *= 1000
+ num += tmpList[i]
+ return num
+
+ def __lt__(self, other):
+ return self.__getNum() < other.__getNum()
+
+ def __le__(self, other):
+ return self.__getNum() <= other.__getNum()
+
+ def __eq__(self, other):
+ return self.__getNum() == other.__getNum()
+
+ def __ne__(self, other):
+ return self.__getNum() != other.__getNum()
+
+ def __gt__(self, other):
+ return self.__getNum() > other.__getNum()
+
+ def __ge__(self, other):
+ return self.__getNum() >= other.__getNum()
diff --git a/build/lib.linux-x86_64-2.7/dogtail/wrapped.py b/build/lib.linux-x86_64-2.7/dogtail/wrapped.py
new file mode 100644
index 0000000000..30c0570fa6
--- /dev/null
+++ b/build/lib.linux-x86_64-2.7/dogtail/wrapped.py
@@ -0,0 +1,33 @@
+"""
+Superclasses for application wrappers
+
+Subclass these classes if you want to create application wrappers, e.g.:
+http://svn.gnome.org/viewvc/dogtail-tests/trunk/appwrappers/dogtail/appwrappers/gedit.py?view=markup
+"""
+__author__ = "Zack Cerza <zcerza@redhat.com>"
+import Accessibility
+
+
+def makeWrapperClass(wrappedClass, name): # pragma: no cover
+ class klass(object):
+
+ def __init__(self, obj):
+ self.obj = obj
+
+ def __getattr__(self, name):
+ if name == 'obj':
+ return self.__dict__['obj']
+ return getattr(self.obj, name)
+
+ def __setattr__(self, name, value):
+ if name == 'obj':
+ self.__dict__['obj'] = value
+ else:
+ return setattr(self.obj, name, value)
+
+ klass.__name__ = name
+ return klass
+
+Application = makeWrapperClass(Accessibility.Application,
+ "WrappedApplication")
+Node = makeWrapperClass(Accessibility.Accessible, "WrappedNode")
diff --git a/build/lib/dogtail/__init__.py b/build/lib/dogtail/__init__.py
new file mode 100644
index 0000000000..76a4a13fcb
--- /dev/null
+++ b/build/lib/dogtail/__init__.py
@@ -0,0 +1,16 @@
+# -*- coding: UTF-8 -*-
+"""
+GUI test tool and automation framework that uses Accessibility (a11y) technologies to communicate with desktop applications.
+
+Authors: Zack Cerza <zcerza@redhat.com>, Ed Rousseau <rousseau@redhat.com>, David Malcolm <dmalcolm@redhat.com>, Vita Humpa <vhumpa@redhat.com>
+"""
+
+__author__ = """Zack Cerza <zcerza@redhat.com>,
+Ed Rousseau <rousseau@redhat.com>,
+David Malcolm <dmalcolm@redhat.com>,
+Vita Humpa <vhumpa@redhat.com>"""
+__version__ = "0.9.0"
+__copyright__ = "Copyright © 2005-2014 Red Hat, Inc."
+__license__ = "GPL"
+__all__ = ("config", "predicate",
+ "procedural", "tc", "tree", "utils", "errors")
diff --git a/build/lib/dogtail/config.py b/build/lib/dogtail/config.py
new file mode 100644
index 0000000000..82e197cf52
--- /dev/null
+++ b/build/lib/dogtail/config.py
@@ -0,0 +1,219 @@
+"""
+The configuration module.
+"""
+__author__ = "Zack Cerza <zcerza@redhat.com>, David Malcolm <dmalcolm@redhat.com>"
+
+import os
+import sys
+import locale
+
+
+def _userTmpDir(baseName):
+ # i.e. /tmp/dogtail-foo
+ return '-'.join(('/'.join(('/tmp', baseName)), os.environ['USER']))
+
+
+class _Config(object):
+
+ """
+ Contains configuration parameters for the dogtail run.
+
+ scratchDir(str):
+ Directory where things like screenshots are stored.
+
+ dataDir(str):
+ Directory where related data files are located.
+
+ logDir(str):
+ Directory where dogtail.tc.TC*-generated logs are stored.
+
+ scriptName(str) [Read-Only]:
+ The name of the script being run.
+
+ encoding(str)
+ The encoding for text, used by dogtail.tc.TCString .
+
+ actionDelay(float):
+ The delay after an action is executed.
+
+ typingDelay(float):
+ The delay after a character is typed on the keyboard.
+
+ runInterval(float):
+ The interval at which dogtail.utils.run() and dogtail.procedural.run()
+ check to see if the application has started up.
+
+ runTimeout(int):
+ The timeout after which dogtail.utils.run() and dogtail.procedural.run()
+ give up on looking for the newly-started application.
+
+ searchBackoffDuration (float):
+ Time in seconds for which to delay when a search fails.
+
+ searchWarningThreshold (int):
+ Number of retries before logging the individual attempts at a search.
+
+ searchCutoffCount (int):
+ Number of times to retry when a search fails.
+
+ defaultDelay (float):
+ Default time in seconds to sleep when delaying.
+
+ childrenLimit (int):
+ When there are a very large number of children of a node, only return
+ this many, starting with the first.
+
+ debugSearching (boolean):
+ Whether to write info on search backoff and retry to the debug log.
+
+ debugSleep (boolean):
+ Whether to log whenever we sleep to the debug log.
+
+ debugSearchPaths (boolean):
+ Whether we should write out debug info when running the SearchPath
+ routines.
+
+ absoluteNodePaths (boolean):
+ Whether we should identify nodes in the logs with long 'abcolute paths', or
+ merely with a short 'relative path'. FIXME: give examples
+
+ ensureSensitivity (boolean):
+ Should we check that ui nodes are sensitive (not 'greyed out') before
+ performing actions on them? If this is True (the default) it will raise
+ an exception if this happens. Can set to False as a workaround for apps
+ and toolkits that don't report sensitivity properly.
+
+ debugTranslation (boolean):
+ Whether we should write out debug information from the translation/i18n
+ subsystem.
+
+ blinkOnActions (boolean):
+ Whether we should blink a rectangle around a Node when an action is
+ performed on it.
+
+ fatalErrors (boolean):
+ Whether errors encountered in dogtail.procedural should be considered
+ fatal. If True, exceptions will be raised. If False, warnings will be
+ passed to the debug logger.
+
+ checkForA11y (boolean):
+ Whether to check if accessibility is enabled. If not, just assume it is
+ (default True).
+
+ logDebugToFile (boolean):
+ Whether to write debug output to a log file.
+
+ logDebugToStdOut (boolean):
+ Whether to print log output to console or not (default True).
+ """
+ @property
+ def scriptName(self):
+ return os.path.basename(sys.argv[0]).replace('.py', '')
+
+ @property
+ def encoding(self):
+ return locale.getpreferredencoding().lower()
+
+ defaults = {
+ # Storage
+ 'scratchDir': '/'.join((_userTmpDir('dogtail'), '')),
+ 'dataDir': '/'.join((_userTmpDir('dogtail'), 'data', '')),
+ 'logDir': '/'.join((_userTmpDir('dogtail'), 'logs', '')),
+ 'scriptName': scriptName.fget(None),
+ 'encoding': encoding.fget(None),
+ 'configFile': None,
+ 'baseFile': None,
+
+ # Timing and Limits
+ 'actionDelay': 1.0,
+ 'typingDelay': 0.1,
+ 'runInterval': 0.5,
+ 'runTimeout': 30,
+ 'searchBackoffDuration': 0.5,
+ 'searchWarningThreshold': 3,
+ 'searchCutoffCount': 20,
+ 'defaultDelay': 0.5,
+ 'childrenLimit': 100,
+
+ # Debug
+ 'debugSearching': False,
+ 'debugSleep': False,
+ 'debugSearchPaths': False,
+ 'logDebugToStdOut': True,
+ 'absoluteNodePaths': False,
+ 'ensureSensitivity': False,
+ 'debugTranslation': False,
+ 'blinkOnActions': False,
+ 'fatalErrors': False,
+ 'checkForA11y': True,
+
+ # Logging
+ 'logDebugToFile': True
+ }
+
+ options = {}
+
+ invalidValue = "__INVALID__"
+
+ def __init__(self):
+ _Config.__createDir(_Config.defaults['scratchDir'])
+ _Config.__createDir(_Config.defaults['logDir'])
+ _Config.__createDir(_Config.defaults['dataDir'])
+
+ def __setattr__(self, name, value):
+ if name not in config.defaults:
+ raise AttributeError(name + " is not a valid option.")
+
+ elif _Config.defaults[name] != value or \
+ _Config.options.get(name, _Config.invalidValue) != value:
+ if 'Dir' in name:
+ _Config.__createDir(value)
+ if value[-1] != os.path.sep:
+ value = value + os.path.sep
+ elif name == 'logDebugToFile':
+ import logging
+ logging.debugLogger = logging.Logger('debug', value)
+ _Config.options[name] = value
+
+ def __getattr__(self, name):
+ try:
+ return _Config.options[name]
+ except KeyError:
+ try:
+ return _Config.defaults[name]
+ except KeyError:
+ raise AttributeError("%s is not a valid option." % name)
+
+ def __createDir(cls, dirName, perms=0o777):
+ """
+ Creates a directory (if it doesn't currently exist), creating any
+ parent directories it needs.
+
+ If perms is None, create with python's default permissions.
+ """
+ dirName = os.path.abspath(dirName)
+ # print "Checking for %s ..." % dirName,
+ if not os.path.isdir(dirName):
+ if perms:
+ umask = os.umask(0)
+ os.makedirs(dirName, perms)
+ os.umask(umask)
+ else:
+ # This is probably a dead code - no other functions call this without the permissions set
+ os.makedirs(dirName) # pragma: no cover
+ __createDir = classmethod(__createDir)
+
+ def load(self, dict):
+ """
+ Loads values from dict, preserving any options already set that are not overridden.
+ """
+ _Config.options.update(dict)
+
+ def reset(self):
+ """
+ Resets all settings to their defaults.
+ """
+ _Config.options = {}
+
+
+config = _Config()
diff --git a/build/lib/dogtail/distro.py b/build/lib/dogtail/distro.py
new file mode 100644
index 0000000000..6f70f19465
--- /dev/null
+++ b/build/lib/dogtail/distro.py
@@ -0,0 +1,362 @@
+"""Handles differences between different distributions
+
+Authors: Dave Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>"""
+__author__ = "Dave Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>"
+
+import os
+import re
+from version import Version
+from logging import debugLogger as logger
+
+
+class DistributionNotSupportedError(Exception): # pragma: no cover
+
+ """
+ This distribution is not supported.
+ """
+ PATCH_MESSAGE = "Please send patches to dogtail-devel-list@gnome.org"
+
+ def __init__(self, distro):
+ self.distro = distro
+
+ def __str__(self):
+ return self.distro + ". " + DistributionNotSupportedError.PATCH_MESSAGE
+
+
+class PackageNotFoundError(Exception):
+
+ """
+ Error finding the requested package.
+ """
+ pass
+
+global packageDb
+global distro
+
+
+class PackageDb(object):
+
+ """
+ Class to abstract the details of whatever software package database is in
+ use (RPM, APT, etc)
+ """
+
+ def __init__(self):
+ self.prefix = '/usr'
+ self.localePrefixes = [self.prefix + '/share/locale']
+
+ def getVersion(self, packageName):
+ """
+ Method to get the version of an installed package as a Version
+ instance (or raise an exception if not found)
+
+ Note: does not know about distributions' internal revision numbers.
+ """
+ raise NotImplementedError
+
+ def getFiles(self, packageName):
+ """
+ Method to get a list of filenames owned by the package, or raise an
+ exception if not found.
+ """
+ raise NotImplementedError
+
+ def getMoFiles(self, locale=None):
+ """
+ Method to get a list of all .mo files on the system, optionally for a
+ specific locale.
+ """
+ moFiles = {}
+
+ def appendIfMoFile(moFiles, dirName, fNames):
+ import re
+ for fName in fNames:
+ if re.match('(.*)\\.mo', fName):
+ moFiles[dirName + '/' + fName] = None
+
+ for localePrefix in self.localePrefixes:
+ if locale:
+ localePrefix = localePrefix + '/' + locale
+ os.path.walk(localePrefix, appendIfMoFile, moFiles)
+
+ return moFiles.keys()
+
+ def getDependencies(self, packageName):
+ """
+ Method to get a list of unique package names that this package
+ is dependent on, or raise an exception if the package is not
+ found.
+ """
+ raise NotImplementedError
+
+
+class _RpmPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+ def getVersion(self, packageName):
+ import rpm
+ ts = rpm.TransactionSet()
+ for header in ts.dbMatch("name", packageName):
+ return Version.fromString(header["version"])
+ raise PackageNotFoundError(packageName)
+
+ def getFiles(self, packageName):
+ import rpm
+ ts = rpm.TransactionSet()
+ for header in ts.dbMatch("name", packageName):
+ return header["filenames"]
+ raise PackageNotFoundError(packageName)
+
+ def getDependencies(self, packageName):
+ import rpm
+ ts = rpm.TransactionSet()
+ for header in ts.dbMatch("name", packageName):
+ # Simulate a set using a hash (to a dummy value);
+ # sets were only added in Python 2.4
+ result = {}
+
+ # Get the list of requirements; these are
+ # sometimes package names, but can also be
+ # so-names of libraries, and invented virtual
+ # ids
+ for requirement in header[rpm.RPMTAG_REQUIRES]:
+ # Get the name of the package providing
+ # this requirement:
+ for depPackageHeader in ts.dbMatch("provides", requirement):
+ depName = depPackageHeader['name']
+ if depName != packageName:
+ # Add to the Hash with a dummy value
+ result[depName] = None
+ return result.keys()
+ raise PackageNotFoundError(packageName)
+
+
+class _AptPackageDb(PackageDb):
+
+ def __init__(self):
+ PackageDb.__init__(self)
+ self.cache = None
+
+ def getVersion(self, packageName):
+ if not self.cache:
+ import apt_pkg
+ apt_pkg.init()
+ self.cache = apt_pkg.GetCache()
+ packages = self.cache.Packages
+ for package in packages:
+ if package.Name == packageName:
+ verString = re.match(
+ '.*Ver:\'(.*)-.*\' Section:', str(package.CurrentVer)).group(1)
+ return Version.fromString(verString)
+ raise PackageNotFoundError(packageName)
+
+ def getFiles(self, packageName):
+ files = []
+ list = os.popen('dpkg -L %s' % packageName).readlines()
+ if not list:
+ raise PackageNotFoundError(packageName)
+ else:
+ for line in list:
+ file = line.strip()
+ if file:
+ files.append(file)
+ return files
+
+ def getDependencies(self, packageName):
+ # Simulate a set using a hash (to a dummy value);
+ # sets were only added in Python 2.4
+ result = {}
+ if not self.cache:
+ import apt_pkg
+ apt_pkg.init()
+ self.cache = apt_pkg.GetCache()
+ packages = self.cache.Packages
+ for package in packages:
+ if package.Name == packageName:
+ current = package.CurrentVer
+ if not current:
+ raise PackageNotFoundError(packageName)
+ depends = current.DependsList
+ list = depends['Depends']
+ for dependency in list:
+ name = dependency[0].TargetPkg.Name
+ # Add to the hash using a dummy value
+ result[name] = None
+ return result.keys()
+
+
+class _UbuntuAptPackageDb(_AptPackageDb):
+
+ def __init__(self):
+ _AptPackageDb.__init__(self)
+ self.localePrefixes.append(self.prefix + '/share/locale-langpack')
+
+
+class _PortagePackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+ def getVersion(self, packageName):
+ # the portage utilities are almost always going to be in
+ # /usr/lib/portage/pym
+ import sys
+ sys.path.append('/usr/lib/portage/pym')
+ import portage
+ # FIXME: this takes the first package returned in the list, in the
+ # case that there are slotted packages, and removes the leading
+ # category such as 'sys-apps'
+ gentooPackageName = portage.db["/"][
+ "vartree"].dbapi.match(packageName)[0].split('/')[1]
+ # this removes the distribution specific versioning returning only the
+ # upstream version
+ upstreamVersion = portage.pkgsplit(gentooPackageName)[1]
+ # print "Version of package is: " + upstreamVersion
+ return Version.fromString(upstreamVersion)
+
+
+class _ConaryPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+ def getVersion(self, packageName):
+ from conaryclient import ConaryClient
+ client = ConaryClient()
+ dbVersions = client.db.getTroveVersionList(packageName)
+ if not len(dbVersions):
+ raise PackageNotFoundError(packageName)
+ return dbVersions[0].trailingRevision().asString().split("-")[0]
+
+# getVersion not implemented because on Solaris multiple modules are installed
+# in single packages, so it is hard to tell what version number of a specific
+# module.
+
+
+class _SolarisPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+
+class JhBuildPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+ prefixes = []
+ prefixes.append(os.environ['LD_LIBRARY_PATH'])
+ prefixes.append(os.environ['XDG_CONFIG_DIRS'])
+ prefixes.append(os.environ['PKG_CONFIG_PATH'])
+ self.prefix = os.path.commonprefix(prefixes)
+ self.localePrefixes.append(self.prefix + '/share/locale')
+
+ def getDependencies(self, packageName):
+ result = {}
+ lines = os.popen('jhbuild list ' + packageName).readlines()
+ for line in lines:
+ if line:
+ result[line.strip()] = None
+ return result.keys()
+
+
+class Distro(object):
+
+ """
+ Class representing a distribution.
+
+ Scripts may want to do arbitrary logic based on whichever distro is in use
+ (e.g. handling differences in names of packages, distribution-specific
+ patches, etc.)
+
+ We can either create methods in the Distro class to handle these, or we
+ can use constructs like isinstance(distro, Ubuntu) to handle this. We can
+ even create hierarchies of distro subclasses to handle this kind of thing
+ (could get messy fast though)
+ """
+
+
+class Fedora(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _RpmPackageDb()
+
+
+class RHEL(Fedora): # pragma: no cover
+ pass
+
+
+class Debian(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _AptPackageDb()
+
+
+class Ubuntu(Debian):
+
+ def __init__(self):
+ self.packageDb = _UbuntuAptPackageDb()
+
+
+class Suse(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _RpmPackageDb()
+
+
+class Gentoo(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _PortagePackageDb()
+
+
+class Conary(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _ConaryPackageDb()
+
+
+class Solaris(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _SolarisPackageDb()
+
+
+class JHBuild(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = JhBuildPackageDb()
+
+
+def detectDistro():
+ logger.log("Detecting distribution:", newline=False)
+
+ if os.environ.get("CERTIFIED_GNOMIE", "no") == "yes":
+ distro = JHBuild() # pragma: no cover
+ elif os.path.exists("/etc/SuSE-release"):
+ distro = Suse() # pragma: no cover
+ elif os.path.exists("/etc/fedora-release"):
+ distro = Fedora() # pragma: no cover
+ elif os.path.exists("/etc/redhat-release"):
+ distro = RHEL() # pragma: no cover
+ elif os.path.exists("/usr/share/doc/ubuntu-minimal"):
+ distro = Ubuntu()
+ elif os.path.exists("/etc/debian_version"): # pragma: no cover
+ distro = Debian() # pragma: no cover
+ elif os.path.exists("/etc/gentoo-release"): # pragma: no cover
+ distro = Gentoo() # pragma: no cover
+ elif os.path.exists("/etc/slackware-version"): # pragma: no cover
+ raise DistributionNotSupportedError("Slackware") # pragma: no cover
+ elif os.path.exists("/var/lib/conarydb/conarydb"): # pragma: no cover
+ distro = Conary() # pragma: no cover
+ elif os.path.exists("/etc/release") and \
+ re.match(".*Solaris", open("/etc/release").readline()): # pragma: no cover
+ distro = Solaris() # pragma: no cover
+ else:
+ raise DistributionNotSupportedError("Unknown") # pragma: no cover
+ logger.log(distro.__class__.__name__)
+ return distro
+
+distro = detectDistro()
+packageDb = distro.packageDb
diff --git a/build/lib/dogtail/dump.py b/build/lib/dogtail/dump.py
new file mode 100644
index 0000000000..3756820a51
--- /dev/null
+++ b/build/lib/dogtail/dump.py
@@ -0,0 +1,33 @@
+"""Utility functions for 'dumping' trees of Node objects.
+
+Author: Zack Cerza <zcerza@redhat.com>"""
+__author__ = "Zack Cerza <zcerza@redhat.com>"
+
+from __builtin__ import file
+
+spacer = ' '
+
+
+def plain(node, fileName=None):
+ """
+ Plain-text dump. The hierarchy is represented through indentation.
+ """
+ def crawl(node, depth):
+ dump(node, depth)
+ for action in node.actions.values():
+ dump(action, depth + 1)
+ for child in node.children:
+ crawl(child, depth + 1)
+
+ def dumpFile(item, depth):
+ _file.write(spacer * depth + str(item) + '\n')
+
+ def dumpStdOut(item, depth):
+ print(spacer * depth + str(item))
+ if fileName:
+ dump = dumpFile
+ _file = file(fileName, 'w')
+ else:
+ dump = dumpStdOut
+
+ crawl(node, 0)
diff --git a/build/lib/dogtail/errors.py b/build/lib/dogtail/errors.py
new file mode 100644
index 0000000000..648af9d598
--- /dev/null
+++ b/build/lib/dogtail/errors.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+"""
+General exceptions; not overly module-specific
+"""
+__author__ = "Zack Cerza <zcerza@redhat.com>"
+from logging import debugLogger as logger
+
+import inspect
+
+
+def warn(message, caller=True):
+ """
+ Generate a warning, and pass it to the debug logger.
+ """
+ frameRec = inspect.stack()[-1]
+ message = "Warning: %s:%s: %s" % (frameRec[1], frameRec[2], message)
+ if caller and frameRec[1] != '<stdin>' and frameRec[1] != '<string>':
+ message = message + ':\n ' + frameRec[4][0]
+ del frameRec
+ logger.log(message)
+
+
+class DependencyNotFoundError(Exception):
+
+ """
+ A dependency was not found.
+ """
+ pass
diff --git a/build/lib/dogtail/i18n.py b/build/lib/dogtail/i18n.py
new file mode 100644
index 0000000000..8117f8d82c
--- /dev/null
+++ b/build/lib/dogtail/i18n.py
@@ -0,0 +1,281 @@
+# -*- coding: utf-8 -*-
+"""
+Internationalization facilities
+
+Authors: David Malcolm <dmalcolm@redhat.com>
+"""
+
+__author__ = """David Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>"""
+
+import config
+
+import os
+import re
+import gettext
+
+from logging import debugLogger as logger
+from __builtin__ import unicode
+
+
+def safeDecode(string):
+ try:
+ string = string.decode('utf-8', 'replace')
+ except UnicodeEncodeError:
+ string = string.encode('utf-8', 'replace')
+ return string
+
+
+def safeEncode(string):
+ pass
+
+
+"""
+Singleton list of TranslationDb instances, to be initialized by the script with
+whatever translation databases it wants.
+"""
+translationDbs = []
+
+
+class TranslationDb(object):
+
+ """
+ Abstract base class representing a database of translations
+ """
+
+ def getTranslationsOf(self, srcName):
+ """
+ Pure virtual method to look up the translation of a string.
+ Returns a list of candidate strings (the translation), empty if not found.
+
+ Note that a source string can map to multiple translated strings. For
+ example, in the French translation of Evolution, the string "Forward" can
+ translate to both
+ (i) "Faire suivre" for forwarding an email, and
+ (ii) "Suivant" for the next page in a wizard.
+ """
+ raise NotImplementedError
+
+
+class GettextTranslationDb(TranslationDb):
+
+ """
+ Implementation of TranslationDb which leverages gettext, using a single
+ translation mo-file.
+ """
+
+ def __init__(self, moFile):
+ self.__moFile = moFile
+ self.__gnutranslations = gettext.GNUTranslations(open(moFile))
+
+ def getTranslationsOf(self, srcName):
+ srcName = safeDecode(srcName)
+ # print "searching for translations of %s"%srcName
+ # Use a dict to get uniqueness:
+ results = {}
+ result = self.__gnutranslations.ugettext(srcName)
+ if result != srcName:
+ results[result] = None
+
+ # Hack alert:
+ #
+ # Note that typical UI definition in GTK etc contains strings with
+ # underscores to denote accelerators.
+ # For example, the stock GTK "Add" item has text "_Add" which e.g.
+ # translates to "A_jouter" in French
+ #
+ # Since these underscores have been stripped out before we see these strings,
+ # we are looking for a translation of "Add" into "Ajouter" in this case, so
+ # we need to fake it, by looking up the string multiple times, with underscores
+ # inserted in all possible positions, stripping underscores out of the result.
+ # Ugly, but it works.
+
+ for index in range(len(srcName)):
+ candidate = srcName[:index] + "_" + srcName[index:]
+ result = self.__gnutranslations.ugettext(candidate)
+ if result != candidate:
+ # Strip out the underscore, and add to the result:
+ results[result.replace('_', '')] = True
+
+ return results.keys()
+
+
+def translate(srcString):
+ """
+ Look up srcString in the various translation databases (if any), returning
+ a list of all matches found (potentially the empty list)
+ """
+ # Use a dict to get uniqueness:
+ results = {}
+ # Try to translate the string:
+ for translationDb in translationDbs:
+ for result in translationDb.getTranslationsOf(srcString):
+ result = safeDecode(result)
+ results[result] = True
+
+ # No translations found:
+ if len(results) == 0:
+ if config.config.debugTranslation:
+ logger.log('Translation not found for "%s"' % srcString)
+ return results.keys()
+
+
+class TranslatableString(object):
+
+ """
+ Class representing a string that we want to match strings against, handling
+ translation for us, by looking it up once at construction time.
+ """
+
+ def __init__(self, untranslatedString):
+ """
+ Constructor looks up the string in all of the translation databases, storing
+ the various translations it finds.
+ """
+ untranslatedString = safeDecode(untranslatedString)
+ self.untranslatedString = untranslatedString
+ self.translatedStrings = translate(untranslatedString)
+
+ def matchedBy(self, string):
+ """
+ Compare the test string against either the translation of the original
+ string (or simply the original string, if no translation was found).
+ """
+ # print "comparing %s against %s"%(string, self)
+ def stringsMatch(inS, outS):
+ """
+ Compares a regular expression to a string
+
+ inS: the regular expression (or normal string)
+ outS: the normal string to be compared against
+ """
+ inString = str(inS)
+ outString = outS
+ if inString == outString:
+ return True
+ inString = inString + '$'
+ inString = safeDecode(inString)
+ outString = safeDecode(outString)
+ if inString[0] == '*':
+ inString = "\\" + inString
+ # Escape all parentheses, since grouping will never be needed here
+ inString = re.sub('([\(\)])', r'\\\1', inString)
+ match = re.match(inString, outString)
+ matched = match is not None
+ return matched
+
+ matched = False
+ # the 'ts' variable keeps track of whether we're working with
+ # translated strings. it's only used for debugging purposes.
+ #ts = 0
+ # print string, str(self)
+ for translatedString in self.translatedStrings:
+ #ts = ts + 1
+ matched = stringsMatch(translatedString, string)
+ if not matched:
+ matched = translatedString == string
+ if matched:
+ return matched
+ # ts=0
+ return stringsMatch(self.untranslatedString, string)
+
+ def __str__(self):
+ """
+ Provide a meaningful debug version of the string (and the translation in
+ use)
+ """
+ if len(self.translatedStrings) > 0:
+ # build an output string, with commas in the correct places
+ translations = ""
+ for tString in self.translatedStrings:
+ translations += u'"%s", ' % safeDecode(tString)
+ result = u'"%s" (%s)' % (
+ safeDecode(self.untranslatedString), translations)
+ return safeDecode(result)
+ else:
+ return '"%s"' % (self.untranslatedString)
+
+
+def isMoFile(filename, language=''):
+ """
+ Does the given filename look like a gettext mo file?
+
+ Optionally: Does the file also contain translations for a certain language,
+ for example 'ja'?
+ """
+ if re.match('(.*)\\.mo$', filename):
+ if not language:
+ return True
+ elif re.match('/usr/share/locale(.*)/%s(.*)/LC_MESSAGES/(.*)\\.mo$' %
+ language, filename):
+ return True
+ else:
+ return False
+ else:
+ return False
+
+
+def loadAllTranslationsForLanguage(language):
+ import distro
+ for moFile in distro.packageDb.getMoFiles(language):
+ translationDbs.append(GettextTranslationDb(moFile))
+
+
+def getMoFilesForPackage(packageName, language='', getDependencies=True):
+ """
+ Look up the named package and find all gettext mo files within it and its
+ dependencies. It is possible to restrict the results to those of a certain
+ language, for example 'ja'.
+ """
+ import distro
+
+ result = []
+ for filename in distro.packageDb.getFiles(packageName):
+ if isMoFile(filename, language):
+ result.append(filename)
+
+ if getDependencies:
+ # Recurse:
+ for dep in distro.packageDb.getDependencies(packageName):
+ # We pass False to the inner call because getDependencies has already
+ # walked the full tree
+ result.extend(getMoFilesForPackage(dep, language, False))
+
+ return result
+
+
+def loadTranslationsFromPackageMoFiles(packageName, getDependencies=True):
+ """
+ Helper function which appends all of the gettext translation mo-files used by
+ the package (and its dependencies) to the translation database list.
+ """
+ # Keep a list of mo-files that are already in use to avoid duplicates.
+ moFiles = {}
+
+ def load(packageName, language='', getDependencies=True):
+ for moFile in getMoFilesForPackage(packageName, language, getDependencies):
+ # Searching the popt mo-files for translations makes gettext bail out,
+ # so we ignore them here. This is
+ # https://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=172155 .
+ if not('popt.mo' in moFile or moFile in moFiles):
+ try:
+ translationDbs.append(GettextTranslationDb(moFile))
+ moFiles[moFile] = None
+ except (AttributeError, IndexError):
+ if config.config.debugTranslation:
+ #import traceback
+ # logger.log(traceback.format_exc())
+ logger.log(
+ "Warning: Failed to load mo-file for translation: " + moFile)
+
+ # Hack alert:
+ #
+ # The following special-case is necessary for Ubuntu, since their
+ # translations are shipped in a single huge package. The downside to
+ # this special case, aside from the simple fact that there is one,
+ # is that it makes automatic translations much slower.
+
+ import distro
+ language = os.environ.get('LANGUAGE', os.environ['LANG'])[0:2]
+ if isinstance(distro.distro, distro.Ubuntu):
+ load('language-pack-gnome-%s' % language, language)
+ load(packageName, language, getDependencies)
diff --git a/build/lib/dogtail/logging.py b/build/lib/dogtail/logging.py
new file mode 100644
index 0000000000..7e73f163b8
--- /dev/null
+++ b/build/lib/dogtail/logging.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+"""
+Logging facilities
+
+Authors: Ed Rousseau <rousseau@redhat.com>, Zack Cerza <zcerza@redhat.com, David Malcolm <dmalcolm@redhat.com>
+"""
+
+__author__ = """Ed Rousseau <rousseau@redhat.com>,
+Zack Cerza <zcerza@redhat.com,
+David Malcolm <dmalcolm@redhat.com>
+"""
+import os
+import sys
+import time
+from config import config
+import codecs
+
+# Timestamp class for file logs
+
+
+class TimeStamp(object):
+
+ """
+ Generates timestamps tempfiles and log entries
+ """
+
+ def __init__(self):
+ self.now = "0"
+ self.timetup = time.localtime()
+
+ def zeroPad(self, int, width=2):
+ """
+ Pads an integer 'int' with zeroes, up to width 'width'.
+
+ Returns a string.
+
+ It will not truncate. If you call zeroPad(100, 2), '100' will be returned.
+ """
+ if int < 10 ** width:
+ return ("0" * (width - len(str(int)))) + str(int)
+ else:
+ return str(int)
+
+ # file stamper
+ def fileStamp(self, filename, addTime=True):
+ """
+ Generates a filename stamp in the format of filename_YYYYMMDD-hhmmss.
+ A format of filename_YYYYMMDD can be used instead by specifying addTime = False.
+ """
+ self.now = filename.strip() + "_"
+ self.timetup = time.localtime()
+
+ # Should produce rel-eng style filestamps
+ # format it all pretty by chopping the tuple
+ fieldCount = 3
+ if addTime:
+ fieldCount = fieldCount + 3
+ for i in range(fieldCount):
+ if i == 3:
+ self.now = self.now + '-'
+ self.now = self.now + self.zeroPad(self.timetup[i])
+ return self.now
+
+ # Log entry stamper
+ def entryStamp(self):
+ """
+ Generates a logfile entry stamp of YYYY.MM.DD HH:MM:SS
+ """
+ self.timetup = time.localtime()
+
+ # This will return a log entry formatted string in YYYY.MM.DD HH:MM:SS
+ for i in range(6):
+ # put in the year
+ if i == 0:
+ self.now = str(self.timetup[i])
+ # Format Month and Day
+ elif i == 1 or i == 2:
+ self.now = self.now + "." + self.zeroPad(self.timetup[i])
+ else:
+ # make the " " between Day and Hour and put in the hour
+ if i == 3:
+ self.now = self.now + " " + self.zeroPad(self.timetup[i])
+ # Otherwise Use the ":" divider
+ else:
+ self.now = self.now + ":" + self.zeroPad(self.timetup[i])
+ return self.now
+
+
+class Logger(object):
+
+ """
+ Writes entries to standard out.
+ """
+ stamper = TimeStamp()
+
+ def __init__(self, logName, file=False, stdOut=True):
+ """
+ name: the name of the log
+ file: The file object to log to.
+ stdOut: Whether to log to standard out.
+ """
+ self.logName = logName
+ self.stdOut = stdOut
+ self.file = file # Handle to the logfile
+ if not self.file:
+ return
+
+ scriptName = config.scriptName
+ if not scriptName:
+ scriptName = 'log'
+ self.fileName = scriptName
+
+ # check to see if we can write to the logDir
+ if os.path.isdir(config.logDir):
+ self.findUniqueName()
+ else:
+ # If path doesn't exist, raise an exception
+ raise IOError(
+ "Log path %s does not exist or is not a directory" % config.logDir)
+
+ def findUniqueName(self):
+ # generate a logfile name and check if it already exists
+ self.fileName = config.logDir + self.stamper.fileStamp(self.fileName) \
+ + '_' + self.logName
+ i = 0
+ while os.path.exists(self.fileName):
+ # Append the pathname
+ if i == 0:
+ self.fileName = self.fileName + "." + str(i)
+ else:
+ logsplit = self.fileName.split(".")
+ logsplit[-1] = str(i)
+ self.fileName = ".".join(logsplit)
+ i += 1
+
+ def createFile(self):
+ # Try to create the file and write the header info
+ print("Creating logfile at %s ..." % self.fileName)
+ self.file = codecs.open(self.fileName, mode='wb', encoding=
+ 'utf-8')
+ self.file.write("##### " + os.path.basename(self.fileName) + '\n')
+ self.file.flush()
+
+ def log(self, message, newline=True, force=False):
+ """
+ Hook used for logging messages. Might eventually be a virtual
+ function, but nice and simple for now.
+
+ If force is True, log to a file irrespective of config.logDebugToFile.
+ """
+ try:
+ message = message.decode('utf-8', 'replace')
+ except UnicodeEncodeError:
+ pass
+
+
+ # Try to open and write the result to the log file.
+ if isinstance(self.file, bool) and (force or config.logDebugToFile):
+ self.createFile()
+
+ if force or config.logDebugToFile:
+ if newline:
+ self.file.write(message + '\n')
+ else:
+ self.file.write(message + ' ')
+ self.file.flush()
+
+ if self.stdOut and config.logDebugToStdOut:
+ if newline:
+ print(message)
+ else:
+ print(message)
+
+
+class ResultsLogger(Logger):
+
+ """
+ Writes entries into the Dogtail log
+ """
+
+ def __init__(self, stdOut=True):
+ Logger.__init__(self, 'results', file=True, stdOut=stdOut)
+
+ # Writes the result of a test case comparison to the log
+ def log(self, entry):
+ """
+ Writes the log entry. Requires a 1 {key: value} pair dict for an argument or else it will throw an exception.
+ """
+ # We require a 1 key: value dict
+ # Strip all leading and trailing witespace from entry dict and convert
+ # to string for writing
+
+ if len(entry) == 1:
+ key = entry.keys()
+ value = entry.values()
+ key = key[0]
+ value = value[0]
+ entry = str(key) + ": " + str(value)
+ else:
+ raise ValueError(entry)
+ print(
+ "Method argument requires a 1 {key: value} dict. Supplied argument not one {key: value}")
+
+ Logger.log(self, self.stamper.entryStamp() + " " + entry,
+ force=True)
+
+debugLogger = Logger('debug', config.logDebugToFile)
+
+import traceback
+
+
+def exceptionHook(exc, value, tb): # pragma: no cover
+ tbStringList = traceback.format_exception(exc, value, tb)
+ tbString = ''.join(tbStringList)
+ debugLogger.log(tbString)
+ sys.exc_clear()
+
+sys.excepthook = exceptionHook
diff --git a/build/lib/dogtail/path.py b/build/lib/dogtail/path.py
new file mode 100644
index 0000000000..5742cfb03e
--- /dev/null
+++ b/build/lib/dogtail/path.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+"""
+Author: David Malcolm <dmalcolm@redhat.com>
+"""
+__author__ = """David Malcolm <dmalcolm@redhat.com>"""
+
+
+class SearchPath(object):
+
+ """
+ Class used by the recording framework (and for more verbose script
+ logging) for identifying nodes in a persistent way, independent of the
+ style of script being written.
+
+ Implemented as a list of (predicate, isRecursive) pairs, giving the
+ 'best' way to find the Accessible wrapped by a Node, starting at the
+ root and applying each search in turn.
+
+ This is somewhat analagous to an absolute path in a filesystem, except
+ that some of searches may be recursive, rather than just searching
+ direct children.
+
+ FIXME: try to ensure uniqueness
+ FIXME: need some heuristics to get 'good' searches, whatever
+ that means
+ """
+
+ def __init__(self):
+ self.__list = []
+
+ def __str__(self):
+ result = "{"
+ for (predicate, isRecursive) in self.__list:
+ result += "/(%s,%s)" % (
+ predicate.describeSearchResult(), isRecursive)
+ return result + "}"
+
+ # We need equality to work so that dicts of these work:
+ def __eq__(self, other):
+ # print "eq: self:%s"%self
+ # print " other:%s"%other
+ if len(self.__list) != len(other.__list):
+ # print "nonequal length"
+ return False
+ else:
+ for i in range(len(self.__list)):
+ if self.__list[i] != other.__list[i]:
+ return False
+ # print True
+ return True
+
+ def append(self, predicate, isRecursive):
+ assert predicate
+ self.__list.append((predicate, isRecursive))
+
+ def __iter__(self):
+ return iter(self.__list)
+
+ def length(self):
+ return len(self.__list)
+
+ def makeScriptMethodCall(self):
+ """
+ Used by the recording system.
+
+ Generate the Python source code that will carry out this search.
+ """
+ result = ""
+ for (predicate, isRecursive) in self.__list:
+ # print predicate
+ # print self.generateVariableName(predicate)
+ result += "." + predicate.makeScriptMethodCall(isRecursive)
+ return result
+
+ def getRelativePath(self, other):
+ """
+ Given another SearchPath instance, if the other is 'below' this
+ one, return a SearchPath that describes how to reach it relative
+ to this one (a copy of the second part of the list). Otherwise
+ return None.
+ """
+ for i in range(len(self.__list)):
+ if self.__list[i] != other.__list[i]:
+ break
+ if i > 0:
+ # Slice from this point to the end:
+ result = SearchPath()
+ result.__list = other.__list[i + 1:]
+
+ if False:
+ print("....................")
+ print("from %s" % self)
+ print("to %s" % other)
+ print("i=%s" % i)
+ print("relative path %s" % result)
+ print("....................")
+
+ return result
+ else:
+ return None
+
+ def getPrefix(self, n):
+ """
+ Get the first n components of this instance as a new instance
+ """
+ result = SearchPath()
+ for i in range(n):
+ result.__list.append(self.__list[i])
+ return result
+
+ def getPredicate(self, i):
+ (predicate, isRecursive) = self.__list[i]
+ return predicate
diff --git a/build/lib/dogtail/predicate.py b/build/lib/dogtail/predicate.py
new file mode 100644
index 0000000000..aa100e1f05
--- /dev/null
+++ b/build/lib/dogtail/predicate.py
@@ -0,0 +1,443 @@
+"""Predicates that can be used when searching for nodes.
+
+Author: David Malcolm <dmalcolm@redhat.com>"""
+__author__ = 'David Malcolm <dmalcolm@redhat.com>'
+
+from i18n import TranslatableString
+from gi.repository import GLib
+from time import sleep
+from logging import debugLogger as logger
+from config import config
+
+def stringMatches(scriptName, reportedName):
+ assert isinstance(scriptName, TranslatableString)
+
+ return scriptName.matchedBy(reportedName)
+
+
+def makeScriptRecursiveArgument(isRecursive, defaultValue):
+ if isRecursive == defaultValue:
+ return ""
+ else:
+ return ", recursive=%s" % isRecursive
+
+
+def makeCamel(string):
+ """
+ Convert string to camelCaps
+ """
+ string = str(string)
+ # FIXME: this function is probably really fragile, lots of difficult cases
+ # here
+
+ # Sanitize string, replacing bad characters with spaces:
+ for char in ":;!@#$%^&*()-+=_~`\\/?|[]{}<>,.\t\n\r\"'":
+ string = string.replace(char, " ")
+ words = string.strip().split(" ")
+ for word in words:
+ word.strip
+ result = ""
+ firstWord = True
+ for word in words:
+ lowercaseWord = word.lower()
+ if firstWord:
+ result += lowercaseWord
+ firstWord = False
+ else:
+ result += lowercaseWord.capitalize()
+ return result
+
+
+class Predicate(object):
+
+ """Abstract base class representing a predicate function on nodes.
+
+ It's more than just a function in that it has data and can describe itself"""
+
+ def satisfiedByNode(self, node):
+ """Pure virtual method returning a boolean if the predicate is satisfied by the node"""
+ raise NotImplementedError
+
+ def describeSearchResult(self, node):
+ raise NotImplementedError
+
+ def makeScriptMethodCall(self, isRecursive):
+ """
+ Method to generate a string containing a (hopefully) readable search
+ method call on a node (to be used when generating Python source code in
+ the event recorder)
+ """
+ raise NotImplementedError
+
+ def makeScriptVariableName(self):
+ """
+ Method to generate a string containing a (hopefully) readable name
+ for a Node instance variable that would be the result of a search on
+ this predicate (to be used when generating Python source code in the
+ event recorder).
+ """
+ raise NotImplementedError
+
+ def __eq__(self, other):
+ """
+ Predicates are considered equal if they are of the same subclass and
+ have the same data
+ """
+ # print "predeq: self:%s"%self
+ # print " other:%s"%other
+ # print "predeq: selfdict:%s"%self.__dict__
+ # print " otherdict:%s"%other.__dict__
+
+ if type(self) != type(other):
+ return False
+ else:
+ return self.__dict__ == other.__dict__
+
+
+class IsAnApplicationNamed(Predicate):
+
+ """Search subclass that looks for an application by name"""
+
+ def __init__(self, appName):
+ self.appName = TranslatableString(appName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ try:
+ return node.roleName == 'application' and stringMatches(self.appName, node.name)
+ except GLib.GError as e:
+ if 'name :1.0 was not provided' in e.message:
+ logger.log("Dogtail: warning: omiting possibly broken at-spi application record")
+ return False
+ else:
+ try:
+ sleep(config.defaults['searchWarningThreshold'])
+ return node.roleName == 'application' and stringMatches(self.appName, node.name)
+ except GLib.GError:
+ logger.log("Dogtail: warning: application may be hanging")
+ return False
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return '%s application' % self.appName
+
+ def makeScriptMethodCall(self, isRecursive):
+ # ignores the isRecursive parameter
+ return "application(%s)" % self.appName
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.appName) + "App"
+
+
+class GenericPredicate(Predicate):
+
+ """SubtreePredicate subclass that takes various optional search fields"""
+
+ def __init__(self, name=None, roleName=None, description=None, label=None, debugName=None):
+ if name:
+ self.name = TranslatableString(name)
+ else:
+ self.name = None
+ self.roleName = roleName
+ self.description = description
+ if label:
+ self.label = TranslatableString(label)
+ else:
+ self.label = None
+
+ if debugName:
+ self.debugName = debugName
+ else:
+ if label:
+ self.debugName = "labelled '%s'" % self.label
+ else:
+ self.debugName = "child with"
+ if name:
+ self.debugName += " name=%s" % self.name
+ if roleName:
+ self.debugName += " roleName='%s'" % roleName
+ if description:
+ self.debugName += " description='%s'" % description
+ assert self.debugName
+
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ # labelled nodes are handled specially:
+ if self.label:
+ # this reverses the search; we're looking for a node with LABELLED_BY
+ # and then checking the label, rather than looking for a label and
+ # then returning whatever LABEL_FOR targets
+ if node.labeller:
+ return stringMatches(self.label, node.labeller.name)
+ else:
+ return False
+ else:
+ # Ensure the node matches any criteria that were set:
+ try:
+ if self.name:
+ if not stringMatches(self.name, node.name):
+ return False
+ if self.roleName:
+ if self.roleName != node.roleName:
+ return False
+ if self.description:
+ if self.description != node.description:
+ return False
+ except GLib.GError as e:
+ if 'name :1.0 was not provided' in e.message:
+ logger.log("Dogtail: warning: omiting possibly broken at-spi application record")
+ return False
+ else:
+ raise e
+ return True
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return self.debugName
+
+ def makeScriptMethodCall(self, isRecursive):
+ if self.label:
+ args = "label=%s" % self.label
+ else:
+ args = ""
+ if self.name:
+ print(self.name)
+ args += " name=%s" % self.name
+ if self.roleName:
+ args += " roleName='%s'" % self.roleName
+ if self.description:
+ args += " description='%s'" % self.description
+ return "child(%s%s)" % (args, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ if self.label:
+ return makeCamel(self.label) + "Node"
+ else:
+ if self.name:
+ return makeCamel(self.name) + "Node"
+ if self.roleName:
+ return makeCamel(self.roleName) + "Node"
+ if self.description:
+ return makeCamel(self.description) + "Node"
+
+
+class IsNamed(Predicate):
+
+ """Predicate subclass that looks simply by name"""
+
+ def __init__(self, name):
+ self.name = TranslatableString(name)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ return stringMatches(self.name, node.name)
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return "named %s" % self.name
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "child(name=%s%s)" % (self.name, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.name) + "Node"
+
+
+class IsAWindowNamed(Predicate):
+
+ """Predicate subclass that looks for a top-level window by name"""
+
+ def __init__(self, windowName):
+ self.windowName = TranslatableString(windowName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ return node.roleName == 'frame' and stringMatches(self.windowName, node.name)
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return "%s window" % self.windowName
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "window(%s%s)" % (self.windowName, makeScriptRecursiveArgument(isRecursive, False))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.windowName) + "Win"
+
+
+class IsAWindow(Predicate):
+
+ """Predicate subclass that looks for top-level windows"""
+
+ def __init__(self):
+ self.satisfiedByNode = lambda node: node.roleName == 'frame'
+
+ def describeSearchResult(self):
+ return "window"
+
+
+class IsADialogNamed(Predicate):
+
+ """Predicate subclass that looks for a top-level dialog by name"""
+
+ def __init__(self, dialogName):
+ self.dialogName = TranslatableString(dialogName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ return node.roleName == 'dialog' and stringMatches(self.dialogName, node.name)
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return '%s dialog' % self.dialogName
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "dialog(%s%s)" % (self.dialogName, makeScriptRecursiveArgument(isRecursive, False))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.dialogName) + "Dlg"
+
+
+class IsLabelledBy(Predicate):
+
+ """Predicate: is this node labelled by another node"""
+ pass
+
+
+class IsLabelledAs(Predicate):
+
+ """Predicate: is this node labelled with the text string (i.e. by another node with that as a name)"""
+
+ def __init__(self, labelText):
+ self.labelText = TranslatableString(labelText)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ # FIXME
+ if node.labeller:
+ return stringMatches(self.labelText, node.labeller.name)
+ else:
+ return False
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return 'labelled %s' % self.labelText
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "child(label=%s%s)" % (self.labelText, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.labelText) + "Node"
+
+
+class IsAMenuNamed(Predicate):
+
+ """Predicate subclass that looks for a menu by name"""
+
+ def __init__(self, menuName):
+ self.menuName = TranslatableString(menuName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'menu' and \
+ stringMatches(self.menuName, node.name)
+
+ def describeSearchResult(self):
+ return '%s menu' % (self.menuName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "menu(%s%s)" % (self.menuName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.menuName) + "Menu"
+
+
+class IsAMenuItemNamed(Predicate):
+
+ """Predicate subclass that looks for a menu item by name"""
+
+ def __init__(self, menuItemName):
+ self.menuItemName = TranslatableString(menuItemName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: \
+ node.roleName.endswith('menu item') and \
+ stringMatches(self.menuItemName, node.name)
+
+ def describeSearchResult(self):
+ return '%s menuitem' % (self.menuItemName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "menuItem(%s%s)" % (self.menuItemName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.menuItemName) + "MenuItem"
+
+
+class IsATextEntryNamed(Predicate):
+
+ """Predicate subclass that looks for a text entry by name"""
+
+ def __init__(self, textEntryName):
+ self.textEntryName = TranslatableString(textEntryName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'text' and \
+ stringMatches(self.textEntryName, node.name)
+
+ def describeSearchResult(self):
+ return '%s textentry' % (self.textEntryName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "textentry(%s%s)" % (self.textEntryName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.textEntryName) + "Entry"
+
+
+class IsAButtonNamed(Predicate):
+
+ """Predicate subclass that looks for a button by name"""
+
+ def __init__(self, buttonName):
+ self.buttonName = TranslatableString(buttonName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'push button' \
+ and stringMatches(self.buttonName, node.name)
+
+ def describeSearchResult(self):
+ return '%s button' % (self.buttonName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "button(%s%s)" % (self.buttonName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.buttonName) + "Button"
+
+
+class IsATabNamed(Predicate):
+
+ """Predicate subclass that looks for a tab by name"""
+
+ def __init__(self, tabName):
+ self.tabName = TranslatableString(tabName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'page tab' and \
+ stringMatches(self.tabName, node.name)
+
+ def describeSearchResult(self):
+ return '%s tab' % (self.tabName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "tab(%s%s)" % (self.tabName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.tabName) + "Tab"
diff --git a/build/lib/dogtail/procedural.py b/build/lib/dogtail/procedural.py
new file mode 100644
index 0000000000..c87ae86181
--- /dev/null
+++ b/build/lib/dogtail/procedural.py
@@ -0,0 +1,455 @@
+"""
+Dogtail's procedural UI
+All the classes here are intended to be single-instance, except for Action.
+"""
+__author__ = 'Zack Cerza <zcerza@redhat.com>'
+#
+#
+# WARNING: Here There Be Dragons (TM) #
+#
+# If you don't understand how to use this API, you almost certainly don't #
+# want to read the code first. We make use of some very non-intuitive #
+# features of Python in order to make the API very simplistic. Therefore, #
+# you should probably only read this code if you're already familiar with #
+# some of Python's advanced features. You have been warned. ;) #
+#
+#
+
+import tree
+import predicate
+from config import config
+from utils import Lock
+import rawinput
+
+#FocusError = "FocusError: %s not found"
+
+
+class FocusError(Exception):
+ pass
+
+import errors
+
+
+def focusFailed(pred):
+ errors.warn('The requested widget could not be focused: %s' %
+ pred.debugName)
+
+ENOARGS = "At least one argument is needed"
+
+
+class FocusBase(object):
+
+ """
+ The base for every class in the module. Does nothing special, really.
+ """
+ node = None
+
+ def __getattr__(self, name):
+ # Fold all the Node's AT-SPI properties into the Focus object.
+ try:
+ return getattr(self.node, name)
+ except AttributeError:
+ raise AttributeError(name)
+
+ def __setattr__(self, name, value):
+ # Fold all the Node's AT-SPI properties into the Focus object.
+ if name == 'node':
+ setattr(self.__class__, name, value)
+ else:
+ try:
+ setattr(self.node, name, value)
+ except AttributeError:
+ raise AttributeError(name)
+
+
+class FocusApplication (FocusBase):
+
+ """
+ Keeps track of which application is currently focused.
+ """
+ desktop = tree.root
+
+ def __call__(self, name):
+ """
+ Search for an application that matches and refocus on the given name.
+ """
+ try:
+ pred = predicate.IsAnApplicationNamed(name)
+ app = self.desktop.findChild(
+ pred, recursive=False, retry=False)
+ except tree.SearchError:
+ if config.fatalErrors:
+ raise FocusError(name)
+ else:
+ focusFailed(pred)
+ return False
+ if app:
+ FocusApplication.node = app
+ FocusDialog.node = None
+ FocusWindow.node = None
+ FocusWidget.node = None
+ return True
+
+
+class FocusDesktop (FocusBase):
+
+ """
+ This isn't used yet, and may never be used.
+ """
+ pass
+
+
+class FocusWindow (FocusBase):
+
+ """
+ Keeps track of which window is currently focused.
+ """
+
+ def __call__(self, name):
+ """
+ Search for a dialog that matches the given name and refocus on it.
+ """
+ result = None
+ pred = predicate.IsAWindowNamed(name)
+ try:
+ result = FocusApplication.node.findChild(
+ pred, requireResult=False, recursive=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWindow.node = result
+ FocusDialog.node = None
+ FocusWidget.node = None
+ else:
+ if config.fatalErrors:
+ raise FocusError(pred.debugName)
+ else:
+ focusFailed(pred)
+ return False
+ return True
+
+
+class FocusDialog (FocusBase):
+
+ """
+ Keeps track of which dialog is currently focused.
+ """
+
+ def __call__(self, name):
+ """
+ Search for a dialog that matches the given name and refocus on it.
+ """
+ result = None
+ pred = predicate.IsADialogNamed(name)
+ try:
+ result = FocusApplication.node.findChild(
+ pred, requireResult=False, recursive=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusDialog.node = result
+ FocusWidget.node = None
+ else:
+ if config.fatalErrors:
+ raise FocusError(pred.debugName)
+ else:
+ focusFailed(pred)
+ return False
+ return True
+
+
+class FocusWidget (FocusBase):
+
+ """
+ Keeps track of which widget is currently focused.
+ """
+
+ def findByPredicate(self, pred):
+ result = None
+ try:
+ result = FocusWidget.node.findChild(
+ pred, requireResult=False, retry=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWidget.node = result
+ else:
+ try:
+ result = FocusDialog.node.findChild(
+ pred, requireResult=False, retry=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWidget.node = result
+ else:
+ try:
+ result = FocusWindow.node.findChild(
+ pred, requireResult=False, retry=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWidget.node = result
+ else:
+ try:
+ result = FocusApplication.node.findChild(
+ pred, requireResult=False, retry=False)
+ if result:
+ FocusWidget.node = result
+ except AttributeError:
+ if config.fatalErrors:
+ raise FocusError(pred)
+ else:
+ focusFailed(pred)
+ return False
+
+ if result is None:
+ FocusWidget.node = result
+ if config.fatalErrors:
+ raise FocusError(pred.debugName)
+ else:
+ focusFailed(pred)
+ return False
+ return True
+
+ def __call__(self, name='', roleName='', description=''):
+ """
+ If name, roleName or description are specified, search for a widget that matches and refocus on it.
+ """
+ if not name and not roleName and not description:
+ raise TypeError(ENOARGS)
+
+ # search for a widget.
+ pred = predicate.GenericPredicate(name=name,
+ roleName=roleName, description=description)
+ return self.findByPredicate(pred)
+
+
+class Focus (FocusBase):
+
+ """
+ The container class for the focused application, dialog and widget.
+ """
+
+ def __getattr__(self, name):
+ raise AttributeError(name)
+
+ def __setattr__(self, name, value):
+ if name in ('application', 'dialog', 'widget', 'window'):
+ self.__dict__[name] = value
+ else:
+ raise AttributeError(name)
+
+ desktop = tree.root
+ application = FocusApplication()
+ app = application # shortcut :)
+ dialog = FocusDialog()
+ window = FocusWindow()
+ frame = window
+ widget = FocusWidget()
+
+ def button(self, name):
+ """
+ A shortcut to self.widget.findByPredicate(predicate.IsAButtonNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsAButtonNamed(name))
+
+ def icon(self, name):
+ """
+ A shortcut to self.widget(name, roleName = 'icon')
+ """
+ return self.widget(name=name, roleName='icon')
+
+ def menu(self, name):
+ """
+ A shortcut to self.widget.findByPredicate(predicate.IsAMenuNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsAMenuNamed(name))
+
+ def menuItem(self, name):
+ """
+ A shortcut to self.widget.findByPredicate(predicate.IsAMenuItemNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsAMenuItemNamed(name))
+
+ def table(self, name=''):
+ """
+ A shortcut to self.widget(name, roleName 'table')
+ """
+ return self.widget(name=name, roleName='table')
+
+ def tableCell(self, name=''):
+ """
+ A shortcut to self.widget(name, roleName 'table cell')
+ """
+ return self.widget(name=name, roleName='table cell')
+
+ def text(self, name=''):
+ """
+ A shortcut to self.widget.findByPredicate(IsATextEntryNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsATextEntryNamed(name))
+
+
+class Action (FocusWidget):
+
+ """
+ Aids in executing AT-SPI actions, refocusing the widget if necessary.
+ """
+
+ def __init__(self, action):
+ """
+ action is a string with the same name as the AT-SPI action you wish to execute using this class.
+ """
+ self.action = action
+
+ def __call__(self, name='', roleName='', description='', delay=config.actionDelay):
+ """
+ If name, roleName or description are specified, first search for a widget that matches and refocus on it.
+ Then execute the action.
+ """
+ if name or roleName or description:
+ FocusWidget.__call__(
+ self, name=name, roleName=roleName, description=description)
+ self.node.doActionNamed(self.action)
+
+ def __getattr__(self, attr):
+ return getattr(FocusWidget.node, attr)
+
+ def __setattr__(self, attr, value):
+ if attr == 'action':
+ self.__dict__[attr] = value
+ else:
+ setattr(FocusWidget, attr, value)
+
+ def button(self, name):
+ """
+ A shortcut to self(name, roleName = 'push button')
+ """
+ self.__call__(name=name, roleName='push button')
+
+ def menu(self, name):
+ """
+ A shortcut to self(name, roleName = 'menu')
+ """
+ self.__call__(name=name, roleName='menu')
+
+ def menuItem(self, name):
+ """
+ A shortcut to self(name, roleName = 'menu item')
+ """
+ self.__call__(name=name, roleName='menu item')
+
+ def table(self, name=''):
+ """
+ A shortcut to self(name, roleName 'table')
+ """
+ self.__call__(name=name, roleName='table')
+
+ def tableCell(self, name=''):
+ """
+ A shortcut to self(name, roleName 'table cell')
+ """
+ self.__call__(name=name, roleName='table cell')
+
+ def text(self, name=''):
+ """
+ A shortcut to self(name, roleName = 'text')
+ """
+ self.__call__(name=name, roleName='text')
+
+
+class Click (Action):
+
+ """
+ A special case of Action, Click will eventually handle raw mouse events.
+ """
+ primary = 1
+ middle = 2
+ secondary = 3
+
+ def __init__(self):
+ Action.__init__(self, 'click')
+
+ def __call__(self, name='', roleName='', description='', raw=True, button=primary, delay=config.actionDelay):
+ """
+ By default, execute a raw mouse event.
+ If raw is False or if button evaluates to False, just pass the rest of
+ the arguments to Action.
+ """
+ if name or roleName or description:
+ FocusWidget.__call__(
+ self, name=name, roleName=roleName, description=description)
+ if raw and button:
+ # We're doing a raw mouse click
+ Click.node.click(button)
+ else:
+ Action.__call__(
+ self, name=name, roleName=roleName, description=description, delay=delay)
+
+
+class Select (Action):
+
+ """
+ Aids in selecting and deselecting widgets, i.e. page tabs
+ """
+ select = 'select'
+ deselect = 'deselect'
+
+ def __init__(self, action):
+ """
+ action must be 'select' or 'deselect'.
+ """
+ if action not in (self.select, self.deselect):
+ raise ValueError(action)
+ Action.__init__(self, action)
+
+ def __call__(self, name='', roleName='', description='', delay=config.actionDelay):
+ """
+ If name, roleName or description are specified, first search for a widget that matches and refocus on it.
+ Then execute the action.
+ """
+ if name or roleName or description:
+ FocusWidget.__call__(
+ self, name=name, roleName=roleName, description=description)
+ func = getattr(self.node, self.action)
+ func()
+
+
+def type(text):
+ if focus.widget.node:
+ focus.widget.node.typeText(text)
+ else:
+ rawinput.typeText(text)
+
+
+def keyCombo(combo):
+ if focus.widget.node:
+ focus.widget.node.keyCombo(combo)
+ else:
+ rawinput.keyCombo(combo)
+
+
+def run(application, arguments='', appName=''):
+ from utils import run as utilsRun
+ pid = utilsRun(application + ' ' + arguments, appName=appName)
+ focus.application(application)
+ return pid
+
+import os
+# tell sniff not to use auto-refresh while script using this module is running
+# may have already been locked by dogtail.tree
+if not os.path.exists('/tmp/sniff_refresh.lock'): # pragma: no cover
+ sniff_lock = Lock(lockname='sniff_refresh.lock', randomize=False)
+ try:
+ sniff_lock.lock()
+ except OSError:
+ pass # lock was already present from other script instance or leftover from killed instance
+ # lock should unlock automatically on script exit.
+
+focus = Focus()
+click = Click()
+activate = Action('activate')
+openItem = Action('open')
+menu = Action('menu')
+select = Select(Select.select)
+deselect = Select(Select.deselect)
diff --git a/build/lib/dogtail/rawinput.py b/build/lib/dogtail/rawinput.py
new file mode 100644
index 0000000000..7100b29bfa
--- /dev/null
+++ b/build/lib/dogtail/rawinput.py
@@ -0,0 +1,252 @@
+# -*- coding: utf-8 -*-
+"""
+Handles raw input using AT-SPI event generation.
+
+Note: Think of keyvals as keysyms, and keynames as keystrings.
+
+Authors: David Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>
+"""
+
+__author__ = """
+David Malcolm <dmalcolm@redhat.com>,
+Zack Cerza <zcerza@redhat.com>
+"""
+import gi
+
+gi.require_version('Gtk', '3.0')
+gi.require_version('Gdk', '3.0')
+
+from gi.repository import Gdk
+from config import config
+from utils import doDelay
+from logging import debugLogger as logger
+from pyatspi import Registry as registry
+from pyatspi import (KEY_SYM, KEY_PRESS, KEY_PRESSRELEASE, KEY_RELEASE)
+from exceptions import ValueError
+from __builtin__ import unicode, unichr
+
+
+def doTypingDelay():
+ doDelay(config.typingDelay)
+
+
+def checkCoordinates(x, y):
+ if x < 0 or y < 0:
+ raise ValueError(
+ "Attempting to generate a mouse event at negative coordinates: (%s,%s)" % (x, y))
+
+
+def click(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button click at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s click at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sc' % button)
+ doDelay(config.actionDelay)
+
+
+def doubleClick(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button double-click at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s doubleclick at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sd' % button)
+ doDelay()
+
+
+def press(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button press at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s press at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sp' % button)
+ doDelay()
+
+
+def release(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button release at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s release at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sr' % button)
+ doDelay()
+
+
+def absoluteMotion(x, y, mouseDelay=None, check=True):
+ """
+ Synthesize mouse absolute motion to (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse absolute motion to (%s,%s)" % (x, y))
+ registry.generateMouseEvent(x, y, 'abs')
+ if mouseDelay:
+ doDelay(mouseDelay)
+ else:
+ doDelay()
+
+
+def relativeMotion(x, y, mouseDelay=None):
+ logger.log("Mouse relative motion of (%s,%s)" % (x, y))
+ registry.generateMouseEvent(x, y, 'rel')
+ if mouseDelay:
+ doDelay(mouseDelay)
+ else:
+ doDelay()
+
+
+def drag(fromXY, toXY, button=1, check=True):
+ """
+ Synthesize a mouse press, drag, and release on the screen.
+ """
+ logger.log("Mouse button %s drag from %s to %s" % (button, fromXY, toXY))
+
+ (x, y) = fromXY
+ press(x, y, button, check)
+ # doDelay()
+
+ (x, y) = toXY
+ absoluteMotion(x, y, check=check)
+ doDelay()
+
+ release(x, y, button, check)
+ doDelay()
+
+
+def typeText(string):
+ """
+ Types the specified string, one character at a time.
+ Please note, you may have to set a higher typing delay,
+ if your machine misses/switches the characters typed.
+ Needed sometimes on slow setups/VMs typing non-ASCII utf8 chars.
+ """
+ if not isinstance(string, unicode):
+ string = string.decode('utf-8')
+ for char in string:
+ pressKey(char)
+
+keyNameAliases = {
+ 'enter': 'Return',
+ 'esc': 'Escape',
+ 'alt': 'Alt_L',
+ 'control': 'Control_L',
+ 'ctrl': 'Control_L',
+ 'shift': 'Shift_L',
+ 'del': 'Delete',
+ 'ins': 'Insert',
+ 'pageup': 'Page_Up',
+ 'pagedown': 'Page_Down',
+ ' ': 'space',
+ '\t': 'Tab',
+ '\n': 'Return'
+}
+
+
+# TODO: Dead code
+def keySymToUniChar(keySym): # pragma: no cover
+ i = Gdk.keyval_to_unicode(keySym)
+ if i:
+ UniChar = unichr(i)
+ else:
+ UniChar = ''
+ return UniChar
+
+
+def uniCharToKeySym(uniChar):
+ # OK, if it's not actually unicode we can fix that, right?
+ if not isinstance(uniChar, unicode):
+ uniChar = unicode(uniChar, 'utf-8')
+ i = ord(uniChar)
+ keySym = Gdk.unicode_to_keyval(i)
+ return keySym
+
+
+# dead code
+def keySymToKeyName(keySym): # pragma: no cover
+ return Gdk.keyval_name(keySym)
+
+
+def keyNameToKeySym(keyName):
+ keyName = keyNameAliases.get(keyName.lower(), keyName)
+ keySym = Gdk.keyval_from_name(keyName)
+ # various error 'codes' returned for non-recognized chars in versions of GTK3.X
+ if keySym == 0xffffff or keySym == 0x0 or keySym is None:
+ try:
+ keySym = uniCharToKeySym(keyName)
+ except: # not even valid utf-8 char
+ try: # Last attempt run at a keyName ('Meta_L', 'Dash' ...)
+ keySym = getattr(Gdk, 'KEY_' + keyName)
+ except AttributeError:
+ raise KeyError(keyName)
+ return keySym
+
+
+def keyNameToKeyCode(keyName):
+ """
+ Use GDK to get the keycode for a given keystring.
+
+ Note that the keycode returned by this function is often incorrect when
+ the requested keystring is obtained by holding down the Shift key.
+
+ Generally you should use uniCharToKeySym() and should only need this
+ function for nonprintable keys anyway.
+ """
+ keymap = Gdk.Keymap.get_for_display(Gdk.Display.get_default())
+ entries = keymap.get_entries_for_keyval(
+ Gdk.keyval_from_name(keyName))
+ try:
+ return entries[1][0].keycode
+ except TypeError:
+ pass
+
+
+def pressKey(keyName):
+ """
+ Presses (and releases) the key specified by keyName.
+ keyName is the English name of the key as seen on the keyboard. Ex: 'enter'
+ Names are looked up in Gdk.KEY_ If they are not found there, they are
+ looked up by uniCharToKeySym().
+ """
+ keySym = keyNameToKeySym(keyName)
+ registry.generateKeyboardEvent(keySym, None, KEY_SYM)
+ doTypingDelay()
+
+
+def keyCombo(comboString):
+ """
+ Generates the appropriate keyboard events to simulate a user pressing the
+ specified key combination.
+
+ comboString is the representation of the key combo to be generated.
+ e.g. '<Control><Alt>p' or '<Control><Shift>PageUp' or '<Control>q'
+ """
+ strings = []
+ for s in comboString.split('<'):
+ if s:
+ for S in s.split('>'):
+ if S:
+ S = keyNameAliases.get(S.lower(), S)
+ strings.append(S)
+ for s in strings:
+ if not hasattr(Gdk, s):
+ if not hasattr(Gdk, 'KEY_' + s):
+ raise ValueError("Cannot find key %s" % s)
+ modifiers = strings[:-1]
+ finalKey = strings[-1]
+ for modifier in modifiers:
+ code = keyNameToKeyCode(modifier)
+ registry.generateKeyboardEvent(code, None, KEY_PRESS)
+ code = keyNameToKeyCode(finalKey)
+ registry.generateKeyboardEvent(code, None, KEY_PRESSRELEASE)
+ for modifier in modifiers:
+ code = keyNameToKeyCode(modifier)
+ registry.generateKeyboardEvent(code, None, KEY_RELEASE)
+ doDelay()
diff --git a/build/lib/dogtail/sessions.py b/build/lib/dogtail/sessions.py
new file mode 100644
index 0000000000..8d4bcce6a3
--- /dev/null
+++ b/build/lib/dogtail/sessions.py
@@ -0,0 +1,231 @@
+import time
+import os
+import pwd
+import errno
+import re
+import subprocess
+import signal
+import tempfile
+import random
+import glob
+from dogtail.config import config
+
+
+def scratchFile(label): # pragma: no cover
+ """Uses tempfile.NamedTemporaryFile() to create a unique tempfile in
+ config.scratchDir, with a filename like:
+ dogtail-headless-<label>.<random junk>"""
+ prefix = "dogtail-headless-"
+ return tempfile.NamedTemporaryFile(prefix="%s%s." % (prefix, label),
+ dir=config.scratchDir)
+
+
+def testBinary(path): # pragma: no cover
+ if (path.startswith(os.path.sep) or
+ path.startswith(os.path.join('.', '')) or
+ path.startswith(os.path.join('..', ''))):
+ if not os.path.exists(path):
+ raise IOError(errno.ENOENT, "No such file", path)
+ if not os.access(path, os.X_OK):
+ raise IOError(errno.ENOEXEC, "Permission denied", path)
+ return True
+
+
+def get_username(): # pragma: no cover
+ return pwd.getpwuid(os.getuid())[0]
+
+
+class Subprocess(object): # pragma: no cover
+
+ def __init__(self, cmdList, environ=None):
+ testBinary(cmdList[0])
+ self.cmdList = cmdList
+ self.environ = environ
+ self._exitCode = None
+
+ def start(self):
+ if self.environ is None:
+ self.environ = os.environ
+ self.popen = subprocess.Popen(
+ self.cmdList, env=self.environ) # , stdout = subprocess.PIPE,
+ # stderr = subprocess.STDOUT, close_fds = True)
+ return self.popen.pid
+
+ def wait(self):
+ return self.popen.wait()
+
+ def stop(self):
+ # The following doesn't exist in python < 2.6, if you can believe it.
+ # self.popen.terminate()
+ os.kill(self.popen.pid, signal.SIGTERM)
+
+ @property
+ def exitCode(self):
+ if self._exitCode is None:
+ self._exitCode = self.wait()
+ return self._exitCode
+
+
+class XServer(Subprocess): # pragma: no cover
+
+ def __init__(self, server="/usr/bin/Xorg",
+ xinitrc="/etc/X11/xinit/Xclients",
+ resolution="1024x768x16"):
+ """resolution is only used with Xvfb."""
+ testBinary(server)
+ self.server = server
+ self._exitCode = None
+ self.xinit = "/usr/bin/xinit"
+ self.display = None
+ self.xinitrc = xinitrc
+ self.resolution = resolution
+
+ @staticmethod
+ def findFreeDisplay():
+ tmp = os.listdir('/tmp')
+ pattern = re.compile('\.X([0-9]+)-lock')
+ usedDisplays = []
+ for file in tmp:
+ match = re.match(pattern, file)
+ if match:
+ usedDisplays.append(int(match.groups()[0]))
+ if not usedDisplays:
+ return ':0'
+ usedDisplays.sort()
+ return ':' + str(usedDisplays[-1] + 1)
+
+ @property
+ def cmdList(self):
+ self.display = self.findFreeDisplay()
+ cmd = []
+ if self.xinit:
+ cmd.append(self.xinit)
+ if self.xinitrc:
+ cmd.append(self.xinitrc)
+ cmd.append('--')
+ cmd.append(self.server)
+ cmd.append(self.display)
+ cmd.extend(['-ac', '-noreset'])
+ if self.server.endswith('Xvfb'):
+ cmd.extend(['-screen', '0', self.resolution])
+ cmd.append('-shmem')
+ return cmd
+
+ def start(self):
+ print(' '.join(self.cmdList))
+ self.popen = subprocess.Popen(self.cmdList)
+ return self.popen.pid
+
+
+class Script(Subprocess): # pragma: no cover
+ pass
+
+
+class Session(object): # pragma: no cover
+
+ cookieName = "DOGTAIL_SESSION_COOKIE"
+
+ def __init__(self, sessionBinary, scriptCmdList=[], scriptDelay=20, logout=True):
+ testBinary(sessionBinary)
+ self.sessionBinary = sessionBinary
+ self.script = Script(scriptCmdList)
+ self.scriptDelay = scriptDelay
+ self.logout = logout
+ self.xserver = XServer()
+ self._cookie = None
+ self._environment = None
+
+ def start(self):
+ self.xinitrcFileObj = scratchFile('xinitrc')
+ self.xserver.xinitrc = self.xinitrcFileObj.name
+ self._buildXInitRC(self.xinitrcFileObj)
+ xServerPid = self.xserver.start()
+ time.sleep(self.scriptDelay)
+ self.script.environ = self.environment
+ scriptPid = self.script.start()
+ return (xServerPid, scriptPid)
+
+ @property
+ def environment(self):
+ def isSessionProcess(fileName):
+ try:
+ if os.path.realpath(path + 'exe') != ('/usr/bin/plasma-desktop'
+ if self.sessionBinary.split('/')[-1] == 'startkde'
+ else self.sessionBinary):
+ return False
+ except OSError:
+ return False
+ pid = fileName.split('/')[2]
+ if pid == 'self' or pid == str(os.getpid()):
+ return False
+ return True
+
+ def getEnvDict(fileName):
+ try:
+ envString = open(fileName, 'r').read()
+ except IOError:
+ return {}
+ envItems = envString.split('\x00')
+ envDict = {}
+ for item in envItems:
+ if not '=' in item:
+ continue
+ k, v = item.split('=', 1)
+ envDict[k] = v
+ return envDict
+
+ def isSessionEnv(envDict):
+ if not envDict:
+ return False
+ if envDict.get(self.cookieName, 'notacookie') == self.cookie:
+ return True
+ return False
+
+ for path in glob.glob('/proc/*/'):
+ if not isSessionProcess(path):
+ continue
+ envFile = path + 'environ'
+ envDict = getEnvDict(envFile)
+ if isSessionEnv(envDict):
+ # print path
+ # print envDict
+ self._environment = envDict
+ if not self._environment:
+ raise RuntimeError("Can't find our environment!")
+ return self._environment
+
+ def wait(self):
+ self.script.wait()
+ return self.xserver.wait()
+
+ def stop(self):
+ try:
+ self.script.stop()
+ except OSError:
+ pass
+ self.xserver.stop()
+
+ def attemptLogout(self):
+ logoutScript = Script('dogtail-logout',
+ environ=self.environment)
+ logoutScript.start()
+ logoutScript.wait()
+
+ @property
+ def cookie(self):
+ if not self._cookie:
+ self._cookie = "%X" % random.getrandbits(16)
+ return self._cookie
+
+ def _buildXInitRC(self, fileObj):
+ lines = [
+ "export %s=%s" % (self.cookieName, self.cookie),
+ "gsettings set org.gnome.desktop.interface toolkit-accessibility true",
+ ". /etc/X11/xinit/xinitrc-common",
+ "export %s" % self.cookieName,
+ "exec -l $SHELL -c \"$CK_XINIT_SESSION $SSH_AGENT %s\"" %
+ (self.sessionBinary),
+ ""]
+
+ fileObj.write('\n'.join(lines).strip())
+ fileObj.flush()
diff --git a/build/lib/dogtail/tc.py b/build/lib/dogtail/tc.py
new file mode 100644
index 0000000000..c7ef9f5bc2
--- /dev/null
+++ b/build/lib/dogtail/tc.py
@@ -0,0 +1,230 @@
+# -*- coding: utf-8 -*-
+"""Test Case magic
+
+Author: Ed Rousseau <rousseau@redhat.com>"""
+__author__ = "Ed Rousseau <rousseau@redhat.com>"
+
+import os
+import os.path
+from config import config
+from logging import ResultsLogger, TimeStamp
+from PIL import Image, ImageChops, ImageStat
+from __builtin__ import unicode, long
+
+
+class TC(object): # pragma: no cover
+
+ """
+ The Test Case Superclass
+ """
+ logger = ResultsLogger()
+
+ def __init__(self):
+ self.encoding = config.encoding
+ # ascii + unicode. 8 bit extended char has been ripped out
+ self.supportedtypes = (
+ "ascii", "utf-8", "utf-16", "utf-16-be", "utf-16-le", "unicode-escape", "raw-unicode-escape",
+ "big5", "gb18030", "eucJP", "eucKR", "shiftJIS")
+
+ # String comparison function
+ def compare(self, label, baseline, undertest, encoding=config.encoding):
+ """
+ Compares 2 strings to see if they are the same. The user may specify
+ the encoding to which the two strings are to be normalized for the
+ comparison. Default encoding is the default system encoding.
+ Normalization to extended 8 bit charactersets is not supported.
+
+ When the origin of either baseline or undertest is a text file whose
+ encoding is something other than ASCII, it is necessary to use
+ codecs.open() instead of open(), so the file's encoding may be
+ specified.
+ """
+ self.label = label.strip()
+ self.baseline = baseline
+ self.undertest = undertest
+ for string in [self.baseline, self.undertest]:
+ try:
+ string = unicode(string, 'utf-8')
+ except TypeError:
+ pass
+ self.encoding = encoding
+
+ # Normalize the encoding type for the comparaison based on
+ # self.encoding
+ if self.encoding in self.supportedtypes:
+ self.baseline = (self.baseline).encode(self.encoding)
+ self.undertest = (self.undertest).encode(self.encoding)
+ # Compare the strings
+ if self.baseline == self.undertest:
+ self.result = {self.label: "Passed"}
+ else:
+ self.result = {self.label: "Failed - " + self.encoding +
+ " strings do not match. " + self.baseline + " expected: Got " + self.undertest}
+ # Pass the test result to the ResultsLogger for writing
+ TC.logger.log(self.result)
+ return self.result
+
+ else:
+ # We should probably raise an exception here
+ self.result = {
+ self.label: "ERROR - " + self.encoding + " is not a supported encoding type"}
+ TC.logger.log(self.result)
+ return self.result
+
+
+# String Test Case subclass
+class TCString(TC): # pragma: no cover
+
+ """
+ String Test Case Class
+ """
+
+ def __init__(self):
+ TC.__init__(self)
+
+# Image test case subclass
+
+
+class TCImage(TC): # pragma: no cover
+
+ """
+ Image Test Case Class.
+ """
+
+ def compare(self, label, baseline, undertest):
+ for _file in (baseline, undertest):
+ if type(_file) is not unicode and type(_file) is not str:
+ raise TypeError("Need filenames!")
+ self.label = label.strip()
+ self.baseline = baseline.strip()
+ self.undertest = undertest.strip()
+ diffName = TimeStamp().fileStamp("diff") + ".png"
+ self.diff = os.path.normpath(
+ os.path.sep.join((config.scratchDir, diffName)))
+
+ self.baseImage = Image.open(self.baseline)
+ self.testImage = Image.open(self.undertest)
+ try:
+ if self.baseImage.size != self.testImage.size:
+ self.result = {
+ self.label: "Failed - images are different sizes"}
+ raise StopIteration
+
+ self.diffImage = ImageChops.difference(self.baseImage,
+ self.testImage)
+ self.diffImage.save(self.diff)
+ result = False
+ for stat in ('stddev', 'mean', 'sum2'):
+ for item in getattr(ImageStat.Stat(self.diffImage), stat):
+ if item:
+ self.result = {self.label: "Failed - see %s" %
+ self.diff}
+ raise StopIteration
+ else:
+ result = True
+ except StopIteration:
+ result = False
+
+ if result:
+ self.result = {self.label: "Passed"}
+
+ TC.logger.log(self.result)
+ return self.result
+
+
+class TCNumber(TC): # pragma: no cover
+
+ """
+ Number Comparaison Test Case Class
+ """
+
+ def __init__(self):
+ TC.__init__(self)
+ self.supportedtypes = ("int", "long", "float", "complex", "oct", "hex")
+
+ # Compare 2 numbers by the type provided in the type arg
+ def compare(self, label, baseline, undertest, type):
+ """
+ Compares 2 numbers to see if they are the same. The user may specify
+ how to normalize mixed type comparisons via the type argument.
+ """
+ self.label = label.strip()
+ self.baseline = baseline
+ self.undertest = undertest
+ self.type = type.strip()
+
+ # If we get a valid type, convert to that type and compare
+ if self.type in self.supportedtypes:
+ # Normalize for comparison
+ if self.type == "int":
+ self.baseline = int(self.baseline)
+ self.undertest = int(self.undertest)
+ elif self.type == "long":
+ self.baseline = long(self.baseline)
+ self.undertest = long(self.undertest)
+ elif self.type == "float":
+ self.baseline = float(self.baseline)
+ self.undertest = float(self.undertest)
+ else:
+ self.baseline = complex(self.baseline)
+ self.undertest = complex(self.undertest)
+
+ # compare
+ if self.baseline == self.undertest:
+ self.result = {self.label: "Passed - numbers are the same"}
+ else:
+ self.result = {self.label: "Failed - " + str(
+ self.baseline) + " expected: Got " + str(self.undertest)}
+ TC.logger.log(self.result)
+ return self.result
+ else:
+ self.result = {
+ self.label: "Failed - " + self.type + " is not in list of supported types"}
+ TC.logger.log(self.result)
+ return self.result
+
+
+class TCBool(TC): # pragma: no cover
+
+ def __init__(self):
+ pass
+
+ def compare(self, label, _bool):
+ """
+ If _bool is True, pass.
+ If _bool is False, fail.
+ """
+ if type(_bool) is not bool:
+ raise TypeError
+ if _bool:
+ result = {label: "Passed"}
+ else:
+ result = {label: "Failed"}
+ TC.logger.log(result)
+
+from tree import Node
+
+
+class TCNode(TC): # pragma: no cover
+
+ def __init__(self):
+ pass
+
+ def compare(self, label, baseline, undertest):
+ """
+ If baseline is None, simply check that undertest is a Node.
+ If baseline is a Node, check that it is equal to undertest.
+ """
+ if baseline is not None and not isinstance(baseline, Node):
+ raise TypeError
+
+ if not isinstance(undertest, Node):
+ result = {label: "Failed - %s is not a Node" % undertest}
+ elif baseline is None:
+ result = {label: "Passed - %s is a Node" % undertest}
+ elif isinstance(baseline, Node):
+ if baseline == undertest:
+ result = {label: "Passed - %s == %s" % (baseline, undertest)}
+ else:
+ result = {label: "Failed - %s != %s" % (baseline, undertest)}
+ TC.logger.log(result)
diff --git a/build/lib/dogtail/tree.py b/build/lib/dogtail/tree.py
new file mode 100644
index 0000000000..e6050bbb98
--- /dev/null
+++ b/build/lib/dogtail/tree.py
@@ -0,0 +1,1318 @@
+"""Makes some sense of the AT-SPI API
+
+The tree API handles various things for you:
+ - fixes most timing issues
+ - can automatically generate (hopefully) highly-readable logs of what the
+script is doing
+ - traps various UI malfunctions, raising exceptions for them (again,
+hopefully improving the logs)
+
+The most important class is Node. Each Node is an element of the desktop UI.
+There is a tree of nodes, starting at 'root', with applications as its
+children, with the top-level windows and dialogs as their children. The various
+widgets that make up the UI appear as descendents in this tree. All of these
+elements (root, the applications, the windows, and the widgets) are represented
+as instances of Node in a tree (provided that the program of interest is
+correctly exporting its user-interface to the accessibility system). The Node
+class is a mixin for Accessible and the various Accessible interfaces.
+
+The Action class represents an action that the accessibility layer exports as
+performable on a specific node, such as clicking on it. It's a wrapper around
+Accessibility.Action.
+
+We often want to look for a node, based on some criteria, and this is provided
+by the Predicate class.
+
+Dogtail implements a high-level searching system, for finding a node (or
+nodes) satisfying whatever criteria you are interested in. It does this with
+a 'backoff and retry' algorithm. This fixes most timing problems e.g. when a
+dialog is in the process of opening but hasn't yet done so.
+
+If a search fails, it waits 'config.searchBackoffDuration' seconds, and then
+tries again, repeatedly. After several failed attempts (determined by
+config.searchWarningThreshold) it will start sending warnings about the search
+to the debug log. If it still can't succeed after 'config.searchCutoffCount'
+attempts, it raises an exception containing details of the search. You can see
+all of this process in the debug log by setting 'config.debugSearching' to True
+
+We also automatically add a short delay after each action
+('config.defaultDelay' gives the time in seconds). We'd hoped that the search
+backoff and retry code would eliminate the need for this, but unfortunately we
+still run into timing issues. For example, Evolution (and probably most
+other apps) set things up on new dialogs and wizard pages as they appear, and
+we can run into 'setting wars' where the app resets the widgetry to defaults
+after our script has already filled out the desired values, and so we lose our
+values. So we give the app time to set the widgetry up before the rest of the
+script runs.
+
+The classes trap various UI malfunctions and raise exceptions that better
+describe what went wrong. For example, they detects attempts to click on an
+insensitive UI element and raise a specific exception for this.
+
+Unfortunately, some applications do not set up the 'sensitive' state
+correctly on their buttons (e.g. Epiphany on form buttons in a web page). The
+current workaround for this is to set config.ensureSensitivity=False, which
+disables the sensitivity testing.
+
+Authors: Zack Cerza <zcerza@redhat.com>, David Malcolm <dmalcolm@redhat.com>
+"""
+__author__ = """Zack Cerza <zcerza@redhat.com>,
+David Malcolm <dmalcolm@redhat.com>
+"""
+
+from config import config
+if config.checkForA11y:
+ from utils import checkForA11y
+ checkForA11y()
+
+import predicate
+from time import sleep
+from utils import doDelay
+from utils import Blinker
+from utils import Lock
+import rawinput
+import path
+from __builtin__ import xrange
+
+from logging import debugLogger as logger
+
+try:
+ import pyatspi
+ import Accessibility
+except ImportError: # pragma: no cover
+ raise ImportError("Error importing the AT-SPI bindings")
+
+# We optionally import the bindings for libWnck.
+try:
+ from gi.repository import Wnck
+ gotWnck = True # pragma: no cover
+except ImportError:
+ # Skip this warning, since the functionality is almost entirely nonworking anyway.
+ # print "Warning: Dogtail could not import the Python bindings for
+ # libwnck. Window-manager manipulation will not be available."
+ gotWnck = False
+
+from gi.repository import GLib
+
+haveWarnedAboutChildrenLimit = False
+
+
+class SearchError(Exception):
+ pass
+
+
+class NotSensitiveError(Exception):
+
+ """
+ The widget is not sensitive.
+ """
+ message = "Cannot %s %s. It is not sensitive."
+
+ def __init__(self, action):
+ self.action = action
+
+ def __str__(self):
+ return self.message % (self.action.name, self.action.node.getLogString())
+
+
+class ActionNotSupported(Exception):
+
+ """
+ The widget does not support the requested action.
+ """
+ message = "Cannot do '%s' action on %s"
+
+ def __init__(self, actionName, node):
+ self.actionName = actionName
+ self.node = node
+
+ def __str__(self):
+ return self.message % (self.actionName, self.node.getLogString())
+
+
+class Action(object):
+
+ """
+ Class representing an action that can be performed on a specific node
+ """
+ # Valid types of actions we know about. Feel free to add any you see.
+ types = ('click',
+ 'press',
+ 'release',
+ 'activate',
+ 'jump',
+ 'check',
+ 'dock',
+ 'undock',
+ 'open',
+ 'menu')
+
+ def __init__(self, node, action, index):
+ self.node = node
+ self.__action = action
+ self.__index = index
+
+ @property
+ def name(self):
+ return self.__action.getName(self.__index)
+
+ @property
+ def description(self):
+ return self.__action.getDescription(self.__index)
+
+ @property
+ def keyBinding(self):
+ return self.__action.getKeyBinding(self.__index)
+
+ def __str__(self):
+ return "[action | %s | %s ]" % \
+ (self.name, self.keyBinding)
+
+ def do(self):
+ """
+ Performs the given tree.Action, with appropriate delays and logging.
+ """
+ logger.log("%s on %s" % (self.name, self.node.getLogString()))
+ if not self.node.sensitive:
+ if config.ensureSensitivity:
+ raise NotSensitiveError(self)
+ else:
+ nSE = NotSensitiveError(self)
+ logger.log("Warning: " + str(nSE))
+ if config.blinkOnActions:
+ self.node.blink()
+ result = self.__action.doAction(self.__index)
+ doDelay(config.actionDelay)
+ return result
+
+
+class Node(object):
+
+ """
+ A node in the tree of UI elements. This class is mixed in with
+ Accessibility.Accessible to both make it easier to use and to add
+ additional functionality. It also has a debugName which is set up
+ automatically when doing searches.
+ """
+
+ def __setupUserData(self):
+ try:
+ len(self.user_data)
+ except (AttributeError, TypeError):
+ self.user_data = {}
+
+ def debugName():
+ doc = "debug name assigned during search operations"
+
+ def fget(self):
+ self.__setupUserData()
+ return self.user_data.get('debugName', None)
+
+ def fset(self, debugName):
+ self.__setupUserData()
+ self.user_data['debugName'] = debugName
+
+ return property(**locals())
+ debugName = debugName()
+ #
+ # Accessible
+ #
+
+ @property
+ def dead(self):
+ """Is the node dead (defunct) ?"""
+ try:
+ if self.roleName == 'invalid':
+ return True
+ self.role
+ self.name
+ if len(self) > 0:
+ self[0]
+ except:
+ return True
+ return False
+
+ @property
+ def children(self):
+ """a list of this Accessible's children"""
+ if self.parent and self.parent.roleName == 'hyper link':
+ print(self.parent.role)
+ return []
+ children = []
+ childCount = self.childCount
+ if childCount > config.childrenLimit:
+ global haveWarnedAboutChildrenLimit
+ if not haveWarnedAboutChildrenLimit:
+ logger.log("Only returning %s children. You may change "
+ "config.childrenLimit if you wish. This message will only"
+ " be printed once." % str(config.childrenLimit))
+ haveWarnedAboutChildrenLimit = True
+ childCount = config.childrenLimit
+ for i in range(childCount):
+ # Workaround for GNOME bug #465103
+ # also solution for GNOME bug #321273
+ try:
+ child = self[i]
+ except LookupError:
+ child = None
+ if child:
+ children.append(child)
+
+ invalidChildren = childCount - len(children)
+ if invalidChildren and config.debugSearching:
+ logger.log("Skipped %s invalid children of %s" %
+ (invalidChildren, str(self)))
+ try:
+ ht = self.queryHypertext()
+ for li in range(ht.getNLinks()):
+ link = ht.getLink(li)
+ for ai in range(link.nAnchors):
+ child = link.getObject(ai)
+ child.__setupUserData()
+ child.user_data['linkAnchor'] = \
+ LinkAnchor(node=child,
+ hypertext=ht,
+ linkIndex=li,
+ anchorIndex=ai)
+ children.append(child)
+ except (NotImplementedError, AttributeError):
+ pass
+
+ return children
+
+ roleName = property(Accessibility.Accessible.getRoleName)
+
+ role = property(Accessibility.Accessible.getRole)
+
+ indexInParent = property(Accessibility.Accessible.getIndexInParent)
+
+ #
+ # Action
+ #
+
+ # Needed to be renamed from doAction due to conflicts
+ # with 'Accessibility.Accessible.doAction' in gtk3 branch
+ def doActionNamed(self, name):
+ """
+ Perform the action with the specified name. For a list of actions
+ supported by this instance, check the 'actions' property.
+ """
+ actions = self.actions
+ if name in actions:
+ return actions[name].do()
+ raise ActionNotSupported(name, self)
+
+ @property
+ def actions(self):
+ """
+ A dictionary of supported action names as keys, with Action objects as
+ values. Common action names include:
+
+ 'click' 'press' 'release' 'activate' 'jump' 'check' 'dock' 'undock'
+ 'open' 'menu'
+ """
+ actions = {}
+ try:
+ action = self.queryAction()
+ for i in range(action.nActions):
+ a = Action(self, action, i)
+ actions[action.getName(i)] = a
+ finally:
+ return actions
+
+ def combovalue():
+ doc = "The value (as a string) currently selected in the combo box."
+
+ def fget(self):
+ return self.name
+
+ def fset(self, value):
+ logger.log("Setting combobox %s to '%s'" % (self.getLogString(),
+ value))
+ self.childNamed(childName=value).doActionNamed('click')
+ doDelay()
+
+ return property(**locals())
+ combovalue = combovalue()
+ #
+ # Hypertext and Hyperlink
+ #
+
+ @property
+ def URI(self):
+ try:
+ return self.user_data['linkAnchor'].URI
+ except (KeyError, AttributeError):
+ raise NotImplementedError
+
+ #
+ # Text and EditableText
+ #
+ def text():
+ doc = """For instances with an AccessibleText interface, the text as a
+ string. This is read-only, unless the instance also has an
+ AccessibleEditableText interface. In this case, you can write values
+ to the attribute. This will get logged in the debug log, and a delay
+ will be added.
+
+ If this instance corresponds to a password entry, use the passwordText
+ property instead."""
+
+ def fget(self):
+ try:
+ return self.queryText().getText(0, -1)
+ except NotImplementedError:
+ return None
+
+ def fset(self, text):
+ try:
+ if config.debugSearching:
+ msg = "Setting text of %s to %s"
+ # Let's not get too crazy if 'text' is really large...
+ # FIXME: Sometimes the next line screws up Unicode strings.
+ if len(text) > 140:
+ txt = text[:134] + " [...]"
+ else:
+ txt = text
+ logger.log(msg % (self.getLogString(), "'%s'" % txt))
+ self.queryEditableText().setTextContents(text)
+ except NotImplementedError:
+ raise AttributeError("can't set attribute")
+
+ return property(**locals())
+ text = text()
+
+ def caretOffset():
+
+ def fget(self):
+ """For instances with an AccessibleText interface, the caret
+ offset as an integer."""
+ return self.queryText().caretOffset
+
+ def fset(self, offset):
+ return self.queryText().setCaretOffset(offset)
+
+ return property(**locals())
+ caretOffset = caretOffset()
+
+ #
+ # Component
+ #
+
+ @property
+ def position(self):
+ """A tuple containing the position of the Accessible: (x, y)"""
+ return self.queryComponent().getPosition(pyatspi.DESKTOP_COORDS)
+
+ @property
+ def size(self):
+ """A tuple containing the size of the Accessible: (w, h)"""
+ return self.queryComponent().getSize()
+
+ @property
+ def extents(self):
+ """A tuple containing the location and size of the Accessible:
+ (x, y, w, h)"""
+ try:
+ ex = self.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
+ return (ex.x, ex.y, ex.width, ex.height)
+ except NotImplementedError:
+ return None
+
+ def contains(self, x, y):
+ try:
+ return self.queryComponent().contains(x, y, pyatspi.DESKTOP_COORDS)
+ except NotImplementedError:
+ return False
+
+ def getChildAtPoint(self, x, y):
+ node = self
+ while True:
+ try:
+ child = node.queryComponent().getAccessibleAtPoint(x, y,
+ pyatspi.DESKTOP_COORDS)
+ if child and child.contains(x, y):
+ node = child
+ else:
+ break
+ except NotImplementedError:
+ break
+ if node and node.contains(x, y):
+ return node
+ else:
+ return None
+
+ def grabFocus(self):
+ "Attempts to set the keyboard focus to this Accessible."
+ return self.queryComponent().grabFocus()
+
+ # def blink(self, count=2):
+ #"""
+ # Blink, baby!
+ #"""
+ # if not self.extents: return False
+ # else:
+ #(x, y, w, h) = self.extents
+ #from utils import Blinker
+ #blinkData = Blinker(x, y, w, h, count)
+ # return True
+
+ def click(self, button=1):
+ """
+ Generates a raw mouse click event, using the specified button.
+ - 1 is left,
+ - 2 is middle,
+ - 3 is right.
+ """
+ logger.log("Clicking on %s" % self.getLogString())
+ clickX = self.position[0] + self.size[0] / 2
+ clickY = self.position[1] + self.size[1] / 2
+ if config.debugSearching:
+ logger.log("raw click on %s %s at (%s,%s)" %
+ (self.name, self.getLogString(), str(clickX), str(clickY)))
+ rawinput.click(clickX, clickY, button)
+
+ def doubleClick(self, button=1):
+ """
+ Generates a raw mouse double-click event, using the specified button.
+ """
+ clickX = self.position[0] + self.size[0] / 2
+ clickY = self.position[1] + self.size[1] / 2
+ if config.debugSearching:
+ logger.log("raw click on %s %s at (%s,%s)" %
+ (self.name, self.getLogString(), str(clickX), str(clickY)))
+ rawinput.doubleClick(clickX, clickY, button)
+
+ def point(self, mouseDelay=None):
+ """
+ Move mouse cursor to the center of the widget.
+ """
+ pointX = self.position[0] + self.size[0] / 2
+ pointY = self.position[1] + self.size[1] / 2
+ logger.log("Pointing on %s %s at (%s,%s)" %
+ (self.name, self.getLogString(), str(pointX), str(pointY)))
+ rawinput.registry.generateMouseEvent(pointX, pointY, 'abs')
+ if mouseDelay:
+ doDelay(mouseDelay)
+ else:
+ doDelay()
+
+ #
+ # RelationSet
+ #
+ @property
+ def labeler(self):
+ """'labeller' (read-only list of Node instances):
+ The node(s) that is/are a label for this node. Generated from
+ 'relations'.
+ """
+ relationSet = self.getRelationSet()
+ for relation in relationSet:
+ if relation.getRelationType() == pyatspi.RELATION_LABELLED_BY:
+ if relation.getNTargets() == 1:
+ return relation.getTarget(0)
+ targets = []
+ for i in range(relation.getNTargets()):
+ targets.append(relation.getTarget(i))
+ return targets
+ labeller = labeler
+
+ @property
+ def labelee(self):
+ """'labellee' (read-only list of Node instances):
+ The node(s) that this node is a label for. Generated from 'relations'.
+ """
+ relationSet = self.getRelationSet()
+ for relation in relationSet:
+ if relation.getRelationType() == pyatspi.RELATION_LABEL_FOR:
+ if relation.getNTargets() == 1:
+ return relation.getTarget(0)
+ targets = []
+ for i in range(relation.getNTargets()):
+ targets.append(relation.getTarget(i))
+ return targets
+ labellee = labelee
+
+ #
+ # StateSet
+ #
+ @property
+ def sensitive(self):
+ """Is the Accessible sensitive (i.e. not greyed out)?"""
+ return self.getState().contains(pyatspi.STATE_SENSITIVE)
+
+ @property
+ def showing(self):
+ return self.getState().contains(pyatspi.STATE_SHOWING)
+
+ @property
+ def focusable(self):
+ """Is the Accessible capable of having keyboard focus?"""
+ return self.getState().contains(pyatspi.STATE_FOCUSABLE)
+
+ @property
+ def focused(self):
+ """Does the Accessible have keyboard focus?"""
+ return self.getState().contains(pyatspi.STATE_FOCUSED)
+
+ @property
+ def checked(self):
+ """Is the Accessible a checked checkbox?"""
+ return self.getState().contains(pyatspi.STATE_CHECKED)
+
+ @property
+ def isChecked(self):
+ """Is the Accessible a checked checkbox? Compatibility property, same as Node.checked."""
+ return self.checked
+
+ #
+ # Selection
+ #
+
+ def selectAll(self):
+ """Selects all children."""
+ result = self.querySelection().selectAll()
+ doDelay()
+ return result
+
+ def deselectAll(self):
+ """Deselects all selected children."""
+ result = self.querySelection().clearSelection()
+ doDelay()
+ return result
+
+ def select(self):
+ """Selects the Accessible."""
+ try:
+ parent = self.parent
+ except AttributeError:
+ raise NotImplementedError
+ result = parent.querySelection().selectChild(self.indexInParent)
+ doDelay()
+ return result
+
+ def deselect(self):
+ """Deselects the Accessible."""
+ try:
+ parent = self.parent
+ except AttributeError:
+ raise NotImplementedError
+ result = parent.querySelection().deselectChild(self.indexInParent)
+ doDelay()
+ return result
+
+ @property
+ def isSelected(self):
+ """Is the Accessible selected? Compatibility property, same as Node.selected."""
+ try:
+ parent = self.parent
+ except AttributeError:
+ raise NotImplementedError
+ return parent.querySelection().isChildSelected(self.indexInParent)
+
+ @property
+ def selected(self):
+ """Is the Accessible selected?"""
+ return self.isSelected
+
+ @property
+ def selectedChildren(self):
+ """Returns a list of children that are selected."""
+ # TODO: hideChildren for Hyperlinks?
+ selection = self.querySelection()
+ selectedChildren = []
+ for i in xrange(selection.nSelectedChildren):
+ selectedChildren.append(selection.getSelectedChild(i))
+
+ #
+ # Value
+ #
+
+ def value():
+ doc = "The value contained by the AccessibleValue interface."
+
+ def fget(self):
+ try:
+ return self.queryValue().currentValue
+ except NotImplementedError:
+ pass
+
+ def fset(self, value):
+ self.queryValue().currentValue = value
+
+ return property(**locals())
+ value = value()
+
+ @property
+ def minValue(self):
+ """The minimum value of self.value"""
+ try:
+ return self.queryValue().minimumValue
+ except NotImplementedError:
+ pass
+
+ @property
+ def minValueIncrement(self):
+ """The minimum value increment of self.value"""
+ try:
+ return self.queryValue().minimumIncrement
+ except NotImplementedError:
+ pass
+
+ @property
+ def maxValue(self):
+ """The maximum value of self.value"""
+ try:
+ return self.queryValue().maximumValue
+ except NotImplementedError:
+ pass
+
+ def typeText(self, string):
+ """
+ Type the given text into the node, with appropriate delays and
+ logging.
+ """
+ logger.log("Typing text into %s: '%s'" % (self.getLogString(), string))
+
+ if self.focusable:
+ if not self.focused:
+ try:
+ self.grabFocus()
+ except Exception:
+ logger.log("Node is focusable but I can't grabFocus!")
+ rawinput.typeText(string)
+ else:
+ logger.log("Node is not focusable; falling back to inserting text")
+ et = self.queryEditableText()
+ et.insertText(self.caretOffset, string, len(string))
+ self.caretOffset += len(string)
+ doDelay()
+
+ def keyCombo(self, comboString):
+ if config.debugSearching:
+ logger.log("Pressing keys '%s' into %s" %
+ (comboString, self.getLogString()))
+ if self.focusable:
+ if not self.focused:
+ try:
+ self.grabFocus()
+ except Exception:
+ logger.log("Node is focusable but I can't grabFocus!")
+ else:
+ logger.log("Node is not focusable; trying key combo anyway")
+ rawinput.keyCombo(comboString)
+
+ def getLogString(self):
+ """
+ Get a string describing this node for the logs,
+ respecting the config.absoluteNodePaths boolean.
+ """
+ if config.absoluteNodePaths:
+ return self.getAbsoluteSearchPath()
+ else:
+ return str(self)
+
+ def satisfies(self, pred):
+ """
+ Does this node satisfy the given predicate?
+ """
+ # the logic is handled by the predicate:
+ assert isinstance(pred, predicate.Predicate)
+ return pred.satisfiedByNode(self)
+
+ def dump(self, type='plain', fileName=None):
+ import dump
+ dumper = getattr(dump, type)
+ dumper(self, fileName)
+
+ def getAbsoluteSearchPath(self):
+ """
+ FIXME: this needs rewriting...
+ Generate a SearchPath instance giving the 'best'
+ way to find the Accessible wrapped by this node again, starting
+ at the root and applying each search in turn.
+
+ This is somewhat analagous to an absolute path in a filesystem,
+ except that some of searches may be recursive, rather than just
+ searching direct children.
+
+ Used by the recording framework for identifying nodes in a
+ persistent way, independent of the style of script being
+ written.
+
+ FIXME: try to ensure uniqueness
+ FIXME: need some heuristics to get 'good' searches, whatever
+ that means
+ """
+ if config.debugSearchPaths:
+ logger.log("getAbsoluteSearchPath(%s)" % self)
+
+ if self.roleName == 'application':
+ result = path.SearchPath()
+ result.append(predicate.IsAnApplicationNamed(self.name), False)
+ return result
+ else:
+ if self.parent:
+ (ancestor, pred, isRecursive) = self.getRelativeSearch()
+ if config.debugSearchPaths:
+ logger.log("got ancestor: %s" % ancestor)
+
+ ancestorPath = ancestor.getAbsoluteSearchPath()
+ ancestorPath.append(pred, isRecursive)
+ return ancestorPath
+ else:
+ # This should be the root node:
+ return path.SearchPath()
+
+ def getRelativeSearch(self):
+ """
+ Get a (ancestorNode, predicate, isRecursive) triple that identifies the
+ best way to find this Node uniquely.
+ FIXME: or None if no such search exists?
+ FIXME: may need to make this more robust
+ FIXME: should this be private?
+ """
+ if config.debugSearchPaths:
+ logger.log("getRelativeSearchPath(%s)" % self)
+
+ assert self
+ assert self.parent
+
+ isRecursive = False
+ ancestor = self.parent
+
+ # iterate up ancestors until you reach an identifiable one,
+ # setting the search to be isRecursive if need be:
+ while not self.__nodeIsIdentifiable(ancestor):
+ ancestor = ancestor.parent
+ isRecursive = True
+
+ # Pick the most appropriate predicate for finding this node:
+ if self.labellee:
+ if self.labellee.name:
+ return (ancestor, predicate.IsLabelledAs(self.labellee.name), isRecursive)
+
+ if self.roleName == 'menu':
+ return (ancestor, predicate.IsAMenuNamed(self.name), isRecursive)
+ elif self.roleName == 'menu item' or self.roleName == 'check menu item':
+ return (ancestor, predicate.IsAMenuItemNamed(self.name), isRecursive)
+ elif self.roleName == 'text':
+ return (ancestor, predicate.IsATextEntryNamed(self.name), isRecursive)
+ elif self.roleName == 'push button':
+ return (ancestor, predicate.IsAButtonNamed(self.name), isRecursive)
+ elif self.roleName == 'frame':
+ return (ancestor, predicate.IsAWindowNamed(self.name), isRecursive)
+ elif self.roleName == 'dialog':
+ return (ancestor, predicate.IsADialogNamed(self.name), isRecursive)
+ else:
+ pred = predicate.GenericPredicate(
+ name=self.name, roleName=self.roleName)
+ return (ancestor, pred, isRecursive)
+
+ def __nodeIsIdentifiable(self, ancestor):
+ if ancestor.labellee:
+ return True
+ elif ancestor.name:
+ return True
+ elif not ancestor.parent:
+ return True
+ else:
+ return False
+
+ def _fastFindChild(self, pred, recursive=True):
+ """
+ Searches for an Accessible using methods from pyatspi.utils
+ """
+ if isinstance(pred, predicate.Predicate):
+ pred = pred.satisfiedByNode
+ if not recursive:
+ cIter = iter(self)
+ while True:
+ try:
+ child = cIter.next()
+ except StopIteration:
+ break
+ if child is not None:
+ if pred(child):
+ return child
+ else:
+ return pyatspi.utils.findDescendant(self, pred)
+
+ def findChild(self, pred, recursive=True, debugName=None,
+ retry=True, requireResult=True):
+ """
+ Search for a node satisyfing the predicate, returning a Node.
+
+ If retry is True (the default), it makes multiple attempts,
+ backing off and retrying on failure, and eventually raises a
+ descriptive exception if the search fails.
+
+ If retry is False, it gives up after one attempt.
+
+ If requireResult is True (the default), an exception is raised after all
+ attempts have failed. If it is false, the function simply returns None.
+ """
+ def describeSearch(parent, pred, recursive, debugName):
+ """
+ Internal helper function
+ """
+ if recursive:
+ noun = "descendent"
+ else:
+ noun = "child"
+ if debugName is None:
+ debugName = pred.describeSearchResult()
+ return "%s of %s: %s" % (noun, parent.getLogString(), debugName)
+
+ assert isinstance(pred, predicate.Predicate)
+ numAttempts = 0
+ while numAttempts < config.searchCutoffCount:
+ if numAttempts >= config.searchWarningThreshold or config.debugSearching:
+ logger.log("searching for %s (attempt %i)" %
+ (describeSearch(self, pred, recursive, debugName), numAttempts))
+
+ result = self._fastFindChild(pred.satisfiedByNode, recursive)
+ if result:
+ assert isinstance(result, Node)
+ if debugName:
+ result.debugName = debugName
+ else:
+ result.debugName = pred.describeSearchResult()
+ return result
+ else:
+ if not retry:
+ break
+ numAttempts += 1
+ if config.debugSearching or config.debugSleep:
+ logger.log("sleeping for %f" %
+ config.searchBackoffDuration)
+ sleep(config.searchBackoffDuration)
+ if requireResult:
+ raise SearchError(describeSearch(self, pred, recursive, debugName))
+
+ # The canonical "search for multiple" method:
+ def findChildren(self, pred, recursive=True, isLambda=False):
+ """
+ Find all children/descendents satisfying the predicate.
+ """
+ if isLambda is True:
+ nodes = self.findChildren(predicate.GenericPredicate(), recursive=recursive)
+ result = []
+ for node in nodes:
+ try:
+ if pred(node):
+ result.append(node)
+ except:
+ pass
+ return result
+ if isinstance(pred, predicate.Predicate):
+ pred = pred.satisfiedByNode
+ if not recursive:
+ cIter = iter(self)
+ result = []
+ while True:
+ try:
+ child = cIter.next()
+ except StopIteration:
+ break
+ if child is not None and pred(child):
+ result.append(child)
+ return result
+ else:
+ descendants = []
+ while True:
+ try:
+ descendants = pyatspi.utils.findAllDescendants(self, pred)
+ break
+ except (GLib.GError, TypeError):
+ continue
+ return descendants
+
+ # The canonical "search above this node" method:
+ def findAncestor(self, pred):
+ """
+ Search up the ancestry of this node, returning the first Node
+ satisfying the predicate, or None.
+ """
+ assert isinstance(pred, predicate.Predicate)
+ candidate = self.parent
+ while candidate is not None:
+ if candidate.satisfies(pred):
+ return candidate
+ else:
+ candidate = candidate.parent
+ # Not found:
+ return None
+
+ # Various wrapper/helper search methods:
+ def child(self, name='', roleName='', description='', label='', recursive=True, retry=True, debugName=None):
+ """
+ Finds a child satisying the given criteria.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.GenericPredicate(name=name, roleName=roleName, description=description, label=label), recursive=recursive, retry=retry, debugName=debugName)
+
+ def isChild(self, name='', roleName='', description='', label='', recursive=True, retry=False, debugName=None):
+ """
+ Determines whether a child satisying the given criteria exists.
+
+ This is implemented using findChild, but will not automatically retry
+ if no such child is found. To make the function retry multiple times set retry to True.
+ Returns a boolean value depending on whether the child was eventually found. Similar to
+ 'child', yet it catches SearchError exception to provide for False results, will raise
+ any other exceptions. It also logs the search.
+ """
+ found = True
+ try:
+ self.findChild(
+ predicate.GenericPredicate(
+ name=name, roleName=roleName, description=description, label=label),
+ recursive=recursive, retry=retry, debugName=debugName)
+ except SearchError:
+ found = False
+ return found
+
+ def menu(self, menuName, recursive=True):
+ """
+ Search below this node for a menu with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsAMenuNamed(menuName=menuName), recursive)
+
+ def menuItem(self, menuItemName, recursive=True):
+ """
+ Search below this node for a menu item with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsAMenuItemNamed(menuItemName=menuItemName), recursive)
+
+ def textentry(self, textEntryName, recursive=True):
+ """
+ Search below this node for a text entry with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsATextEntryNamed(textEntryName=textEntryName), recursive)
+
+ def button(self, buttonName, recursive=True):
+ """
+ Search below this node for a button with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsAButtonNamed(buttonName=buttonName), recursive)
+
+ def childLabelled(self, labelText, recursive=True):
+ """
+ Search below this node for a child labelled with the given text.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsLabelledAs(labelText), recursive)
+
+ def childNamed(self, childName, recursive=True):
+ """
+ Search below this node for a child with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsNamed(childName), recursive)
+
+ def tab(self, tabName, recursive=True):
+ """
+ Search below this node for a tab with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsATabNamed(tabName=tabName), recursive)
+
+ def getUserVisibleStrings(self):
+ """
+ Get all user-visible strings in this node and its descendents.
+
+ (Could be implemented as an attribute)
+ """
+ result = []
+ if self.name:
+ result.append(self.name)
+ if self.description:
+ result.append(self.description)
+ try:
+ children = self.children
+ except Exception:
+ return result
+ for child in children:
+ result.extend(child.getUserVisibleStrings())
+ return result
+
+ def blink(self):
+ """
+ Blink, baby!
+ """
+ if not self.extents:
+ return False
+ else:
+ (x, y, w, h) = self.extents
+ Blinker(x, y, w, h)
+ return True
+
+
+class LinkAnchor(object):
+
+ """
+ Class storing info about an anchor within an Accessibility.Hyperlink, which
+ is in turn stored within an Accessibility.Hypertext.
+ """
+
+ def __init__(self, node, hypertext, linkIndex, anchorIndex):
+ self.node = node
+ self.hypertext = hypertext
+ self.linkIndex = linkIndex
+ self.anchorIndex = anchorIndex
+
+ @property
+ def link(self):
+ return self.hypertext.getLink(self.linkIndex)
+
+ @property
+ def URI(self):
+ return self.link.getURI(self.anchorIndex)
+
+
+class Root (Node):
+
+ """
+ FIXME:
+ """
+
+ def applications(self):
+ """
+ Get all applications.
+ """
+ return root.findChildren(predicate.GenericPredicate(
+ roleName="application"), recursive=False)
+
+ def application(self, appName, retry=True):
+ """
+ Gets an application by name, returning an Application instance
+ or raising an exception.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return root.findChild(predicate.IsAnApplicationNamed(appName), recursive=False, retry=retry)
+
+
+class Application (Node):
+
+ def dialog(self, dialogName, recursive=False):
+ """
+ Search below this node for a dialog with the given name,
+ returning a Window instance.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+
+ FIXME: should this method activate the dialog?
+ """
+ return self.findChild(predicate.IsADialogNamed(dialogName=dialogName), recursive)
+
+ def window(self, windowName, recursive=False):
+ """
+ Search below this node for a window with the given name,
+ returning a Window instance.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+
+ FIXME: this bit isn't true:
+ The window will be automatically activated (raised and focused
+ by the window manager) if wnck bindings are available.
+ """
+ result = self.findChild(
+ predicate.IsAWindowNamed(windowName=windowName), recursive)
+ # FIXME: activate the WnckWindow ?
+ # if gotWnck:
+ # result.activate()
+ return result
+
+ def getWnckApplication(self): # pragma: no cover
+ """
+ Get the wnck.Application instance for this application, or None
+
+ Currently implemented via a hack: requires the app to have a
+ window, and looks up the application of that window
+
+ wnck.Application can give you the pid, the icon, etc
+
+ FIXME: untested
+ """
+ window = self.child(roleName='frame')
+ if window:
+ wnckWindow = window.getWnckWindow()
+ return wnckWindow.get_application()
+
+
+class Window (Node):
+
+ def getWnckWindow(self): # pragma: no cover
+ """
+ Get the wnck.Window instance for this window, or None
+ """
+ # FIXME: this probably needs rewriting:
+ screen = Wnck.screen_get_default()
+
+ # You have to force an update before any of the wnck methods
+ # do anything:
+ screen.force_update()
+
+ for wnckWindow in screen.get_windows():
+ # FIXME: a dubious hack: search by window title:
+ if wnckWindow.get_name() == self.name:
+ return wnckWindow
+
+ def activate(self): # pragma: no cover
+ """
+ Activates the wnck.Window associated with this Window.
+
+ FIXME: doesn't yet work
+ """
+ wnckWindow = self.getWnckWindow()
+ # Activate it with a timestamp of 0; this may confuse
+ # alt-tabbing through windows etc:
+ # FIXME: is there a better way of getting a timestamp?
+ # gdk_x11_get_server_time (), with a dummy window
+ wnckWindow.activate(0)
+
+
+class Wizard (Window):
+
+ """
+ Note that the buttons of a GnomeDruid were not accessible until
+ recent versions of libgnomeui. This is
+ http://bugzilla.gnome.org/show_bug.cgi?id=157936
+ and is fixed in gnome-2.10 and gnome-2.12 (in CVS libgnomeui);
+ there's a patch attached to that bug.
+
+ This bug is known to affect FC3; fixed in FC5
+ """
+
+ def __init__(self, node, debugName=None):
+ Node.__init__(self, node)
+ if debugName:
+ self.debugName = debugName
+ logger.log("%s is on '%s' page" % (self, self.getPageTitle()))
+
+ def currentPage(self):
+ """
+ Get the current page of this wizard
+
+ FIXME: this is currently a hack, supporting only GnomeDruid
+ """
+ pageHolder = self.child(roleName='panel')
+ for child in pageHolder.children:
+ # current child has SHOWING state set, we hope:
+ # print child
+ # print child.showing
+ if child.showing:
+ return child
+ raise "Unable to determine current page of %s" % self
+
+ def getPageTitle(self):
+ """
+ Get the string title of the current page of this wizard
+
+ FIXME: this is currently a total hack, supporting only GnomeDruid
+ """
+ currentPage = self.currentPage()
+ return currentPage.child(roleName='panel').child(roleName='panel').child(roleName='label', recursive=False).text
+
+ def clickForward(self):
+ """
+ Click on the 'Forward' button to advance to next page of wizard.
+
+ It will log the title of the new page that is reached.
+
+ FIXME: what if it's Next rather than Forward ???
+
+ This will only work if your libgnomeui has accessible buttons;
+ see above.
+ """
+ fwd = self.child("Forward")
+ fwd.click()
+
+ # Log the new wizard page; it's helpful when debugging scripts
+ logger.log("%s is now on '%s' page" % (self, self.getPageTitle()))
+ # FIXME disabled for now (can't get valid page titles)
+
+ def clickApply(self):
+ """
+ Click on the 'Apply' button to advance to next page of wizard.
+ FIXME: what if it's Finish rather than Apply ???
+
+ This will only work if your libgnomeui has accessible buttons;
+ see above.
+ """
+ fwd = self.child("Apply")
+ fwd.click()
+
+ # FIXME: debug logging?
+
+Accessibility.Accessible.__bases__ = (
+ Application, Root, Node,) + Accessibility.Accessible.__bases__
+
+try:
+ root = pyatspi.Registry.getDesktop(0)
+ root.debugName = 'root'
+except Exception: # pragma: no cover
+ # Warn if AT-SPI's desktop object doesn't show up.
+ logger.log(
+ "Error: AT-SPI's desktop is not visible. Do you have accessibility enabled?")
+
+# Check that there are applications running. Warn if none are.
+children = root.children
+if not children: # pragma: no cover
+ logger.log(
+ "Warning: AT-SPI's desktop is visible but it has no children. Are you running any AT-SPI-aware applications?")
+del children
+
+import os
+# sniff also imports from tree and we don't want to run this code from
+# sniff itself
+if not os.path.exists('/tmp/sniff_running.lock'):
+ if not os.path.exists('/tmp/sniff_refresh.lock'): # may have already been locked by dogtail.procedural
+ # tell sniff not to use auto-refresh while script using this module is
+ # running
+ sniff_lock = Lock(lockname='sniff_refresh.lock', randomize=False)
+ try:
+ sniff_lock.lock()
+ except OSError: # pragma: no cover
+ pass # lock was already present from other script instance or leftover from killed instance
+ # lock should unlock automatically on script exit.
+
+# Convenient place to set some debug variables:
+#config.debugSearching = True
+#config.absoluteNodePaths = True
+#config.logDebugToFile = False
diff --git a/build/lib/dogtail/utils.py b/build/lib/dogtail/utils.py
new file mode 100644
index 0000000000..be3c78adec
--- /dev/null
+++ b/build/lib/dogtail/utils.py
@@ -0,0 +1,395 @@
+# -*- coding: utf-8 -*-
+"""
+Various utilities
+
+Authors: Ed Rousseau <rousseau@redhat.com>, Zack Cerza <zcerza@redhat.com, David Malcolm <dmalcolm@redhat.com>
+"""
+
+__author__ = """Ed Rousseau <rousseau@redhat.com>,
+Zack Cerza <zcerza@redhat.com,
+David Malcolm <dmalcolm@redhat.com>
+"""
+
+import os
+import sys
+import subprocess
+import cairo
+import predicate
+import errno
+import shlex
+
+import gi
+gi.require_version('Gtk', '3.0')
+gi.require_version('Gdk', '3.0')
+
+from gi.repository import Gtk
+from gi.repository import GLib
+from config import config
+from time import sleep
+from logging import debugLogger as logger
+from logging import TimeStamp
+from __builtin__ import file
+
+
+def screenshot(file='screenshot.png', timeStamp=True):
+ """
+ This function wraps the ImageMagick import command to take a screenshot.
+
+ The file argument may be specified as 'foo', 'foo.png', or using any other
+ extension that ImageMagick supports. PNG is the default.
+
+ By default, screenshot filenames are in the format of foo_YYYYMMDD-hhmmss.png .
+ The timeStamp argument may be set to False to name the file foo.png.
+ """
+ if not isinstance(timeStamp, bool):
+ raise TypeError("timeStampt must be True or False")
+ # config is supposed to create this for us. If it's not there, bail.
+ assert os.path.isdir(config.scratchDir)
+
+ baseName = ''.join(file.split('.')[0:-1])
+ fileExt = file.split('.')[-1].lower()
+ if not baseName:
+ baseName = file
+ fileExt = 'png'
+
+ if timeStamp:
+ ts = TimeStamp()
+ newFile = ts.fileStamp(baseName) + '.' + fileExt
+ path = config.scratchDir + newFile
+ else:
+ newFile = baseName + '.' + fileExt
+ path = config.scratchDir + newFile
+
+ from gi.repository import Gdk
+ from gi.repository import GLib
+ from gi.repository import GdkPixbuf
+ rootWindow = Gdk.get_default_root_window()
+ geometry = rootWindow.get_geometry()
+ pixbuf = GdkPixbuf.Pixbuf(colorspace=GdkPixbuf.Colorspace.RGB,
+ has_alpha=False,
+ bits_per_sample=8,
+ width=geometry[2],
+ height=geometry[3])
+
+ pixbuf = Gdk.pixbuf_get_from_window(rootWindow, 0, 0,
+ geometry[2], geometry[3])
+ # GdkPixbuf.Pixbuf.save() needs 'jpeg' and not 'jpg'
+ if fileExt == 'jpg':
+ fileExt = 'jpeg'
+ try:
+ pixbuf.savev(path, fileExt, [], [])
+ except GLib.GError:
+ raise ValueError("Failed to save screenshot in %s format" % fileExt)
+ assert os.path.exists(path)
+ logger.log("Screenshot taken: " + path)
+ return path
+
+
+def run(string, timeout=config.runTimeout, interval=config.runInterval, desktop=None, dumb=False, appName=''):
+ """
+ Runs an application. [For simple command execution such as 'rm *', use os.popen() or os.system()]
+ If dumb is omitted or is False, polls at interval seconds until the application is finished starting, or until timeout is reached.
+ If dumb is True, returns when timeout is reached.
+ """
+ if not desktop:
+ from tree import root as desktop
+ args = shlex.split(string)
+ os.environ['GTK_MODULES'] = 'gail:atk-bridge'
+ pid = subprocess.Popen(args, env=os.environ).pid
+
+ if not appName:
+ appName = args[0]
+
+ if dumb:
+ # We're starting a non-AT-SPI-aware application. Disable startup
+ # detection.
+ doDelay(timeout)
+ else:
+ # Startup detection code
+ # The timing here is not totally precise, but it's good enough for now.
+ time = 0
+ while time < timeout:
+ time = time + interval
+ try:
+ for child in desktop.children[::-1]:
+ if child.name == appName:
+ for grandchild in child.children:
+ if grandchild.roleName == 'frame':
+ from procedural import focus
+ focus.application.node = child
+ doDelay(interval)
+ return pid
+ except AttributeError: # pragma: no cover
+ pass
+ doDelay(interval)
+ return pid
+
+
+def doDelay(delay=None):
+ """
+ Utility function to insert a delay (with logging and a configurable
+ default delay)
+ """
+ if delay is None:
+ delay = config.defaultDelay
+ if config.debugSleep:
+ logger.log("sleeping for %f" % delay)
+ sleep(delay)
+
+
+class Highlight (Gtk.Window): # pragma: no cover
+
+ def __init__(self, x, y, w, h): # pragma: no cover
+ super(Highlight, self).__init__()
+ self.set_decorated(False)
+ self.set_has_resize_grip(False)
+ self.set_default_size(w, h)
+ self.screen = self.get_screen()
+ self.visual = self.screen.get_rgba_visual()
+ if self.visual is not None and self.screen.is_composited():
+ self.set_visual(self.visual)
+ self.set_app_paintable(True)
+ self.connect("draw", self.area_draw)
+ self.show_all()
+ self.move(x, y)
+
+ def area_draw(self, widget, cr): # pragma: no cover
+ cr.set_source_rgba(.0, .0, .0, 0.0)
+ cr.set_operator(cairo.OPERATOR_SOURCE)
+ cr.paint()
+ cr.set_operator(cairo.OPERATOR_OVER)
+ cr.set_source_rgb(0.9, 0.1, 0.1)
+ cr.set_line_width(6)
+ cr.rectangle(0, 0, self.get_size()[0], self.get_size()[1])
+ cr.stroke()
+
+
+class Blinker(object): # pragma: no cover
+ INTERVAL_MS = 1000
+ main_loop = GLib.MainLoop()
+
+ def __init__(self, x, y, w, h): # pragma: no cover
+ self.highlight_window = Highlight(x, y, w, h)
+ if self.highlight_window.screen.is_composited() is not False:
+ self.timeout_handler_id = GLib.timeout_add(
+ Blinker.INTERVAL_MS, self.destroyHighlight)
+ self.main_loop.run()
+ else:
+ self.highlight_window.destroy()
+
+ def destroyHighlight(self): # pragma: no cover
+ self.highlight_window.destroy()
+ self.main_loop.quit()
+ return False
+
+
+class Lock(object):
+
+ """
+ A mutex implementation that uses atomicity of the mkdir operation in UNIX-like
+ systems. This can be used by scripts to provide for mutual exlusion, either in single
+ scripts using threads etc. or i.e. to handle sitations of possible collisions among
+ multiple running scripts. You can choose to make randomized single-script wise locks
+ or a more general locks if you do not choose to randomize the lockdir name
+ """
+
+ def __init__(self, location='/tmp', lockname='dogtail_lockdir_', randomize=True):
+ """
+ You can change the default lockdir location or name. Setting randomize to
+ False will result in no random string being appened to the lockdir name.
+ """
+ self.lockdir = os.path.join(os.path.normpath(location), lockname)
+ if randomize:
+ self.lockdir = "%s%s" % (self.lockdir, self.__getPostfix())
+
+ def lock(self):
+ """
+ Creates a lockdir based on the settings on Lock() instance creation.
+ Raises OSError exception of the lock is already present. Should be
+ atomic on POSIX compliant systems.
+ """
+ locked_msg = 'Dogtail lock: Already locked with the same lock'
+ if not os.path.exists(self.lockdir):
+ try:
+ os.mkdir(self.lockdir)
+ return self.lockdir
+ except OSError as e:
+ if e.errno == errno.EEXIST and os.path.isdir(self.lockdir):
+ raise OSError(locked_msg)
+ else:
+ raise OSError(locked_msg)
+
+ def unlock(self):
+ """
+ Removes a lock. Will raise OSError exception if the lock was not present.
+ Should be atomic on POSIX compliant systems.
+ """
+ import os # have to import here for situations when executed from __del__
+ if os.path.exists(self.lockdir):
+ try:
+ os.rmdir(self.lockdir)
+ except OSError as e:
+ if e.erron == errno.EEXIST:
+ raise OSError('Dogtail unlock: lockdir removed elsewhere!')
+ else:
+ raise OSError('Dogtail unlock: not locked')
+
+ def __del__(self):
+ """
+ Makes sure lock is removed when the process ends. Although not when killed indeed.
+ """
+ self.unlock()
+
+ def __getPostfix(self):
+ import random
+ import string
+ return ''.join(random.choice(string.letters + string.digits) for x in range(5))
+
+
+a11yDConfKey = 'org.gnome.desktop.interface'
+
+
+def isA11yEnabled():
+ """
+ Checks if accessibility is enabled via DConf.
+ """
+ from gi.repository.Gio import Settings
+ InterfaceSettings = Settings(a11yDConfKey)
+ dconfEnabled = InterfaceSettings.get_boolean('toolkit-accessibility')
+ if os.environ.get('GTK_MODULES', '').find('gail:atk-bridge') == -1:
+ envEnabled = False
+ else:
+ envEnabled = True # pragma: no cover
+ return (dconfEnabled or envEnabled)
+
+
+def bailBecauseA11yIsDisabled():
+ if sys.argv[0].endswith("pydoc"):
+ return # pragma: no cover
+ try:
+ if file("/proc/%s/cmdline" % os.getpid()).read().find('epydoc') != -1:
+ return # pragma: no cover
+ except: # pragma: no cover
+ pass # pragma: no cover
+ logger.log("Dogtail requires that Assistive Technology support be enabled."
+ "\nYou can enable accessibility with sniff or by running:\n"
+ "'gsettings set org.gnome.desktop.interface toolkit-accessibility true'\nAborting...")
+ sys.exit(1)
+
+
+def enableA11y(enable=True):
+ """
+ Enables accessibility via DConf.
+ """
+ from gi.repository.Gio import Settings
+ InterfaceSettings = Settings(schema=a11yDConfKey)
+ InterfaceSettings.set_boolean('toolkit-accessibility', enable)
+
+
+def checkForA11y():
+ """
+ Checks if accessibility is enabled, and halts execution if it is not.
+ """
+ if not isA11yEnabled(): # pragma: no cover
+ bailBecauseA11yIsDisabled()
+
+
+def checkForA11yInteractively(): # pragma: no cover
+ """
+ Checks if accessibility is enabled, and presents a dialog prompting the
+ user if it should be enabled if it is not already, then halts execution.
+ """
+ if isA11yEnabled():
+ return
+ from gi.repository import Gtk
+ dialog = Gtk.Dialog('Enable Assistive Technology Support?',
+ None,
+ Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
+ (Gtk.STOCK_QUIT, Gtk.ResponseType.CLOSE,
+ "_Enable", Gtk.ResponseType.ACCEPT))
+ question = """Dogtail requires that Assistive Technology Support be enabled for it to function. Would you like to enable Assistive Technology support now?
+
+Note that you will have to log out for the change to fully take effect.
+ """.strip()
+ dialog.set_default_response(Gtk.ResponseType.ACCEPT)
+ questionLabel = Gtk.Label(label=question)
+ questionLabel.set_line_wrap(True)
+ dialog.vbox.pack_start(questionLabel, True, True, 0)
+ dialog.show_all()
+ result = dialog.run()
+ if result == Gtk.ResponseType.ACCEPT:
+ logger.log("Enabling accessibility...")
+ enableA11y()
+ elif result == Gtk.ResponseType.CLOSE:
+ bailBecauseA11yIsDisabled()
+ dialog.destroy()
+
+
+class GnomeShell(object): # pragma: no cover
+
+ """
+ Utility class to help working with certain atributes of gnome-shell.
+ Currently that means handling the Application menu available for apps
+ on the top gnome-shell panel. Searching for the menu and its items is
+ somewhat tricky due to fuzzy a11y tree of gnome-shell, mainly since the
+ actual menu is not present as child to the menu-spawning button. Also,
+ the menus get constructed/destroyed on the fly with application focus
+ changes. Thus current application name as displayed plus a reference
+ known menu item (with 'Quit' as default) are required by these methods.
+ """
+
+ def __init__(self, classic_mode=False):
+ from tree import root
+ self.shell = root.application('gnome-shell')
+
+ def getApplicationMenuList(self, search_by_item='Quit'):
+ """
+ Returns list of all menu item nodes. Searches for the menu by a reference item.
+ Provide a different item name, if the 'Quit' is not present - but beware picking one
+ present elsewhere, like 'Lock' or 'Power Off' present under the user menu.
+ """
+ matches = self.shell.findChildren(
+ predicate.GenericPredicate(name=search_by_item, roleName='label'))
+ for match in matches:
+ ancestor = match.parent.parent.parent
+ if ancestor.roleName == 'panel':
+ return ancestor.findChildren(predicate.GenericPredicate(roleName='label'))
+ from tree import SearchError
+ raise SearchError("Could not find the Application menu based on '%s' item. Please provide an existing reference item"
+ % search_by_item)
+
+ def getApplicationMenuButton(self, app_name):
+ """
+ Returns the application menu 'button' node as present on the gnome-shell top panel.
+ """
+ try:
+ return self.shell[0][0][3].child(app_name, roleName='label')
+ except:
+ from tree import SearchError
+ raise SearchError(
+ "Application menu button of %s could not be found within gnome-shell!" % app_name)
+
+ def getApplicationMenuItem(self, item, search_by_item='Quit'):
+ """
+ Returns a particilar menu item node. Uses a different 'Quit' or custom item name for reference, but also
+ attempts to use the given item if the general reference fails.
+ """
+ try:
+ menu_items = self.getApplicationMenuList(search_by_item)
+ except:
+ menu_items = self.getApplicationMenuList(item)
+ for node in menu_items:
+ if node.name == item:
+ return node
+ raise Exception(
+ 'Could not find the item, did application focus change?')
+
+ def clickApplicationMenuItem(self, app_name, item, search_by_item='Quit'):
+ """
+ Executes the given menu item through opening the menu first followed
+ by a click at the particular item. The menu search reference 'Quit'
+ may be customized. Also attempts to use the given item for reference
+ if search fails with the default/custom one.
+ """
+ self.getApplicationMenuButton(app_name).click()
+ self.getApplicationMenuItem(item, search_by_item).click()
diff --git a/build/lib/dogtail/version.py b/build/lib/dogtail/version.py
new file mode 100644
index 0000000000..4d44080615
--- /dev/null
+++ b/build/lib/dogtail/version.py
@@ -0,0 +1,56 @@
+"""Handles versioning of software packages
+
+Author: Dave Malcolm <dmalcolm@redhat.com>"""
+__author__ = 'Dave Malcolm <dmalcolm@redhat.com>'
+
+
+class Version(object):
+
+ """
+ Class representing a version of a software package.
+ Stored internally as a list of subversions, from major to minor.
+ Overloaded comparison operators ought to work sanely.
+ """
+
+ def __init__(self, versionList):
+ self.versionList = versionList
+
+ def fromString(versionString):
+ """
+ Parse a string of the form number.number.number
+ """
+ return Version(map(int, versionString.split(".")))
+ fromString = staticmethod(fromString)
+
+ def __str__(self):
+ return ".".join(map(str, self.versionList))
+
+ def __getNum(self):
+ tmpList = list(self.versionList)
+
+ while len(tmpList) < 5:
+ tmpList += [0]
+
+ num = 0
+ for i in range(len(tmpList)):
+ num *= 1000
+ num += tmpList[i]
+ return num
+
+ def __lt__(self, other):
+ return self.__getNum() < other.__getNum()
+
+ def __le__(self, other):
+ return self.__getNum() <= other.__getNum()
+
+ def __eq__(self, other):
+ return self.__getNum() == other.__getNum()
+
+ def __ne__(self, other):
+ return self.__getNum() != other.__getNum()
+
+ def __gt__(self, other):
+ return self.__getNum() > other.__getNum()
+
+ def __ge__(self, other):
+ return self.__getNum() >= other.__getNum()
diff --git a/build/lib/dogtail/wrapped.py b/build/lib/dogtail/wrapped.py
new file mode 100644
index 0000000000..30c0570fa6
--- /dev/null
+++ b/build/lib/dogtail/wrapped.py
@@ -0,0 +1,33 @@
+"""
+Superclasses for application wrappers
+
+Subclass these classes if you want to create application wrappers, e.g.:
+http://svn.gnome.org/viewvc/dogtail-tests/trunk/appwrappers/dogtail/appwrappers/gedit.py?view=markup
+"""
+__author__ = "Zack Cerza <zcerza@redhat.com>"
+import Accessibility
+
+
+def makeWrapperClass(wrappedClass, name): # pragma: no cover
+ class klass(object):
+
+ def __init__(self, obj):
+ self.obj = obj
+
+ def __getattr__(self, name):
+ if name == 'obj':
+ return self.__dict__['obj']
+ return getattr(self.obj, name)
+
+ def __setattr__(self, name, value):
+ if name == 'obj':
+ self.__dict__['obj'] = value
+ else:
+ return setattr(self.obj, name, value)
+
+ klass.__name__ = name
+ return klass
+
+Application = makeWrapperClass(Accessibility.Application,
+ "WrappedApplication")
+Node = makeWrapperClass(Accessibility.Accessible, "WrappedNode")
diff --git a/build/scripts-2.7/dogtail-detect-session b/build/scripts-2.7/dogtail-detect-session
new file mode 100755
index 0000000000..79bb617020
--- /dev/null
+++ b/build/scripts-2.7/dogtail-detect-session
@@ -0,0 +1,66 @@
+#!/usr/bin/python
+"""
+dogtail-detect session
+
+This script checks for some main pieces of a running GNOME session,
+specifically gnome-panel and metacity.
+
+It checks to see that the gnome-panel node has at least some child nodes.
+For example, the main gnome-panel node by default has two direct descendents:
+the top panel, and the bottom panel.
+Metacity's existence is also checked. However, metacity currently never has
+any child nodes.
+
+If a proper session is running, the scripts exits with a status of 0.
+If no session is found, a non-zero exit code is returned.
+
+Author: Zack Cerza <zcerza@redhat.com>
+"""
+
+__author__ = "Zack Cerza <zcerza@redhat.com>"
+
+from dogtail.procedural import *
+import sys
+
+
+def GNOME():
+ """
+ "Is an accessibility-enabled GNOME session running?"
+ """
+ running = False
+ try:
+ assert focus.desktop
+ assert focus.desktop.children
+
+ focus.application('gnome-panel')
+ assert focus.application.children
+
+ focus.application('metacity')
+ print focus.application.node
+ assert focus.application.node
+ running = True
+ print "GNOME Session detected."
+ except AttributeError or AssertionError or FocusError:
+ print "ERROR: No session found."
+ return running
+
+
+def KDE():
+ """
+ "Is an accessibility-enabled KDE session running?"
+ """
+ running = False
+ return running
+
+
+def JustSomeApps():
+ """
+ "Is at least one accessibility-enabled application running?"
+ """
+ assert focus.desktop
+ assert focus.desktop.children
+
+if GNOME() or KDE() or JustSomeApps():
+ sys.exit()
+else:
+ sys.exit(1)
diff --git a/build/scripts-2.7/dogtail-logout b/build/scripts-2.7/dogtail-logout
new file mode 100755
index 0000000000..c76a4519e0
--- /dev/null
+++ b/build/scripts-2.7/dogtail-logout
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+# Logs out the full gnome session. Be sure to have your documents saved, as running
+# may cause loosing the changes, or it may halt the logout process.
+
+from dogtail.tree import *
+from dogtail.rawinput import pressKey
+from time import sleep
+import getpass
+
+# A gnome-shell object
+shell = root.application('gnome-shell')
+# Click onto a super menu label that we find under the g-s top panel object.
+# We need these indexes as g-s a11y support is a wee bit messy.
+shell[0][1][2].child(getpass.getuser(), roleName='label').click()
+# We can child this all the way down from the app as there's no other Log
+# Out... label
+shell.child('Log Out...', roleName='label').click()
+# This takes care of the 60 second dialog.
+# Sometimes a dialog warning about unsaved work in gedit etc. pops out, but that has the same
+# push button in which case this will take care of that dialog. If another dialog pops-out
+# in the affected application however, that might put the logout process on hold again. Unfortunatelly
+# we cannot do anything about that with dotail at that point as a11y registry got disabled already
+# by the logout process.
+shell[0][1].child(roleName='dialog', recursive=False).child(
+ 'Log Out', roleName='push button').click()
+
+# Give the session some time to end before we kill it.
+sleep(10)
diff --git a/build/scripts-2.7/dogtail-run-headless b/build/scripts-2.7/dogtail-run-headless
new file mode 100755
index 0000000000..edab53ddee
--- /dev/null
+++ b/build/scripts-2.7/dogtail-run-headless
@@ -0,0 +1,79 @@
+#!/usr/bin/python
+"""
+dogtail-run-headless
+
+This script runs a session within an X server, allowing dogtail scripts to be
+run on systems with no graphic cards, among other things.
+
+Scripts are run in the current directory. After they are finished, dogtail can
+optionally log out of the session, which will also termninate the X server.
+"""
+
+import optparse
+from dogtail import sessions
+import sys
+import os.path
+
+
+def findXServers(path="/usr/bin"):
+ l = [os.path.join(path, f) for f in os.listdir(path) if f[0] == 'X']
+ s = set(os.path.realpath(p) for p in l)
+ return list(s)
+
+
+def parse():
+ yesno = ('y', 'n')
+ sessions = ("GNOME", "KDE")
+ usage = "usage: %prog: [options] {script [args]}"
+ parser = optparse.OptionParser(usage=usage)
+
+ parser.add_option("-s", "--session", type="choice",
+ dest="session",
+ choices=sessions,
+ help="which session to use")
+ parser.add_option("-x", "--x-server", type="choice",
+ dest="xserver",
+ choices=findXServers(), help="which X server to use")
+ parser.add_option("-l", "--logout", type="choice",
+ dest="logout",
+ choices=yesno,
+ help="attempt to log out of the session gracefully after" +
+ "script completion")
+ parser.add_option("-t", "--terminate", type="choice",
+ dest="terminate",
+ choices=yesno,
+ help="after script completion, and after any attempt to log" +
+ "out, terminate the session")
+
+ parser.set_defaults(session=sessions[0], logout='y', terminate='y')
+ options, args = parser.parse_args()
+ if not args:
+ parser.print_usage()
+ sys.exit(1)
+ return options, args
+
+
+def main():
+ options, args = parse()
+ if 'XDG_RUNTIME_DIR' in os.environ:
+ del os.environ['XDG_RUNTIME_DIR']
+ if options.session == "GNOME":
+ session = sessions.Session(sessionBinary='/usr/bin/gnome-session',
+ scriptCmdList=args, scriptDelay=10)
+ if options.session == "KDE":
+ session = sessions.Session(sessionBinary='/usr/bin/startkde',
+ scriptCmdList=args, scriptDelay=25)
+ if options.xserver:
+ session.xserver.server = options.xserver
+ pid = session.start()
+ scriptExitCode = session.script.exitCode
+ if options.logout == 'y':
+ session.attemptLogout()
+ if options.terminate == 'y':
+ session.stop()
+ else:
+ session.wait()
+ sys.exit(scriptExitCode)
+
+if __name__ == "__main__":
+ main()
diff --git a/build/scripts-2.7/dogtail-run-headless-next b/build/scripts-2.7/dogtail-run-headless-next
new file mode 100755
index 0000000000..ae481cf1c4
--- /dev/null
+++ b/build/scripts-2.7/dogtail-run-headless-next
@@ -0,0 +1,319 @@
+#!/usr/bin/python
+descr = """
+Unlike the original headless script this will make use of an Display Manager
+(DM - currently gdm) to handle starting the X server and user session. It's motivated
+by changes related to systemd - that disallows running a gnome session from an
+environment spawned by 'su'. The original headless will not work in these cases
+anymore on systemd systems
+
+Instead this script uses the AutoLogin feature of the DM, so that when it starts DM's
+service the session will login for particular user at once. It then uses the
+environment properties from the new session and runs the target script inthere.
+
+Will work with distros where 'service gdm/kdm start/stop' takes an effect, and quite
+likely only on systemd systems that use systemd-logind service.
+
+Even if you are still able to use dogtail-run-headless in your usecase, you might
+consider switching to this script - as making use of DM is likely to be more reliable
+and less buggy compared to headless itself taking care of everything.
+"""
+
+drop_overview = '''from dogtail.utils import absoluteMotion, keyPress
+absoluteMotion(100,100)
+keyPress('esc')'''
+
+import argparse
+import sys
+import os
+import glob
+import subprocess
+import time
+import ConfigParser
+import shutil
+import re
+from dogtail.sessions import Script
+
+preserve_envs = ['PYTHONPATH', 'TEST']
+
+
+def getSessionEnvironment(sessionBinary):
+
+ def isSessionProcess(fileName):
+ try:
+ if os.path.realpath(fileName + 'exe') != ('/usr/bin/plasma-desktop'
+ if sessionBinary.split('/')[-1] == 'startkde'
+ else sessionBinary):
+ return False
+ except OSError:
+ return False
+ pid = fileName.split('/')[2]
+ if pid == 'self' or pid == str(os.getpid()):
+ return False
+ return True
+
+ def getEnvDict(fileName):
+ try:
+ envString = open(fileName, 'r').read()
+ except IOError:
+ return {}
+ envItems = envString.split('\x00')
+ envDict = {}
+ for item in envItems:
+ if not '=' in item:
+ continue
+ k, v = item.split('=', 1)
+ envDict[k] = v
+ return envDict
+
+ def copyVars(envDict):
+ '''Copy a couple of old variables we want to preserve'''
+ for env in preserve_envs:
+ if os.environ.has_key(env):
+ envDict[env] = os.environ[env]
+ return envDict
+
+ envDict = False
+ for path in glob.glob('/proc/*/'):
+ if not isSessionProcess(path):
+ continue
+ envFile = path + 'environ'
+ envDict = getEnvDict(envFile)
+ if not envDict:
+ raise RuntimeError("Can't find our environment!")
+ return copyVars(envDict)
+
+
+def execCodeWithEnv(code, env=None):
+ with open("/tmp/execcode.dogtail", "w") as text_file:
+ text_file.write(code)
+ subprocess.Popen('python /tmp/execcode.dogtail'.split(),
+ env=(os.environ if env is None else env)).wait()
+
+
+class DisplayManagerSession(object):
+
+ gdm_config = '/etc/gdm/custom.conf'
+ kdm_config = '/etc/kde/kdm/kdmrc'
+ gdm_options = {'section': 'daemon', 'enable':
+ 'AutomaticLoginEnable', 'user': 'AutomaticLogin'}
+ kdm_options = {'section': 'X-:0-Core', 'enable':
+ 'AutoLoginEnable', 'user': 'AutoLoginUser'}
+ scriptDelay = 20
+ user = 'test'
+
+ def isProcessRunning(self, process):
+ '''Gives true if process can be greped out of full ps dump '''
+ s = subprocess.Popen(["ps", "axw"], stdout=subprocess.PIPE)
+ for x in s.stdout:
+ if re.search(process, x):
+ return True
+ return False
+
+ def waitForProcess(self, process, invert=False):
+ '''Waits until a process appears'''
+ while self.isProcessRunning(process) is invert:
+ time.sleep(1)
+
+ def __init__(self, dm='gdm', session='gnome', session_binary='gnome-shell', user=None):
+ self.session_binary = session_binary
+ self.session = session
+ self.accountfile = '/var/lib/AccountsService/users/%s' % self.user
+ if user is not None:
+ self.user = user
+ if dm == 'gdm':
+ self.tmp_file = '/tmp/%s' % os.path.basename(self.gdm_config)
+ self.options = self.gdm_options
+ self.config = self.gdm_config
+ elif dm == 'kdm':
+ self.tmp_file = '/tmp/%s' % os.path.basename(self.kdm_config)
+ self.options = self.kdm_options
+ self.config = self.kdm_config
+ self.dm = dm
+
+ def setup(self, restore=False):
+ shutil.copy(self.config, self.tmp_file)
+ config = ConfigParser.SafeConfigParser()
+ config.optionxform = str
+ config.read(self.tmp_file)
+ if not restore:
+ config.set(self.options['section'], self.options['enable'], 'true')
+ config.set(
+ self.options['section'], self.options['user'], self.user)
+ else:
+ config.remove_option(
+ self.options['section'], self.options['enable'])
+ config.remove_option(self.options['section'], self.options['user'])
+ output = open(self.tmp_file, 'w')
+ config.write(output)
+ output.flush()
+ subprocess.Popen('sudo mv -f %s %s' %
+ (self.tmp_file, self.config), shell=True).wait()
+ if not restore:
+ if 'kwin' in self.session_binary:
+ try:
+ os.makedirs(os.getenv('HOME') + '/.kde/env/')
+ except:
+ pass
+ subprocess.Popen(
+ 'echo "export QT_ACCESSIBILITY=1" > ~/.kde/env/qt-at-spi.sh', shell=True).wait()
+ if self.dm == 'gdm':
+ need_restart = False
+ tempfile = '/tmp/%s_headless' % self.user
+ subprocess.Popen('cp -f %s %s' % (self.accountfile, tempfile), shell=True).wait()
+ account_config = ConfigParser.SafeConfigParser()
+ account_config.optionxform = str
+ account_config.read(tempfile)
+ try:
+ saved_session = account_config.get('User', 'XSession')
+ if self.session is None:
+ if 'kde' in saved_session and 'gnome-shell' in self.session_binary:
+ self.session_binary = '/usr/bin/kwin'
+ elif 'gnome' in saved_session and 'kwin' in self.session_binary:
+ self.session_binary = '/usr/bin/gnome-shell'
+ elif saved_session != self.session:
+ account_config.set('User', 'XSession', self.session)
+ need_restart = True
+ except ConfigParser.NoSectionError:
+ account_config.add_section('User')
+ account_config.set('User', 'XSession', self.session if self.session else '')
+ account_config.set('User', 'SystemAccount', 'false')
+ need_restart = True
+ if need_restart:
+ output = open(tempfile, 'w')
+ account_config.write(output)
+ output.flush()
+ subprocess.Popen('sudo mv -f %s %s' % (tempfile, self.accountfile), shell=True).wait()
+ time.sleep(1)
+ os.system('sudo service accounts-daemon restart')
+ time.sleep(6) # prevent a possible race condition
+ os.system('sudo service systemd-logind restart')
+ time.sleep(6) # these are fast, but we still need to make sure no races happen
+ else:
+ subprocess.Popen('sudo rm -f %s' % tempfile, shell=True).wait()
+ elif self.dm == 'kdm':
+ if self.session is not None:
+ subprocess.Popen(
+ 'echo "[Desktop]\nSession=%s" > /home/%s/.dmrc' % (self.session, self.user), shell=True).wait()
+
+ def start(self, restart=False):
+ if restart:
+ subprocess.Popen(('sudo service %s stop' % (self.dm)).split())
+ time.sleep(0.5)
+ subprocess.Popen(('sudo service %s start' % (self.dm)).split())
+ self.waitForProcess(self.session_binary.split('/')[-1])
+ # some extra time for an environment (shell/kwin) to load all resources
+ # etc.
+ if self.dm == 'kdm':
+ time.sleep(10) # KDE keeps loading screen on untill all is loaded
+ else:
+ time.sleep(4) # GNOME shows stuff as it appears
+
+ def setA11y(self, enable):
+ subprocess.Popen('/usr/bin/gsettings set org.gnome.desktop.interface toolkit-accessibility %s'
+ % ('true' if enable else 'false'), shell=True, env=os.environ)
+# if enable: # mouse is at 0x0 at the start - which brings in the overview
+# execCodeWithEnv(drop_overview, env = os.environ)
+# time.sleep(2) # time for the overview to go away
+
+ def stop(self):
+ subprocess.Popen(('sudo service %s stop' % (self.dm)).split()).wait()
+ self.waitForProcess('/usr/bin/%s' % self.dm, invert=True)
+ time.sleep(3) # extra safe time
+ # did i.e. gnome-shell get stuck running?
+ if self.isProcessRunning(self.session_binary.split('/')[-1]):
+ print(
+ 'dogtail-run-headless-next: WARNING: %s still running, proceeding with kill -9' %
+ self.session_binary.split('/')[-1])
+ subprocess.Popen(
+ ('sudo pkill --signal 9 %s' % (self.session_binary.split('/')[-1])).split()).wait()
+ time.sleep(1)
+
+
+def parse():
+ parser = argparse.ArgumentParser(
+ prog='$ dogtail-run-headless-next', description=descr, formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument('script', help="""Command to execute the script""")
+ parser.add_argument('--session', required=False,
+ help="""What session to use. Not specifying results in system default session on first login
+ or test user's last session with follow-up logins.
+ Otherwise you can set any xsession desktop file name here (i.e. 'gnome-classic', 'gnome', 'lxde' etc.).
+ 'kde' defaults to 'kde-plasma for legacy reasons.'""")
+ parser.add_argument('--session-binary', required=False,
+ help="""Specify an in-session ever-running binary (full path) to get the environment from. Only needed for non-gnome/kde sessions.""")
+ parser.add_argument('--dm', required=False,
+ help="""What display manager to use for spawning session. Supported are 'gdm' (default) and 'kdm'.""")
+ parser.add_argument('--restart', action='store_true',
+ help="""Restart previously running display manager session before script execution.""")
+ parser.add_argument('--dont-start', action='store_true',
+ help="""Use already running session (doesn't have to be under Display Manager)""")
+ parser.add_argument('--dont-kill', action='store_true',
+ help="""Do not kill session when script exits.""")
+ parser.add_argument('--disable-a11y', action='store_true',
+ help="""Disable accessibility technologies on script(not session) exit.""")
+ return parser.parse_args()
+
+
+def main():
+ args = parse()
+ scriptCmdList = args.script.split()
+
+ if args.session is None or 'gnome' in args.session:
+ if args.session_binary is None:
+ args.session_binary = '/usr/bin/gnome-shell'
+ elif 'kde' in args.session:
+ if args.session_binary is None:
+ args.session_binary = '/usr/bin/kwin'
+ if args.session == 'kde':
+ args.session = 'kde-plasma'
+ else:
+ if args.session_binary is None:
+ print('dogtail-run-headless-next: Need to specify a --session-binary ever-present in the session to get env from.')
+ sys.exit(-1)
+ if args.dm == 'gdm' or args.dm is None:
+ dm_name = 'gdm'
+ elif args.dm == 'kdm':
+ dm_name = 'kdm'
+ else:
+ print('dogtail-run-headless-next: I do not recognize the display manager!')
+ sys.exit(-1)
+
+ print('dogtail-run-headless-next: Using display manager: %s' % dm_name)
+
+ dm = DisplayManagerSession(dm_name, args.session, args.session_binary)
+
+ if args.dont_start is not True:
+ dm.setup()
+ dm.start(restart=args.restart)
+
+ print('dogtail-run-headless-next: Using %s to bind to the session' % dm.session_binary)
+
+ try:
+ os.environ = getSessionEnvironment(dm.session_binary)
+ except:
+ print(
+ 'dogtail-run-headless-next: Could not get the environment from %s process' %
+ dm.session_binary)
+ dm.stop()
+ dm.setup(restore=True)
+ sys.exit(1)
+
+ if dm_name == 'gdm':
+ dm.setA11y(True)
+
+ script = Script(scriptCmdList)
+ scriptPid = script.start()
+ print('dogtail-run-headless-next: Started the script with PID %d' % scriptPid)
+ exitCode = script.wait()
+ print('dogtail-run-headless-next: The script has finnished with return code %d' % exitCode)
+
+ if args.disable_a11y is True:
+ dm.setA11y(False)
+
+ if args.dont_kill is False:
+ dm.stop()
+ dm.setup(restore=True)
+
+ sys.exit(exitCode)
+
+if __name__ == "__main__":
+ main()
diff --git a/build/scripts-2.7/sniff b/build/scripts-2.7/sniff
new file mode 100755
index 0000000000..f55adfa966
--- /dev/null
+++ b/build/scripts-2.7/sniff
@@ -0,0 +1,798 @@
+#!/usr/bin/python
+# -*- coding: UTF8 -*-
+"""
+http://en.wikipedia.org/wiki/Model-view-controller
+
+The SniffApp class sets up all of sniff's widgets.
+
+Data storage is handled by the SniffModel class.
+There is no SniffView class; we just use a GtkTreeView.
+Data display is handled by the SniffController class.
+"""
+import gi
+gi.require_version('Gtk', '3.0')
+gi.require_version('Gdk', '3.0')
+import sys
+from dogtail.config import config
+
+if config.checkForA11y:
+ from dogtail.utils import checkForA11yInteractively
+ checkForA11yInteractively()
+
+config.logDebugToFile = False
+config.childrenLimit = 100000
+
+import pyatspi
+import Accessibility
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository import Gio
+from gi.repository import GdkPixbuf
+from gi.repository import GObject
+from gi.repository import GLib
+
+builder = Gtk.Builder()
+
+
+class SniffApp(object):
+ appName = 'Sniff'
+ appAuthors = ['Zack Cerza <zcerza@redhat.com>',
+ 'David Malcolm <dmalcolm@redhat.com']
+
+ def __init__(self):
+ self.builder = builder
+ import os
+ if os.path.exists('sniff.ui'):
+ self.builder.add_from_file('sniff.ui')
+ else:
+ import os
+ path = os.path.abspath(
+ os.path.join(__file__, os.path.pardir, os.path.pardir))
+ if path is '/': # in case the path is /bin/sniff
+ path = '/usr'
+ self.builder.add_from_file(path +
+ '/share/dogtail/glade/sniff.ui')
+ self.app = self.builder.get_object(self.appName)
+ try:
+ self.app.set_icon_from_file('../icons/dogtail-head.svg')
+ except Exception:
+ import os
+ path = os.path.abspath(
+ os.path.join(__file__, os.path.pardir, os.path.pardir))
+ if path is '/':
+ path = '/usr'
+ self.app.set_icon_from_file(os.path.join(path,
+ 'share/icons/hicolor/scalable/apps/dogtail-head.svg'))
+ self.setUpWidgets()
+ self.connectSignals()
+ self.app.show_all()
+ Gtk.main()
+
+ def setUpWidgets(self):
+ self.quit1 = self.builder.get_object('quit1')
+ self.expand_all1 = self.builder.get_object('expand_all1')
+ self.collapse_all1 = self.builder.get_object('collapse_all1')
+ self.about1 = self.builder.get_object('about1')
+ self.refreshMenuItem = self.builder.get_object('refresh1')
+ self.autoRefreshMenuItem = self.builder.get_object('autorefresh')
+ self.setRootMenuItem = self.builder.get_object('setRootMenuItem')
+ self.unsetRootMenuItem = self.builder.get_object('unsetRootMenuItem')
+ self.about = None
+
+ self.tree = SniffController()
+
+ def connectSignals(self):
+ self.app.connect('delete_event', self.quit, self)
+ self.quit1.connect('activate', self.quit, self)
+ self.expand_all1.connect('activate', self.tree.expandAll, True)
+ self.collapse_all1.connect('activate', self.tree.expandAll, False)
+ self.about1.connect('activate', self.showAbout, self)
+ self.refreshMenuItem.connect('activate', self.tree.refresh)
+ self.autoRefreshMenuItem.connect(
+ 'toggled', self.tree.toggleAutoRefresh)
+ self.setRootMenuItem.connect('activate', self.tree.changeRoot, True)
+ self.unsetRootMenuItem.connect('activate', self.tree.changeRoot, False)
+
+ self.setStartupAutoRefresh()
+
+ def setStartupAutoRefresh(self):
+ import os
+ if not os.path.exists('/tmp/sniff_refresh.lock'):
+ self.autoRefreshMenuItem.set_active(True)
+
+ def showAbout(self, *args):
+ if not self.about:
+ self.about = Gtk.AboutDialog()
+ self.about.set_name(self.appName)
+ self.about.set_authors(self.appAuthors)
+ self.about.set_comments('Explore your desktop with Dogtail')
+ self.about.set_website('http://people.redhat.com/zcerza/dogtail/')
+ self.about.connect("response", self.hideAbout)
+ self.about.show_all()
+
+ def hideAbout(self, window, response):
+ if response == Gtk.ResponseType.CANCEL:
+ window.hide()
+
+ def quit(self, *args):
+ Gtk.main_quit()
+
+
+class SniffController(object):
+ invalidBufferCallbackID = None
+
+ def __init__(self):
+ self.builder = builder
+ self.nameTextLabel = self.builder.get_object('nameTextLabel')
+ self.nameTextLabel.set_text('')
+ self.roleNameTextLabel = self.builder.get_object('roleNameTextLabel')
+ self.roleNameTextLabel.set_text('')
+ self.descTextLabel = self.builder.get_object('descTextLabel')
+ self.descTextLabel.set_text('')
+ self.actionsTextLabel = self.builder.get_object('actionsTextLabel')
+ self.actionsTextLabel.set_text('')
+ self.textTextView = self.builder.get_object('textTextView')
+ self.textTextViewBufferCallbackID = self.invalidBufferCallbackID
+ self.textTextView.set_sensitive(False)
+ self.textTextView.get_buffer().set_text('')
+ self.labelerButton = self.builder.get_object('labelerButton')
+ self.labelerButton.set_sensitive(False)
+ self.labeleeButton = self.builder.get_object('labeleeButton')
+ self.labeleeButton.set_sensitive(False)
+ self.stateView = self.builder.get_object('stateTreeView')
+ self.highlight1 = self.builder.get_object('highlight1')
+ self.autorefresh = self.builder.get_object('autorefresh')
+ self.stateModel = StateModel()
+ self.setUpStateView()
+ self.treeView = self.builder.get_object('treeTreeView')
+ self.treeSelection = self.treeView.get_selection()
+ self.treeModel = SniffModel()
+ self.setUpTreeView()
+ self.connectSignals()
+ self.refresh()
+
+ def setUpStateView(self):
+ self.stateView.set_model(self.stateModel)
+ cellRenderer = Gtk.CellRendererText()
+ col = Gtk.TreeViewColumn('Present States', cellRenderer,
+ text=self.stateModel.stateColumn)
+ col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ self.stateView.insert_column(col, -1)
+
+ def setUpTreeView(self):
+ self.treeView.set_enable_tree_lines(True)
+
+ self.treeView.set_model(self.treeModel)
+
+ col = Gtk.TreeViewColumn()
+ cellRenderer = Gtk.CellRendererPixbuf()
+ col.pack_start(cellRenderer, expand=False)
+ col.add_attribute(cellRenderer, 'pixbuf', self.treeModel.pixbufColumn)
+
+ cellRenderer = Gtk.CellRendererText()
+ col.pack_end(cellRenderer, expand=False)
+ col.add_attribute(cellRenderer, 'text', self.treeModel.nameColumn)
+
+ col.set_title('Name')
+
+ self.treeView.insert_column(col, -1)
+
+ for column in self.treeView.get_columns():
+ column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ column.set_resizable(True)
+ self.treeView.show()
+ path = 0
+ self.treeView.expand_all()
+ #self.rowExpanded(self.treeView, self.treeModel.get_iter(path), path)
+
+ def changeRoot(self, menuItem, toSelected=True, *args):
+ if toSelected:
+ node = self.getSelectedNode()
+ if toSelected and node:
+ self.treeModel.changeRoot(node)
+ elif not toSelected:
+ self.treeModel.reset()
+ else:
+ return
+ self.refresh(refreshModel=False)
+
+ def refresh(self, menuItem=None, refreshModel=True, *args):
+ if refreshModel:
+ self.treeModel.refresh()
+ rootPath = self.treeModel.get_path(self.treeModel.get_iter_first())
+ self.treeView.expand_all()
+ self.treeView.expand_row(rootPath, False)
+
+ def toggleAutoRefresh(self, *args):
+ if self.autorefresh.get_active() is True:
+ pyatspi.Registry.registerEventListener(self.treeModel.nodeChanged,
+ 'object:children-changed')
+ pyatspi.Registry.registerEventListener(self.treeModel.nodeChanged,
+ 'object:property-change:accessible-name')
+ pyatspi.Registry.registerEventListener(self.treeModel.nodeChanged,
+ 'object:property-change:accessible-state')
+ pyatspi.Registry.registerEventListener(self.treeModel.nodeChanged,
+ 'object:state-changed')
+ self.refresh()
+ else:
+ pyatspi.Registry.deregisterEventListener(
+ self.treeModel.nodeChanged,
+ 'object:children-changed')
+ pyatspi.Registry.deregisterEventListener(
+ self.treeModel.nodeChanged,
+ 'object:property-change:accessible-name')
+ pyatspi.Registry.deregisterEventListener(
+ self.treeModel.nodeChanged,
+ 'object:property-change:accessible-state')
+ pyatspi.Registry.deregisterEventListener(
+ self.treeModel.nodeChanged,
+ 'object:state-changed')
+
+ def connectSignals(self):
+ self.labelerButton.connect('clicked', self.showRelationTarget,
+ 'labeler')
+ self.labeleeButton.connect('clicked', self.showRelationTarget,
+ 'labelee')
+ self.treeView.connect('button-press-event', self.buttonPress)
+ self.treeView.connect('key-press-event', self.keyPress)
+ self.treeView.connect('row-expanded', self.rowExpanded, self.treeModel)
+ self.treeView.connect('row-collapsed', self.rowCollapsed)
+ self.treeSelection.connect('changed', self.selectionChanged)
+ self.refresh()
+
+ def selectionChanged(self, treeSelection):
+ node = self.getSelectedNode()
+ if node:
+ self.setUpBottomPane(node)
+ if self.highlight1.get_active() is True:
+ node.blink()
+
+ def getSelectedNode(self):
+ (store, iter) = self.treeView.get_selection().get_selected()
+ if not iter:
+ node = None
+ else:
+ node = self.treeModel.getNode(iter)
+ return node
+
+ def expandAll(self, widget, *args):
+ if args[0] == True:
+ self.treeView.expand_all()
+ elif args[0] == False:
+ self.treeView.collapse_all()
+
+ def rowExpanded(self, treeview, iter, path, *userParams):
+ row = self.treeModel[path]
+ childRows = row.iterchildren()
+ while True:
+ try:
+ childRow = childRows.next()
+ self.treeModel.populateChildren(childRow.iter)
+ except StopIteration:
+ break
+
+ def rowCollapsed(self, treeview, iter, path, *userParams):
+ row = self.treeModel[path]
+ childRows = row.iterchildren()
+ try:
+ while True:
+ childRow = childRows.next()
+ grandChildRows = childRow.iterchildren()
+ try:
+ while True:
+ grandChildRow = grandChildRows.next()
+ self.treeModel.remove(grandChildRow.iter)
+ except StopIteration:
+ pass
+ except StopIteration:
+ pass
+
+ def menuItemActivate(self, menuItem, *userParams):
+ if len(userParams) < 2:
+ return
+ method = userParams[0]
+ arg = userParams[1]
+ method(arg)
+
+ def keyPress(self, widget, event, *userParams):
+ if event.keyval == Gdk.KEY_Return:
+ path = self.treeSelection.get_selected_rows()[1][0]
+ if self.treeView.row_expanded(path):
+ self.treeView.collapse_row(path)
+ else:
+ self.treeView.expand_row(path, False)
+ return False
+
+ def buttonPress(self, widget, event, *userParams):
+ try:
+ path, treeViewCol, relX, relY = \
+ self.treeView.get_path_at_pos(int(event.x),
+ int(event.y))
+ except TypeError:
+ return
+ node = self.treeModel.getNode(self.treeModel.get_iter(path))
+ if node == None:
+ return
+
+ if event.button == 3:
+ self.menu = Gtk.Menu()
+ menuItem = None
+ if node.actions:
+ for action in node.actions.keys():
+ menuItem = Gtk.MenuItem(action.capitalize())
+ menuItem.connect(
+ 'activate', self.menuItemActivate, node.doActionNamed, action)
+ menuItem.show()
+ self.menu.append(menuItem)
+ if not menuItem:
+ return
+ self.menu.show_all()
+ self.menu.popup(None, None, None, None, event.button, event.time)
+
+ def showRelationTarget(self, button, relation, *args):
+ target = getattr(self.getSelectedNode(), relation)
+ if not target:
+ return
+ try:
+ target.blink()
+ except:
+ import traceback
+ traceback.print_exc()
+
+ def setUpBottomPane(self, node):
+ """Generic code for setting up the table under the TreeView"""
+ if node == None:
+ return
+ self.nameTextLabel.set_text(node.name)
+ self.roleNameTextLabel.set_text(node.roleName)
+ self.descTextLabel.set_text(node.description)
+ str = ''
+ if node.actions:
+ str = ' '.join(node.actions.keys())
+ self.actionsTextLabel.set_text(str)
+
+ # Have we connected this signal yet?
+ # If so, disconnect it before proceeding.
+ if self.textTextViewBufferCallbackID != self.invalidBufferCallbackID:
+ self.textTextView.get_buffer().disconnect(
+ self.textTextViewBufferCallbackID)
+ self.textTextViewBufferCallbackID = self.invalidBufferCallbackID
+
+ if node.text is not None:
+ buffer = self.textTextView.get_buffer()
+ buffer.set_text(node.text)
+ try:
+ node.queryEditableText()
+ # Remember the handler ID of this connection.
+ self.textTextView.set_sensitive(True)
+ self.textTextViewBufferCallbackID = \
+ buffer.connect('changed', self.changeText, node)
+ except NotImplementedError:
+ self.textTextView.set_sensitive(False)
+ else:
+ self.textTextView.get_buffer().set_text('')
+ self.textTextView.set_sensitive(False)
+
+ if node.labeler and not node.labeler.dead:
+ self.labelerButton.set_sensitive(True)
+ self.labelerButton.show()
+ # elif node.labeler and node.labeler.dead:
+ # print "labeler is dead", node.labeler
+ else:
+ self.labelerButton.set_sensitive(False)
+ self.labelerButton.hide()
+ if node.labelee and not node.labelee.dead:
+ self.labeleeButton.set_sensitive(True)
+ self.labeleeButton.show()
+ # elif node.labelee and node.labelee.dead:
+ # print "labelee is dead", node.labelee
+ else:
+ self.labeleeButton.set_sensitive(False)
+ self.labeleeButton.hide()
+
+ self.stateModel.setNode(node)
+
+ def changeText(self, textBuffer, node):
+ if node == None:
+ return
+ node.text = textBuffer.get_text(textBuffer.get_start_iter(),
+ textBuffer.get_end_iter())
+
+
+class SniffModel(Gtk.TreeStore):
+ nodeColumn = 0
+ nameColumn = 1
+ pixbufColumn = 2
+ eventQueue = []
+ cache = {}
+
+ def __init__(self):
+ self.builder = builder
+ #self.autorefresh = self.builder.get_object('autorefresh')
+ Gtk.TreeStore.__init__(self, GObject.TYPE_PYOBJECT,
+ GObject.TYPE_STRING, GdkPixbuf.Pixbuf)
+ root = pyatspi.Registry.getDesktop(0)
+ self.rootNode = self.initialRootNode = root
+ self.appendAndPopulate(None, self.rootNode)
+
+ def __contains__(self, item):
+ from dogtail.tree import Node
+ if isinstance(item, Node):
+ if item in self.cache:
+ row = self.cache[item]
+ # If row is None, we need to call getPath() to be sure
+ if not row:
+ path = self.getPath(item)
+ return path is not None
+ elif row in self:
+ return True
+ return False
+ elif isinstance(item, Gtk.TreeIter):
+ return self.iter_is_valid(item)
+ elif isinstance(item, list) or isinstance(item, tuple):
+ try:
+ iter = self.get_iter(item)
+ except ValueError:
+ return False
+ return iter in self
+ elif isinstance(item, Gtk.TreeRowReference):
+ return item.valid() and item.get_path() in self
+ else:
+ raise TypeError
+
+ def changeRoot(self, node):
+ self.rootNode = node
+ self.refresh()
+
+ def reset(self):
+ self.rootNode = self.initialRootNode
+ self.refresh()
+
+ def refresh(self):
+ self.cache.clear()
+ self.clear()
+ self.appendAndPopulate(None, self.rootNode)
+
+ def append(self, parentIter, node):
+ if node:
+ self.cache[node] = None
+ pb = self.getPixbufForNode(node)
+ return Gtk.TreeStore.append(self, parentIter, (node, node.name, pb))
+
+ def remove(self, iter):
+ node = self.getNode(iter)
+ try:
+ del self.cache[node]
+ finally:
+ return Gtk.TreeStore.remove(self, iter)
+
+ def populateChildren(self, iter):
+ if not iter in self:
+ return False
+ result = True
+ node = self.getNode(iter)
+ try:
+ for child in node.children:
+ if child in self:
+ continue
+ result = result and self.append(iter, child)
+ except GLib.GError as e:
+ if 'name :1.0 was not provided' in e.message:
+ print("Dogtail: warning: omiting possibly broken at-spi application record")
+ return result
+
+ def appendAndPopulate(self, iter, node):
+ childIter = self.append(iter, node)
+ return self.populateChildren(childIter)
+
+ def getNode(self, iter):
+ if not iter in self:
+ return None
+ return self.get_value(iter, self.nodeColumn)
+
+ def getPath(self, node):
+ if not node:
+ raise ValueError
+ try:
+ indexInParent = node.indexInParent
+ except LookupError:
+ return None
+ root = pyatspi.Registry.getDesktop(0)
+ row = self.cache.get(node, None)
+ path = []
+ needParent = True
+ if row:
+ if row in self:
+ path = row.get_path()
+ else:
+ del self.cache[node]
+ elif node == self.rootNode:
+ indexInParent = 0
+ needParent = False
+ elif node.role == pyatspi.ROLE_APPLICATION or node.roleName == \
+ 'application':
+ path = [0]
+ indexInParent = list(root.children).index(node)
+ needParent = False
+ elif not node.parent:
+ return None
+ elif (0 <= indexInParent <= (len(node.parent) - 1)) and \
+ node.parent[indexInParent] != node:
+ return None
+ siblings = node.parent.children
+ sibIndex = siblings.index(node)
+ try:
+ if siblings[sibIndex] != node:
+ return None
+ else:
+ indexInParent = sibIndex
+ except ValueError:
+ return None
+ if type(path) == list:
+ if needParent:
+ parentPath = self.getPath(node.parent)
+ if parentPath is None:
+ return None
+ else:
+ path = list(parentPath)
+ path.append(indexInParent)
+
+ path = tuple(path)
+ try:
+ nodeByPath = self.getNode(self.get_iter(path))
+ if node != nodeByPath:
+ # print "%s is not %s!" % (node, nodeByPath)
+ return None
+ except ValueError:
+ # print "I smell a bug in %s..." % node.getApplication()
+ return None
+
+ #self.cache[node] = Gtk.TreeRowReference(self, path)
+ return path
+
+ def processEvents(self):
+ if not len(self.eventQueue):
+ return
+ queueCopy = self.eventQueue[:]
+ for event in queueCopy:
+ self.processChangedNode(event)
+ self.eventQueue.remove(event)
+ return False
+
+ def nodeChanged(self, event):
+ # if self.autorefresh.get_active() is False: return
+ node = event.source
+ if not node or not node in self:
+ return
+ app = event.host_application
+ if app and app.name == 'sniff':
+ return
+ self.eventQueue.append(event)
+ GObject.idle_add(self.processEvents)
+
+ def processChangedNode(self, event):
+ node = event.source
+ if not node or not node in self:
+ return
+ path = self.getPath(node)
+ try:
+ iter = self.get_iter(path)
+ except (ValueError, TypeError):
+ return
+ if event.type.major == "property-change":
+ if event.type.minor == "accessible-name":
+ node = self.getNode(iter)
+ self.set_value(iter, self.nameColumn, node.name)
+ elif event.type.minor == "accessible-state":
+ print str(event)
+ elif event.type.major == "state-changed":
+ print str(event)
+ elif event.type.major == "children-changed":
+ if event.type.minor == 'add':
+ for child in node.children:
+ if not child in self:
+ if len(child) > 0:
+ self.appendAndPopulate(iter, child)
+ else:
+ # If it has no children now, give it a sec
+ # to come up with some.
+ GObject.timeout_add(1000,
+ self.__addNodeCB, iter, child)
+ elif event.type.minor == 'remove':
+ self.__removeNodeCB(iter, node, path)
+
+ def __addNodeCB(self, iter, parent):
+ self.appendAndPopulate(iter, parent)
+ return False
+
+ def __removeNodeCB(self, iter, parent, path):
+ childRow = self.iter_children(iter)
+ while childRow is not None:
+ node = self.getNode(childRow)
+ if node is None:
+ break
+ if node and self.getNode(childRow) not in parent:
+ self.remove(childRow)
+ else:
+ childRow = self.iter_next(childRow)
+
+ def __populateCB(self, iter):
+ self.populateChildren(iter)
+ return False
+
+ def getPixbufForNode(self, node):
+ theme = Gtk.IconTheme().get_default()
+ try:
+ if node.role == pyatspi.ROLE_APPLICATION:
+ # FIXME: Use the pixbuf from libwcnk (if available):
+ # wnckApp = Application(node).getWnckApplication()
+ # if wnckApp
+ try:
+ return theme.load_icon(node.name, 24,
+ Gtk.IconLookupFlags.USE_BUILTIN)
+ except GObject.GError:
+ try:
+ return theme.load_icon(node.name.lower(), 24,
+ Gtk.IconLookupFlags.USE_BUILTIN)
+ except GObject.GError:
+ return None
+ elif node.parent:
+ return iconForRole[node.role]
+ else:
+ return theme.load_icon("user-desktop", 24,
+ Gtk.IconLookupFlags.USE_BUILTIN)
+ except Exception:
+ return theme.load_icon("dialog-error", 24,
+ Gtk.IconLookupFlags.USE_BUILTIN)
+
+
+class StateModel(Gtk.ListStore):
+ stateColumn = 0
+ statesSupported = ['checked', 'focusable', 'focused', 'sensitive',
+ 'showing']
+
+ def __init__(self):
+ Gtk.ListStore.__init__(self, GObject.TYPE_STRING)
+
+ def setNode(self, node):
+ self.clear()
+ for stateName in self.statesSupported:
+ if getattr(node, stateName) is True:
+ self.append((stateName.capitalize(),))
+
+
+def loadIcon(iconName):
+ try:
+ pixbuf = GdkPixbuf.Pixbuf.new_from_file('icons/' + iconName)
+ except GObject.GError:
+ import os
+ path = os.path.abspath(
+ os.path.join(__file__, os.path.pardir, os.path.pardir))
+ if path is '/':
+ path = '/usr'
+ iconName = os.path.join(path, 'share/dogtail/icons/', iconName)
+ pixbuf = GdkPixbuf.Pixbuf.new_from_file(iconName)
+ return pixbuf
+
+button_xpm = loadIcon("button.xpm")
+checkbutton_xpm = loadIcon("checkbutton.xpm")
+checkmenuitem_xpm = loadIcon("checkmenuitem.xpm")
+colorselection_xpm = loadIcon("colorselection.xpm")
+combo_xpm = loadIcon("combo.xpm")
+dialog_xpm = loadIcon("dialog.xpm")
+image_xpm = loadIcon("image.xpm")
+label_xpm = loadIcon("label.xpm")
+menubar_xpm = loadIcon("menubar.xpm")
+menuitem_xpm = loadIcon("menuitem.xpm")
+notebook_xpm = loadIcon("notebook.xpm")
+scrolledwindow_xpm = loadIcon("scrolledwindow.xpm")
+spinbutton_xpm = loadIcon("spinbutton.xpm")
+statusbar_xpm = loadIcon("statusbar.xpm")
+table_xpm = loadIcon("table.xpm")
+text_xpm = loadIcon("text.xpm")
+toolbar_xpm = loadIcon("toolbar.xpm")
+tree_xpm = loadIcon("tree.xpm")
+treeitem_xpm = loadIcon("treeitem.xpm")
+unknown_xpm = loadIcon("unknown.xpm")
+viewport_xpm = loadIcon("viewport.xpm")
+vscrollbar_xpm = loadIcon("vscrollbar.xpm")
+vseparator_xpm = loadIcon("vseparator.xpm")
+window_xpm = loadIcon("window.xpm")
+
+iconForRole = {
+ pyatspi.ROLE_INVALID: None,
+ # pyatspi doesn't have the following... not even sure if it exists
+ # anywhere.
+ # atspi.SPI_ROLE_ACCEL_LABEL : label_xpm,
+ pyatspi.ROLE_ALERT: None,
+ pyatspi.ROLE_ANIMATION: None,
+ pyatspi.ROLE_ARROW: None,
+ pyatspi.ROLE_CALENDAR: None,
+ pyatspi.ROLE_CANVAS: None,
+ pyatspi.ROLE_CHECK_BOX: checkbutton_xpm,
+ pyatspi.ROLE_CHECK_MENU_ITEM: checkmenuitem_xpm,
+ pyatspi.ROLE_COLOR_CHOOSER: colorselection_xpm,
+ pyatspi.ROLE_COLUMN_HEADER: None,
+ pyatspi.ROLE_COMBO_BOX: combo_xpm,
+ pyatspi.ROLE_DATE_EDITOR: None,
+ pyatspi.ROLE_DESKTOP_ICON: None,
+ pyatspi.ROLE_DESKTOP_FRAME: None,
+ pyatspi.ROLE_DIAL: None,
+ pyatspi.ROLE_DIALOG: dialog_xpm,
+ pyatspi.ROLE_DIRECTORY_PANE: None,
+ pyatspi.ROLE_DRAWING_AREA: None,
+ pyatspi.ROLE_FILE_CHOOSER: None,
+ pyatspi.ROLE_FILLER: None,
+ pyatspi.ROLE_FONT_CHOOSER: None,
+ pyatspi.ROLE_FRAME: window_xpm,
+ pyatspi.ROLE_GLASS_PANE: None,
+ pyatspi.ROLE_HTML_CONTAINER: None,
+ pyatspi.ROLE_ICON: image_xpm,
+ pyatspi.ROLE_IMAGE: image_xpm,
+ pyatspi.ROLE_INTERNAL_FRAME: None,
+ pyatspi.ROLE_LABEL: label_xpm,
+ pyatspi.ROLE_LAYERED_PANE: viewport_xpm,
+ pyatspi.ROLE_LIST: None,
+ pyatspi.ROLE_LIST_ITEM: None,
+ pyatspi.ROLE_MENU: menuitem_xpm,
+ pyatspi.ROLE_MENU_BAR: menubar_xpm,
+ pyatspi.ROLE_MENU_ITEM: menuitem_xpm,
+ pyatspi.ROLE_OPTION_PANE: None,
+ pyatspi.ROLE_PAGE_TAB: notebook_xpm,
+ pyatspi.ROLE_PAGE_TAB_LIST: notebook_xpm,
+ pyatspi.ROLE_PANEL: viewport_xpm,
+ pyatspi.ROLE_PASSWORD_TEXT: None,
+ pyatspi.ROLE_POPUP_MENU: None,
+ pyatspi.ROLE_PROGRESS_BAR: None,
+ pyatspi.ROLE_PUSH_BUTTON: button_xpm,
+ pyatspi.ROLE_RADIO_BUTTON: None,
+ pyatspi.ROLE_RADIO_MENU_ITEM: None,
+ pyatspi.ROLE_ROOT_PANE: viewport_xpm,
+ pyatspi.ROLE_ROW_HEADER: None,
+ pyatspi.ROLE_SCROLL_BAR: vscrollbar_xpm,
+ pyatspi.ROLE_SCROLL_PANE: scrolledwindow_xpm,
+ pyatspi.ROLE_SEPARATOR: vseparator_xpm,
+ pyatspi.ROLE_SLIDER: None,
+ pyatspi.ROLE_SPIN_BUTTON: spinbutton_xpm,
+ pyatspi.ROLE_SPLIT_PANE: None,
+ pyatspi.ROLE_STATUS_BAR: statusbar_xpm,
+ pyatspi.ROLE_TABLE: table_xpm,
+ pyatspi.ROLE_TABLE_CELL: treeitem_xpm,
+ pyatspi.ROLE_TABLE_COLUMN_HEADER: None,
+ pyatspi.ROLE_TABLE_ROW_HEADER: None,
+ pyatspi.ROLE_TEAROFF_MENU_ITEM: None,
+ pyatspi.ROLE_TERMINAL: None,
+ pyatspi.ROLE_TEXT: text_xpm,
+ pyatspi.ROLE_TOGGLE_BUTTON: None,
+ pyatspi.ROLE_TOOL_BAR: toolbar_xpm,
+ pyatspi.ROLE_TOOL_TIP: None,
+ pyatspi.ROLE_TREE: tree_xpm,
+ pyatspi.ROLE_TREE_TABLE: tree_xpm,
+ pyatspi.ROLE_UNKNOWN: unknown_xpm,
+ pyatspi.ROLE_VIEWPORT: viewport_xpm,
+ pyatspi.ROLE_WINDOW: window_xpm,
+ pyatspi.ROLE_EXTENDED: None,
+ pyatspi.ROLE_HEADER: None,
+ pyatspi.ROLE_FOOTER: None,
+ pyatspi.ROLE_PARAGRAPH: None,
+ pyatspi.ROLE_RULER: None,
+ pyatspi.ROLE_APPLICATION: None,
+ pyatspi.ROLE_AUTOCOMPLETE: None,
+ pyatspi.ROLE_EDITBAR: None,
+ pyatspi.ROLE_EMBEDDED: None,
+ pyatspi.ROLE_LAST_DEFINED: None}
+
+
+def main():
+ from dogtail.utils import Lock
+ # We need this to prohibit sniff making(and removing on exit)
+ # sniff_refresh lock when importing Node
+ sniff_running = Lock(lockname='sniff_running.lock', randomize=False)
+ try:
+ sniff_running.lock()
+ except OSError:
+ pass
+ sniff = SniffApp()
+
+if __name__ == '__main__':
+ main()
diff --git a/dogtail.spec b/dogtail.spec
new file mode 100644
index 0000000000..9a4fc47694
--- /dev/null
+++ b/dogtail.spec
@@ -0,0 +1,213 @@
+Summary: GUI test tool and automation framework
+Name: dogtail
+Version: 0.9.0
+Release: 1%{?dist}
+License: GPLv2
+URL: http://dogtail.fedorahosted.org/
+Source0: http://fedorahosted.org/released/dogtail/%{name}-%{version}.tar.gz
+
+BuildArch: noarch
+
+BuildRequires: python2-devel
+BuildRequires: desktop-file-utils
+
+Requires: pyatspi
+Requires: pygobject3
+Requires: pycairo
+Requires: rpm-python
+Requires: xorg-x11-xinit
+Requires: python-imaging
+
+%description
+GUI test tool and automation framework that uses assistive technologies to
+communicate with desktop applications.
+
+%prep
+%setup -q -n %{name}-%{version}
+
+%build
+python ./setup.py build
+
+%install
+python ./setup.py install -O2 --root=$RPM_BUILD_ROOT --record=%{name}.files
+rm -rf $RPM_BUILD_ROOT/%{_docdir}/dogtail
+rm -f $RPM_BUILD_ROOT/%{python_sitelib}/%{name}-%{version}-py%{python_version}.egg-info
+find examples -type f -exec chmod 0644 \{\} \;
+desktop-file-install $RPM_BUILD_ROOT/%{_datadir}/applications/sniff.desktop \
+ --dir=$RPM_BUILD_ROOT/%{_datadir}/applications \
+
+%post
+/bin/touch --no-create %{_datadir}/icons/hicolor &>/dev/null || :
+
+%postun
+if [ $1 -eq 0 ] ; then
+ /bin/touch --no-create %{_datadir}/icons/hicolor &>/dev/null
+ /usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || :
+fi
+
+%posttrans
+/usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || :
+
+%files
+%{_bindir}/*
+%{python_sitelib}/dogtail/
+%{_datadir}/applications/*
+%{_datadir}/dogtail/
+%{_datadir}/icons/hicolor/*
+%doc COPYING
+%doc README
+%doc NEWS
+%doc examples/
+
+%changelog
+* Wed Apr 16 2014 Vitezslav Humpa <vhumpa@redhat.com> - 0.9.0-1
+- Update to upstream version 0.9.0
+
+* Tue Jul 9 2013 Vitezslav Humpa <vhumpa@redhat.com> - 0.8.2-1
+- Update to upstream version 0.8.2
+
+* Sat Feb 23 2013 Rahul Sundaram <sundaram@fedoraproject.org> - 0.8.1-5
+- drop unversioned obsolete
+
+* Sat Feb 23 2013 Rahul Sundaram <sundaram@fedoraproject.org> - 0.8.1-4
+- remove vendor tag from desktop file. https://fedorahosted.org/fpc/ticket/247
+- clean up spec to follow current guidelines
+
+* Wed Feb 13 2013 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.8.1-3
+- Rebuilt for https://fedoraproject.org/wiki/Fedora_19_Mass_Rebuild
+
+* Wed Jul 18 2012 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.8.0-3
+- Rebuilt for https://fedoraproject.org/wiki/Fedora_18_Mass_Rebuild
+
+* Mon Jun 04 2012 Jaroslav Reznik <jreznik@redhat.com> - 0.8.0-2
+- respin
+
+* Thu May 31 2012 Jaroslav Reznik <jreznik@redhat.com> - 0.8.0-1
+- Update to 0.8.0 Final
+- New upstream release
+
+* Mon Apr 16 2012 Jaroslav Reznik <jreznik@redhat.com> - 0.8.0-0.5.beta5
+- Update to 0.8.0 beta 5
+
+* Mon Apr 02 2012 Jaroslav Reznik <jreznik@redhat.com> - 0.8.0-0.2.beta2
+- Update to 0.8.0 beta 2
+
+* Mon Mar 19 2012 Jaroslav Reznik <jreznik@redhat.com> - 0.8.0-0.1.beta1
+- Update to 0.8.0 beta 1
+
+* Fri Jan 13 2012 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.7.0-4
+- Rebuilt for https://fedoraproject.org/wiki/Fedora_17_Mass_Rebuild
+
+* Tue Feb 08 2011 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.7.0-3
+- Rebuilt for https://fedoraproject.org/wiki/Fedora_15_Mass_Rebuild
+
+* Wed Jul 21 2010 David Malcolm <dmalcolm@redhat.com> - 0.7.0-2
+- Rebuilt for https://fedoraproject.org/wiki/Features/Python_2.7/MassRebuild
+
+* Thu Oct 08 2009 Zack Cerza <zcerza@redhat.com> - 0.7.0-1
+- New upstream release.
+- Drop Requires on xorg-x11-server-Xvfb.
+- Update URL and Source0.
+- Ship NEWS file.
+
+* Fri Jul 24 2009 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.6.90-4.401
+- Rebuilt for https://fedoraproject.org/wiki/Fedora_12_Mass_Rebuild
+
+* Tue Feb 24 2009 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.6.90-3.401
+- Rebuilt for https://fedoraproject.org/wiki/Fedora_11_Mass_Rebuild
+
+* Sat Nov 29 2008 Ignacio Vazquez-Abrams <ivazqueznet+rpm@gmail.com> - 0.6.90-2.401
+- Rebuild for Python 2.6
+
+* Tue Aug 12 2008 Zack Cerza <zcerza@redhat.com> - 0.6.90-1.401
+- New upstream snapshot.
+- Require python-imaging
+
+* Tue Aug 12 2008 Zack Cerza <zcerza@redhat.com> - 0.6.90-1.381.2
+- Really fix license tag.
+
+* Wed Jul 16 2008 Tom "spot" Callaway <tcallawa@redhat.com> - 0.6.90-1.381.1
+- fix license tag
+
+* Thu Jan 31 2008 Zack Cerza <zcerza@redhat.com> - 0.6.90-1.381
+- New upstream snapshot.
+- Obsolete pyspi; Require at-spi-python.
+- Require pygtk2-libglade.
+- Don't ship the .egg-info file.
+
+* Wed Jan 3 2007 Zack Cerza <zcerza@redhat.com> - 0.6.1-1
+- New upstream release.
+
+* Thu Dec 7 2006 Jeremy Katz <katzj@redhat.com> - 0.6.0-2
+- build for python 2.5
+- BR python-devel
+
+* Wed Sep 13 2006 Zack Cerza <zcerza@redhat.com> - 0.6.0-1
+- New upstream release.
+- Add Requires for xorg-x11-xinit.
+- Add Requires for gnome-python2-gconf.
+- Bump pyspi Requires.
+- Remove upstreamed patches.
+
+* Fri Aug 18 2006 Zack Cerza <zcerza@redhat.com> - 0.5.2-3
+- Add Requires for xorg-x11-xinit. Closes: #203189.
+
+* Fri Aug 11 2006 Zack Cerza <zcerza@redhat.com> - 0.5.2-2
+- Added headless-gconf.patch to use the python gconf bindings.
+- Added desktop-file-categories.patch to put sniff and dogtail-recorder under
+ the 'Programming' menu.
+
+* Tue Aug 01 2006 Zack Cerza <zcerza@redhat.com> - 0.5.2-1
+- New upstream release.
+- Update Requires from Xvfb to xorg-x11-server-Xvfb.
+- Bump pyspi Requires.
+- Remove ImageMagick Requires.
+- Escape post-macro in changelog-macro.
+
+* Mon Apr 17 2006 Zack Cerza <zcerza@redhat.com> - 0.5.1-3
+- Fix the URL field.
+
+* Tue Mar 21 2006 Zack Cerza <zcerza@redhat.com> - 0.5.1-2
+- Fix URL and Source0 fields.
+- Fix desktop-file-utils magic; use desktop-file-install.
+
+* Fri Feb 24 2006 Zack Cerza <zcerza@redhat.com> - 0.5.1-1
+- Remove BuildRequires on at-spi-devel. Added one on python.
+- Use macros instead of absolute paths.
+- Touch _datadir/icons/hicolor/ before running gtk-update-icon-cache.
+- Require and use desktop-file-utils.
+- postun = post.
+- Shorten BuildArchitectures to BuildArch. The former worked, but even vim's
+ hilighting hated it.
+- Put each *Requires on a separate line.
+- Remove __os_install_post definition.
+- Use Fedora Extras BuildRoot.
+- Instead of _libdir, which kills the build if it's /usr/lib64, use a
+ python macro to define python_sitelib and use that.
+- Remove the executable bit on the examples in install scriptlet.
+- Remove call to /bin/rm in post scriptlet.
+- Use dist in Release.
+
+* Fri Feb 17 2006 Zack Cerza <zcerza@redhat.com> - 0.5.0-2
+- It looks like xorg-x11-Xvfb changed names. Require 'Xvfb' instead.
+- Remove Requires on python-elementtree, since RHEL4 didn't have it. The
+ functionality it provides is probably never used anyway, and will most likely
+ be removed in the future.
+- Don't run gtk-update-icon-cache if it doesn't exist.
+
+* Fri Feb 3 2006 Zack Cerza <zcerza@redhat.com> - 0.5.0-1
+- New upstream release.
+- Added missing BuildRequires on at-spi-devel.
+- Added Requires on pyspi >= 0.5.3.
+- Added Requires on rpm-python, pygtk2, ImageMagick, xorg-x11-Xvfb,
+ python-elementtree.
+- Moved documentation (including examples) to the correct place.
+- Make sure /usr/share/doc/dogtail is removed.
+- Added 'gtk-update-icon-cache' to %%post.
+
+* Mon Oct 24 2005 Zack Cerza <zcerza@redhat.com> - 0.4.3-1
+- New upstream release.
+
+* Sat Oct 8 2005 Jeremy Katz <katzj@redhat.com> - 0.4.2-1
+- Initial build.
+
diff --git a/dogtail/__init__.py b/dogtail/__init__.py
new file mode 100755
index 0000000000..76a4a13fcb
--- /dev/null
+++ b/dogtail/__init__.py
@@ -0,0 +1,16 @@
+# -*- coding: UTF-8 -*-
+"""
+GUI test tool and automation framework that uses Accessibility (a11y) technologies to communicate with desktop applications.
+
+Authors: Zack Cerza <zcerza@redhat.com>, Ed Rousseau <rousseau@redhat.com>, David Malcolm <dmalcolm@redhat.com>, Vita Humpa <vhumpa@redhat.com>
+"""
+
+__author__ = """Zack Cerza <zcerza@redhat.com>,
+Ed Rousseau <rousseau@redhat.com>,
+David Malcolm <dmalcolm@redhat.com>,
+Vita Humpa <vhumpa@redhat.com>"""
+__version__ = "0.9.0"
+__copyright__ = "Copyright © 2005-2014 Red Hat, Inc."
+__license__ = "GPL"
+__all__ = ("config", "predicate",
+ "procedural", "tc", "tree", "utils", "errors")
diff --git a/dogtail/__init__.pyc b/dogtail/__init__.pyc
new file mode 100644
index 0000000000..3c6e83c425
--- /dev/null
+++ b/dogtail/__init__.pyc
Binary files differ
diff --git a/dogtail/config.py b/dogtail/config.py
new file mode 100755
index 0000000000..82e197cf52
--- /dev/null
+++ b/dogtail/config.py
@@ -0,0 +1,219 @@
+"""
+The configuration module.
+"""
+__author__ = "Zack Cerza <zcerza@redhat.com>, David Malcolm <dmalcolm@redhat.com>"
+
+import os
+import sys
+import locale
+
+
+def _userTmpDir(baseName):
+ # i.e. /tmp/dogtail-foo
+ return '-'.join(('/'.join(('/tmp', baseName)), os.environ['USER']))
+
+
+class _Config(object):
+
+ """
+ Contains configuration parameters for the dogtail run.
+
+ scratchDir(str):
+ Directory where things like screenshots are stored.
+
+ dataDir(str):
+ Directory where related data files are located.
+
+ logDir(str):
+ Directory where dogtail.tc.TC*-generated logs are stored.
+
+ scriptName(str) [Read-Only]:
+ The name of the script being run.
+
+ encoding(str)
+ The encoding for text, used by dogtail.tc.TCString .
+
+ actionDelay(float):
+ The delay after an action is executed.
+
+ typingDelay(float):
+ The delay after a character is typed on the keyboard.
+
+ runInterval(float):
+ The interval at which dogtail.utils.run() and dogtail.procedural.run()
+ check to see if the application has started up.
+
+ runTimeout(int):
+ The timeout after which dogtail.utils.run() and dogtail.procedural.run()
+ give up on looking for the newly-started application.
+
+ searchBackoffDuration (float):
+ Time in seconds for which to delay when a search fails.
+
+ searchWarningThreshold (int):
+ Number of retries before logging the individual attempts at a search.
+
+ searchCutoffCount (int):
+ Number of times to retry when a search fails.
+
+ defaultDelay (float):
+ Default time in seconds to sleep when delaying.
+
+ childrenLimit (int):
+ When there are a very large number of children of a node, only return
+ this many, starting with the first.
+
+ debugSearching (boolean):
+ Whether to write info on search backoff and retry to the debug log.
+
+ debugSleep (boolean):
+ Whether to log whenever we sleep to the debug log.
+
+ debugSearchPaths (boolean):
+ Whether we should write out debug info when running the SearchPath
+ routines.
+
+ absoluteNodePaths (boolean):
+ Whether we should identify nodes in the logs with long 'abcolute paths', or
+ merely with a short 'relative path'. FIXME: give examples
+
+ ensureSensitivity (boolean):
+ Should we check that ui nodes are sensitive (not 'greyed out') before
+ performing actions on them? If this is True (the default) it will raise
+ an exception if this happens. Can set to False as a workaround for apps
+ and toolkits that don't report sensitivity properly.
+
+ debugTranslation (boolean):
+ Whether we should write out debug information from the translation/i18n
+ subsystem.
+
+ blinkOnActions (boolean):
+ Whether we should blink a rectangle around a Node when an action is
+ performed on it.
+
+ fatalErrors (boolean):
+ Whether errors encountered in dogtail.procedural should be considered
+ fatal. If True, exceptions will be raised. If False, warnings will be
+ passed to the debug logger.
+
+ checkForA11y (boolean):
+ Whether to check if accessibility is enabled. If not, just assume it is
+ (default True).
+
+ logDebugToFile (boolean):
+ Whether to write debug output to a log file.
+
+ logDebugToStdOut (boolean):
+ Whether to print log output to console or not (default True).
+ """
+ @property
+ def scriptName(self):
+ return os.path.basename(sys.argv[0]).replace('.py', '')
+
+ @property
+ def encoding(self):
+ return locale.getpreferredencoding().lower()
+
+ defaults = {
+ # Storage
+ 'scratchDir': '/'.join((_userTmpDir('dogtail'), '')),
+ 'dataDir': '/'.join((_userTmpDir('dogtail'), 'data', '')),
+ 'logDir': '/'.join((_userTmpDir('dogtail'), 'logs', '')),
+ 'scriptName': scriptName.fget(None),
+ 'encoding': encoding.fget(None),
+ 'configFile': None,
+ 'baseFile': None,
+
+ # Timing and Limits
+ 'actionDelay': 1.0,
+ 'typingDelay': 0.1,
+ 'runInterval': 0.5,
+ 'runTimeout': 30,
+ 'searchBackoffDuration': 0.5,
+ 'searchWarningThreshold': 3,
+ 'searchCutoffCount': 20,
+ 'defaultDelay': 0.5,
+ 'childrenLimit': 100,
+
+ # Debug
+ 'debugSearching': False,
+ 'debugSleep': False,
+ 'debugSearchPaths': False,
+ 'logDebugToStdOut': True,
+ 'absoluteNodePaths': False,
+ 'ensureSensitivity': False,
+ 'debugTranslation': False,
+ 'blinkOnActions': False,
+ 'fatalErrors': False,
+ 'checkForA11y': True,
+
+ # Logging
+ 'logDebugToFile': True
+ }
+
+ options = {}
+
+ invalidValue = "__INVALID__"
+
+ def __init__(self):
+ _Config.__createDir(_Config.defaults['scratchDir'])
+ _Config.__createDir(_Config.defaults['logDir'])
+ _Config.__createDir(_Config.defaults['dataDir'])
+
+ def __setattr__(self, name, value):
+ if name not in config.defaults:
+ raise AttributeError(name + " is not a valid option.")
+
+ elif _Config.defaults[name] != value or \
+ _Config.options.get(name, _Config.invalidValue) != value:
+ if 'Dir' in name:
+ _Config.__createDir(value)
+ if value[-1] != os.path.sep:
+ value = value + os.path.sep
+ elif name == 'logDebugToFile':
+ import logging
+ logging.debugLogger = logging.Logger('debug', value)
+ _Config.options[name] = value
+
+ def __getattr__(self, name):
+ try:
+ return _Config.options[name]
+ except KeyError:
+ try:
+ return _Config.defaults[name]
+ except KeyError:
+ raise AttributeError("%s is not a valid option." % name)
+
+ def __createDir(cls, dirName, perms=0o777):
+ """
+ Creates a directory (if it doesn't currently exist), creating any
+ parent directories it needs.
+
+ If perms is None, create with python's default permissions.
+ """
+ dirName = os.path.abspath(dirName)
+ # print "Checking for %s ..." % dirName,
+ if not os.path.isdir(dirName):
+ if perms:
+ umask = os.umask(0)
+ os.makedirs(dirName, perms)
+ os.umask(umask)
+ else:
+ # This is probably a dead code - no other functions call this without the permissions set
+ os.makedirs(dirName) # pragma: no cover
+ __createDir = classmethod(__createDir)
+
+ def load(self, dict):
+ """
+ Loads values from dict, preserving any options already set that are not overridden.
+ """
+ _Config.options.update(dict)
+
+ def reset(self):
+ """
+ Resets all settings to their defaults.
+ """
+ _Config.options = {}
+
+
+config = _Config()
diff --git a/dogtail/config.pyc b/dogtail/config.pyc
new file mode 100644
index 0000000000..e9c4f40726
--- /dev/null
+++ b/dogtail/config.pyc
Binary files differ
diff --git a/dogtail/distro.py b/dogtail/distro.py
new file mode 100755
index 0000000000..6f70f19465
--- /dev/null
+++ b/dogtail/distro.py
@@ -0,0 +1,362 @@
+"""Handles differences between different distributions
+
+Authors: Dave Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>"""
+__author__ = "Dave Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>"
+
+import os
+import re
+from version import Version
+from logging import debugLogger as logger
+
+
+class DistributionNotSupportedError(Exception): # pragma: no cover
+
+ """
+ This distribution is not supported.
+ """
+ PATCH_MESSAGE = "Please send patches to dogtail-devel-list@gnome.org"
+
+ def __init__(self, distro):
+ self.distro = distro
+
+ def __str__(self):
+ return self.distro + ". " + DistributionNotSupportedError.PATCH_MESSAGE
+
+
+class PackageNotFoundError(Exception):
+
+ """
+ Error finding the requested package.
+ """
+ pass
+
+global packageDb
+global distro
+
+
+class PackageDb(object):
+
+ """
+ Class to abstract the details of whatever software package database is in
+ use (RPM, APT, etc)
+ """
+
+ def __init__(self):
+ self.prefix = '/usr'
+ self.localePrefixes = [self.prefix + '/share/locale']
+
+ def getVersion(self, packageName):
+ """
+ Method to get the version of an installed package as a Version
+ instance (or raise an exception if not found)
+
+ Note: does not know about distributions' internal revision numbers.
+ """
+ raise NotImplementedError
+
+ def getFiles(self, packageName):
+ """
+ Method to get a list of filenames owned by the package, or raise an
+ exception if not found.
+ """
+ raise NotImplementedError
+
+ def getMoFiles(self, locale=None):
+ """
+ Method to get a list of all .mo files on the system, optionally for a
+ specific locale.
+ """
+ moFiles = {}
+
+ def appendIfMoFile(moFiles, dirName, fNames):
+ import re
+ for fName in fNames:
+ if re.match('(.*)\\.mo', fName):
+ moFiles[dirName + '/' + fName] = None
+
+ for localePrefix in self.localePrefixes:
+ if locale:
+ localePrefix = localePrefix + '/' + locale
+ os.path.walk(localePrefix, appendIfMoFile, moFiles)
+
+ return moFiles.keys()
+
+ def getDependencies(self, packageName):
+ """
+ Method to get a list of unique package names that this package
+ is dependent on, or raise an exception if the package is not
+ found.
+ """
+ raise NotImplementedError
+
+
+class _RpmPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+ def getVersion(self, packageName):
+ import rpm
+ ts = rpm.TransactionSet()
+ for header in ts.dbMatch("name", packageName):
+ return Version.fromString(header["version"])
+ raise PackageNotFoundError(packageName)
+
+ def getFiles(self, packageName):
+ import rpm
+ ts = rpm.TransactionSet()
+ for header in ts.dbMatch("name", packageName):
+ return header["filenames"]
+ raise PackageNotFoundError(packageName)
+
+ def getDependencies(self, packageName):
+ import rpm
+ ts = rpm.TransactionSet()
+ for header in ts.dbMatch("name", packageName):
+ # Simulate a set using a hash (to a dummy value);
+ # sets were only added in Python 2.4
+ result = {}
+
+ # Get the list of requirements; these are
+ # sometimes package names, but can also be
+ # so-names of libraries, and invented virtual
+ # ids
+ for requirement in header[rpm.RPMTAG_REQUIRES]:
+ # Get the name of the package providing
+ # this requirement:
+ for depPackageHeader in ts.dbMatch("provides", requirement):
+ depName = depPackageHeader['name']
+ if depName != packageName:
+ # Add to the Hash with a dummy value
+ result[depName] = None
+ return result.keys()
+ raise PackageNotFoundError(packageName)
+
+
+class _AptPackageDb(PackageDb):
+
+ def __init__(self):
+ PackageDb.__init__(self)
+ self.cache = None
+
+ def getVersion(self, packageName):
+ if not self.cache:
+ import apt_pkg
+ apt_pkg.init()
+ self.cache = apt_pkg.GetCache()
+ packages = self.cache.Packages
+ for package in packages:
+ if package.Name == packageName:
+ verString = re.match(
+ '.*Ver:\'(.*)-.*\' Section:', str(package.CurrentVer)).group(1)
+ return Version.fromString(verString)
+ raise PackageNotFoundError(packageName)
+
+ def getFiles(self, packageName):
+ files = []
+ list = os.popen('dpkg -L %s' % packageName).readlines()
+ if not list:
+ raise PackageNotFoundError(packageName)
+ else:
+ for line in list:
+ file = line.strip()
+ if file:
+ files.append(file)
+ return files
+
+ def getDependencies(self, packageName):
+ # Simulate a set using a hash (to a dummy value);
+ # sets were only added in Python 2.4
+ result = {}
+ if not self.cache:
+ import apt_pkg
+ apt_pkg.init()
+ self.cache = apt_pkg.GetCache()
+ packages = self.cache.Packages
+ for package in packages:
+ if package.Name == packageName:
+ current = package.CurrentVer
+ if not current:
+ raise PackageNotFoundError(packageName)
+ depends = current.DependsList
+ list = depends['Depends']
+ for dependency in list:
+ name = dependency[0].TargetPkg.Name
+ # Add to the hash using a dummy value
+ result[name] = None
+ return result.keys()
+
+
+class _UbuntuAptPackageDb(_AptPackageDb):
+
+ def __init__(self):
+ _AptPackageDb.__init__(self)
+ self.localePrefixes.append(self.prefix + '/share/locale-langpack')
+
+
+class _PortagePackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+ def getVersion(self, packageName):
+ # the portage utilities are almost always going to be in
+ # /usr/lib/portage/pym
+ import sys
+ sys.path.append('/usr/lib/portage/pym')
+ import portage
+ # FIXME: this takes the first package returned in the list, in the
+ # case that there are slotted packages, and removes the leading
+ # category such as 'sys-apps'
+ gentooPackageName = portage.db["/"][
+ "vartree"].dbapi.match(packageName)[0].split('/')[1]
+ # this removes the distribution specific versioning returning only the
+ # upstream version
+ upstreamVersion = portage.pkgsplit(gentooPackageName)[1]
+ # print "Version of package is: " + upstreamVersion
+ return Version.fromString(upstreamVersion)
+
+
+class _ConaryPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+ def getVersion(self, packageName):
+ from conaryclient import ConaryClient
+ client = ConaryClient()
+ dbVersions = client.db.getTroveVersionList(packageName)
+ if not len(dbVersions):
+ raise PackageNotFoundError(packageName)
+ return dbVersions[0].trailingRevision().asString().split("-")[0]
+
+# getVersion not implemented because on Solaris multiple modules are installed
+# in single packages, so it is hard to tell what version number of a specific
+# module.
+
+
+class _SolarisPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+
+
+class JhBuildPackageDb(PackageDb): # pragma: no cover
+
+ def __init__(self):
+ PackageDb.__init__(self)
+ prefixes = []
+ prefixes.append(os.environ['LD_LIBRARY_PATH'])
+ prefixes.append(os.environ['XDG_CONFIG_DIRS'])
+ prefixes.append(os.environ['PKG_CONFIG_PATH'])
+ self.prefix = os.path.commonprefix(prefixes)
+ self.localePrefixes.append(self.prefix + '/share/locale')
+
+ def getDependencies(self, packageName):
+ result = {}
+ lines = os.popen('jhbuild list ' + packageName).readlines()
+ for line in lines:
+ if line:
+ result[line.strip()] = None
+ return result.keys()
+
+
+class Distro(object):
+
+ """
+ Class representing a distribution.
+
+ Scripts may want to do arbitrary logic based on whichever distro is in use
+ (e.g. handling differences in names of packages, distribution-specific
+ patches, etc.)
+
+ We can either create methods in the Distro class to handle these, or we
+ can use constructs like isinstance(distro, Ubuntu) to handle this. We can
+ even create hierarchies of distro subclasses to handle this kind of thing
+ (could get messy fast though)
+ """
+
+
+class Fedora(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _RpmPackageDb()
+
+
+class RHEL(Fedora): # pragma: no cover
+ pass
+
+
+class Debian(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _AptPackageDb()
+
+
+class Ubuntu(Debian):
+
+ def __init__(self):
+ self.packageDb = _UbuntuAptPackageDb()
+
+
+class Suse(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _RpmPackageDb()
+
+
+class Gentoo(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _PortagePackageDb()
+
+
+class Conary(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _ConaryPackageDb()
+
+
+class Solaris(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = _SolarisPackageDb()
+
+
+class JHBuild(Distro): # pragma: no cover
+
+ def __init__(self):
+ self.packageDb = JhBuildPackageDb()
+
+
+def detectDistro():
+ logger.log("Detecting distribution:", newline=False)
+
+ if os.environ.get("CERTIFIED_GNOMIE", "no") == "yes":
+ distro = JHBuild() # pragma: no cover
+ elif os.path.exists("/etc/SuSE-release"):
+ distro = Suse() # pragma: no cover
+ elif os.path.exists("/etc/fedora-release"):
+ distro = Fedora() # pragma: no cover
+ elif os.path.exists("/etc/redhat-release"):
+ distro = RHEL() # pragma: no cover
+ elif os.path.exists("/usr/share/doc/ubuntu-minimal"):
+ distro = Ubuntu()
+ elif os.path.exists("/etc/debian_version"): # pragma: no cover
+ distro = Debian() # pragma: no cover
+ elif os.path.exists("/etc/gentoo-release"): # pragma: no cover
+ distro = Gentoo() # pragma: no cover
+ elif os.path.exists("/etc/slackware-version"): # pragma: no cover
+ raise DistributionNotSupportedError("Slackware") # pragma: no cover
+ elif os.path.exists("/var/lib/conarydb/conarydb"): # pragma: no cover
+ distro = Conary() # pragma: no cover
+ elif os.path.exists("/etc/release") and \
+ re.match(".*Solaris", open("/etc/release").readline()): # pragma: no cover
+ distro = Solaris() # pragma: no cover
+ else:
+ raise DistributionNotSupportedError("Unknown") # pragma: no cover
+ logger.log(distro.__class__.__name__)
+ return distro
+
+distro = detectDistro()
+packageDb = distro.packageDb
diff --git a/dogtail/dump.py b/dogtail/dump.py
new file mode 100755
index 0000000000..3756820a51
--- /dev/null
+++ b/dogtail/dump.py
@@ -0,0 +1,33 @@
+"""Utility functions for 'dumping' trees of Node objects.
+
+Author: Zack Cerza <zcerza@redhat.com>"""
+__author__ = "Zack Cerza <zcerza@redhat.com>"
+
+from __builtin__ import file
+
+spacer = ' '
+
+
+def plain(node, fileName=None):
+ """
+ Plain-text dump. The hierarchy is represented through indentation.
+ """
+ def crawl(node, depth):
+ dump(node, depth)
+ for action in node.actions.values():
+ dump(action, depth + 1)
+ for child in node.children:
+ crawl(child, depth + 1)
+
+ def dumpFile(item, depth):
+ _file.write(spacer * depth + str(item) + '\n')
+
+ def dumpStdOut(item, depth):
+ print(spacer * depth + str(item))
+ if fileName:
+ dump = dumpFile
+ _file = file(fileName, 'w')
+ else:
+ dump = dumpStdOut
+
+ crawl(node, 0)
diff --git a/dogtail/errors.py b/dogtail/errors.py
new file mode 100755
index 0000000000..648af9d598
--- /dev/null
+++ b/dogtail/errors.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+"""
+General exceptions; not overly module-specific
+"""
+__author__ = "Zack Cerza <zcerza@redhat.com>"
+from logging import debugLogger as logger
+
+import inspect
+
+
+def warn(message, caller=True):
+ """
+ Generate a warning, and pass it to the debug logger.
+ """
+ frameRec = inspect.stack()[-1]
+ message = "Warning: %s:%s: %s" % (frameRec[1], frameRec[2], message)
+ if caller and frameRec[1] != '<stdin>' and frameRec[1] != '<string>':
+ message = message + ':\n ' + frameRec[4][0]
+ del frameRec
+ logger.log(message)
+
+
+class DependencyNotFoundError(Exception):
+
+ """
+ A dependency was not found.
+ """
+ pass
diff --git a/dogtail/i18n.py b/dogtail/i18n.py
new file mode 100755
index 0000000000..8117f8d82c
--- /dev/null
+++ b/dogtail/i18n.py
@@ -0,0 +1,281 @@
+# -*- coding: utf-8 -*-
+"""
+Internationalization facilities
+
+Authors: David Malcolm <dmalcolm@redhat.com>
+"""
+
+__author__ = """David Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>"""
+
+import config
+
+import os
+import re
+import gettext
+
+from logging import debugLogger as logger
+from __builtin__ import unicode
+
+
+def safeDecode(string):
+ try:
+ string = string.decode('utf-8', 'replace')
+ except UnicodeEncodeError:
+ string = string.encode('utf-8', 'replace')
+ return string
+
+
+def safeEncode(string):
+ pass
+
+
+"""
+Singleton list of TranslationDb instances, to be initialized by the script with
+whatever translation databases it wants.
+"""
+translationDbs = []
+
+
+class TranslationDb(object):
+
+ """
+ Abstract base class representing a database of translations
+ """
+
+ def getTranslationsOf(self, srcName):
+ """
+ Pure virtual method to look up the translation of a string.
+ Returns a list of candidate strings (the translation), empty if not found.
+
+ Note that a source string can map to multiple translated strings. For
+ example, in the French translation of Evolution, the string "Forward" can
+ translate to both
+ (i) "Faire suivre" for forwarding an email, and
+ (ii) "Suivant" for the next page in a wizard.
+ """
+ raise NotImplementedError
+
+
+class GettextTranslationDb(TranslationDb):
+
+ """
+ Implementation of TranslationDb which leverages gettext, using a single
+ translation mo-file.
+ """
+
+ def __init__(self, moFile):
+ self.__moFile = moFile
+ self.__gnutranslations = gettext.GNUTranslations(open(moFile))
+
+ def getTranslationsOf(self, srcName):
+ srcName = safeDecode(srcName)
+ # print "searching for translations of %s"%srcName
+ # Use a dict to get uniqueness:
+ results = {}
+ result = self.__gnutranslations.ugettext(srcName)
+ if result != srcName:
+ results[result] = None
+
+ # Hack alert:
+ #
+ # Note that typical UI definition in GTK etc contains strings with
+ # underscores to denote accelerators.
+ # For example, the stock GTK "Add" item has text "_Add" which e.g.
+ # translates to "A_jouter" in French
+ #
+ # Since these underscores have been stripped out before we see these strings,
+ # we are looking for a translation of "Add" into "Ajouter" in this case, so
+ # we need to fake it, by looking up the string multiple times, with underscores
+ # inserted in all possible positions, stripping underscores out of the result.
+ # Ugly, but it works.
+
+ for index in range(len(srcName)):
+ candidate = srcName[:index] + "_" + srcName[index:]
+ result = self.__gnutranslations.ugettext(candidate)
+ if result != candidate:
+ # Strip out the underscore, and add to the result:
+ results[result.replace('_', '')] = True
+
+ return results.keys()
+
+
+def translate(srcString):
+ """
+ Look up srcString in the various translation databases (if any), returning
+ a list of all matches found (potentially the empty list)
+ """
+ # Use a dict to get uniqueness:
+ results = {}
+ # Try to translate the string:
+ for translationDb in translationDbs:
+ for result in translationDb.getTranslationsOf(srcString):
+ result = safeDecode(result)
+ results[result] = True
+
+ # No translations found:
+ if len(results) == 0:
+ if config.config.debugTranslation:
+ logger.log('Translation not found for "%s"' % srcString)
+ return results.keys()
+
+
+class TranslatableString(object):
+
+ """
+ Class representing a string that we want to match strings against, handling
+ translation for us, by looking it up once at construction time.
+ """
+
+ def __init__(self, untranslatedString):
+ """
+ Constructor looks up the string in all of the translation databases, storing
+ the various translations it finds.
+ """
+ untranslatedString = safeDecode(untranslatedString)
+ self.untranslatedString = untranslatedString
+ self.translatedStrings = translate(untranslatedString)
+
+ def matchedBy(self, string):
+ """
+ Compare the test string against either the translation of the original
+ string (or simply the original string, if no translation was found).
+ """
+ # print "comparing %s against %s"%(string, self)
+ def stringsMatch(inS, outS):
+ """
+ Compares a regular expression to a string
+
+ inS: the regular expression (or normal string)
+ outS: the normal string to be compared against
+ """
+ inString = str(inS)
+ outString = outS
+ if inString == outString:
+ return True
+ inString = inString + '$'
+ inString = safeDecode(inString)
+ outString = safeDecode(outString)
+ if inString[0] == '*':
+ inString = "\\" + inString
+ # Escape all parentheses, since grouping will never be needed here
+ inString = re.sub('([\(\)])', r'\\\1', inString)
+ match = re.match(inString, outString)
+ matched = match is not None
+ return matched
+
+ matched = False
+ # the 'ts' variable keeps track of whether we're working with
+ # translated strings. it's only used for debugging purposes.
+ #ts = 0
+ # print string, str(self)
+ for translatedString in self.translatedStrings:
+ #ts = ts + 1
+ matched = stringsMatch(translatedString, string)
+ if not matched:
+ matched = translatedString == string
+ if matched:
+ return matched
+ # ts=0
+ return stringsMatch(self.untranslatedString, string)
+
+ def __str__(self):
+ """
+ Provide a meaningful debug version of the string (and the translation in
+ use)
+ """
+ if len(self.translatedStrings) > 0:
+ # build an output string, with commas in the correct places
+ translations = ""
+ for tString in self.translatedStrings:
+ translations += u'"%s", ' % safeDecode(tString)
+ result = u'"%s" (%s)' % (
+ safeDecode(self.untranslatedString), translations)
+ return safeDecode(result)
+ else:
+ return '"%s"' % (self.untranslatedString)
+
+
+def isMoFile(filename, language=''):
+ """
+ Does the given filename look like a gettext mo file?
+
+ Optionally: Does the file also contain translations for a certain language,
+ for example 'ja'?
+ """
+ if re.match('(.*)\\.mo$', filename):
+ if not language:
+ return True
+ elif re.match('/usr/share/locale(.*)/%s(.*)/LC_MESSAGES/(.*)\\.mo$' %
+ language, filename):
+ return True
+ else:
+ return False
+ else:
+ return False
+
+
+def loadAllTranslationsForLanguage(language):
+ import distro
+ for moFile in distro.packageDb.getMoFiles(language):
+ translationDbs.append(GettextTranslationDb(moFile))
+
+
+def getMoFilesForPackage(packageName, language='', getDependencies=True):
+ """
+ Look up the named package and find all gettext mo files within it and its
+ dependencies. It is possible to restrict the results to those of a certain
+ language, for example 'ja'.
+ """
+ import distro
+
+ result = []
+ for filename in distro.packageDb.getFiles(packageName):
+ if isMoFile(filename, language):
+ result.append(filename)
+
+ if getDependencies:
+ # Recurse:
+ for dep in distro.packageDb.getDependencies(packageName):
+ # We pass False to the inner call because getDependencies has already
+ # walked the full tree
+ result.extend(getMoFilesForPackage(dep, language, False))
+
+ return result
+
+
+def loadTranslationsFromPackageMoFiles(packageName, getDependencies=True):
+ """
+ Helper function which appends all of the gettext translation mo-files used by
+ the package (and its dependencies) to the translation database list.
+ """
+ # Keep a list of mo-files that are already in use to avoid duplicates.
+ moFiles = {}
+
+ def load(packageName, language='', getDependencies=True):
+ for moFile in getMoFilesForPackage(packageName, language, getDependencies):
+ # Searching the popt mo-files for translations makes gettext bail out,
+ # so we ignore them here. This is
+ # https://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=172155 .
+ if not('popt.mo' in moFile or moFile in moFiles):
+ try:
+ translationDbs.append(GettextTranslationDb(moFile))
+ moFiles[moFile] = None
+ except (AttributeError, IndexError):
+ if config.config.debugTranslation:
+ #import traceback
+ # logger.log(traceback.format_exc())
+ logger.log(
+ "Warning: Failed to load mo-file for translation: " + moFile)
+
+ # Hack alert:
+ #
+ # The following special-case is necessary for Ubuntu, since their
+ # translations are shipped in a single huge package. The downside to
+ # this special case, aside from the simple fact that there is one,
+ # is that it makes automatic translations much slower.
+
+ import distro
+ language = os.environ.get('LANGUAGE', os.environ['LANG'])[0:2]
+ if isinstance(distro.distro, distro.Ubuntu):
+ load('language-pack-gnome-%s' % language, language)
+ load(packageName, language, getDependencies)
diff --git a/dogtail/i18n.pyc b/dogtail/i18n.pyc
new file mode 100644
index 0000000000..d999807462
--- /dev/null
+++ b/dogtail/i18n.pyc
Binary files differ
diff --git a/dogtail/logging.py b/dogtail/logging.py
new file mode 100755
index 0000000000..7e73f163b8
--- /dev/null
+++ b/dogtail/logging.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+"""
+Logging facilities
+
+Authors: Ed Rousseau <rousseau@redhat.com>, Zack Cerza <zcerza@redhat.com, David Malcolm <dmalcolm@redhat.com>
+"""
+
+__author__ = """Ed Rousseau <rousseau@redhat.com>,
+Zack Cerza <zcerza@redhat.com,
+David Malcolm <dmalcolm@redhat.com>
+"""
+import os
+import sys
+import time
+from config import config
+import codecs
+
+# Timestamp class for file logs
+
+
+class TimeStamp(object):
+
+ """
+ Generates timestamps tempfiles and log entries
+ """
+
+ def __init__(self):
+ self.now = "0"
+ self.timetup = time.localtime()
+
+ def zeroPad(self, int, width=2):
+ """
+ Pads an integer 'int' with zeroes, up to width 'width'.
+
+ Returns a string.
+
+ It will not truncate. If you call zeroPad(100, 2), '100' will be returned.
+ """
+ if int < 10 ** width:
+ return ("0" * (width - len(str(int)))) + str(int)
+ else:
+ return str(int)
+
+ # file stamper
+ def fileStamp(self, filename, addTime=True):
+ """
+ Generates a filename stamp in the format of filename_YYYYMMDD-hhmmss.
+ A format of filename_YYYYMMDD can be used instead by specifying addTime = False.
+ """
+ self.now = filename.strip() + "_"
+ self.timetup = time.localtime()
+
+ # Should produce rel-eng style filestamps
+ # format it all pretty by chopping the tuple
+ fieldCount = 3
+ if addTime:
+ fieldCount = fieldCount + 3
+ for i in range(fieldCount):
+ if i == 3:
+ self.now = self.now + '-'
+ self.now = self.now + self.zeroPad(self.timetup[i])
+ return self.now
+
+ # Log entry stamper
+ def entryStamp(self):
+ """
+ Generates a logfile entry stamp of YYYY.MM.DD HH:MM:SS
+ """
+ self.timetup = time.localtime()
+
+ # This will return a log entry formatted string in YYYY.MM.DD HH:MM:SS
+ for i in range(6):
+ # put in the year
+ if i == 0:
+ self.now = str(self.timetup[i])
+ # Format Month and Day
+ elif i == 1 or i == 2:
+ self.now = self.now + "." + self.zeroPad(self.timetup[i])
+ else:
+ # make the " " between Day and Hour and put in the hour
+ if i == 3:
+ self.now = self.now + " " + self.zeroPad(self.timetup[i])
+ # Otherwise Use the ":" divider
+ else:
+ self.now = self.now + ":" + self.zeroPad(self.timetup[i])
+ return self.now
+
+
+class Logger(object):
+
+ """
+ Writes entries to standard out.
+ """
+ stamper = TimeStamp()
+
+ def __init__(self, logName, file=False, stdOut=True):
+ """
+ name: the name of the log
+ file: The file object to log to.
+ stdOut: Whether to log to standard out.
+ """
+ self.logName = logName
+ self.stdOut = stdOut
+ self.file = file # Handle to the logfile
+ if not self.file:
+ return
+
+ scriptName = config.scriptName
+ if not scriptName:
+ scriptName = 'log'
+ self.fileName = scriptName
+
+ # check to see if we can write to the logDir
+ if os.path.isdir(config.logDir):
+ self.findUniqueName()
+ else:
+ # If path doesn't exist, raise an exception
+ raise IOError(
+ "Log path %s does not exist or is not a directory" % config.logDir)
+
+ def findUniqueName(self):
+ # generate a logfile name and check if it already exists
+ self.fileName = config.logDir + self.stamper.fileStamp(self.fileName) \
+ + '_' + self.logName
+ i = 0
+ while os.path.exists(self.fileName):
+ # Append the pathname
+ if i == 0:
+ self.fileName = self.fileName + "." + str(i)
+ else:
+ logsplit = self.fileName.split(".")
+ logsplit[-1] = str(i)
+ self.fileName = ".".join(logsplit)
+ i += 1
+
+ def createFile(self):
+ # Try to create the file and write the header info
+ print("Creating logfile at %s ..." % self.fileName)
+ self.file = codecs.open(self.fileName, mode='wb', encoding=
+ 'utf-8')
+ self.file.write("##### " + os.path.basename(self.fileName) + '\n')
+ self.file.flush()
+
+ def log(self, message, newline=True, force=False):
+ """
+ Hook used for logging messages. Might eventually be a virtual
+ function, but nice and simple for now.
+
+ If force is True, log to a file irrespective of config.logDebugToFile.
+ """
+ try:
+ message = message.decode('utf-8', 'replace')
+ except UnicodeEncodeError:
+ pass
+
+
+ # Try to open and write the result to the log file.
+ if isinstance(self.file, bool) and (force or config.logDebugToFile):
+ self.createFile()
+
+ if force or config.logDebugToFile:
+ if newline:
+ self.file.write(message + '\n')
+ else:
+ self.file.write(message + ' ')
+ self.file.flush()
+
+ if self.stdOut and config.logDebugToStdOut:
+ if newline:
+ print(message)
+ else:
+ print(message)
+
+
+class ResultsLogger(Logger):
+
+ """
+ Writes entries into the Dogtail log
+ """
+
+ def __init__(self, stdOut=True):
+ Logger.__init__(self, 'results', file=True, stdOut=stdOut)
+
+ # Writes the result of a test case comparison to the log
+ def log(self, entry):
+ """
+ Writes the log entry. Requires a 1 {key: value} pair dict for an argument or else it will throw an exception.
+ """
+ # We require a 1 key: value dict
+ # Strip all leading and trailing witespace from entry dict and convert
+ # to string for writing
+
+ if len(entry) == 1:
+ key = entry.keys()
+ value = entry.values()
+ key = key[0]
+ value = value[0]
+ entry = str(key) + ": " + str(value)
+ else:
+ raise ValueError(entry)
+ print(
+ "Method argument requires a 1 {key: value} dict. Supplied argument not one {key: value}")
+
+ Logger.log(self, self.stamper.entryStamp() + " " + entry,
+ force=True)
+
+debugLogger = Logger('debug', config.logDebugToFile)
+
+import traceback
+
+
+def exceptionHook(exc, value, tb): # pragma: no cover
+ tbStringList = traceback.format_exception(exc, value, tb)
+ tbString = ''.join(tbStringList)
+ debugLogger.log(tbString)
+ sys.exc_clear()
+
+sys.excepthook = exceptionHook
diff --git a/dogtail/logging.pyc b/dogtail/logging.pyc
new file mode 100644
index 0000000000..9b01a4b4cc
--- /dev/null
+++ b/dogtail/logging.pyc
Binary files differ
diff --git a/dogtail/path.py b/dogtail/path.py
new file mode 100755
index 0000000000..5742cfb03e
--- /dev/null
+++ b/dogtail/path.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+"""
+Author: David Malcolm <dmalcolm@redhat.com>
+"""
+__author__ = """David Malcolm <dmalcolm@redhat.com>"""
+
+
+class SearchPath(object):
+
+ """
+ Class used by the recording framework (and for more verbose script
+ logging) for identifying nodes in a persistent way, independent of the
+ style of script being written.
+
+ Implemented as a list of (predicate, isRecursive) pairs, giving the
+ 'best' way to find the Accessible wrapped by a Node, starting at the
+ root and applying each search in turn.
+
+ This is somewhat analagous to an absolute path in a filesystem, except
+ that some of searches may be recursive, rather than just searching
+ direct children.
+
+ FIXME: try to ensure uniqueness
+ FIXME: need some heuristics to get 'good' searches, whatever
+ that means
+ """
+
+ def __init__(self):
+ self.__list = []
+
+ def __str__(self):
+ result = "{"
+ for (predicate, isRecursive) in self.__list:
+ result += "/(%s,%s)" % (
+ predicate.describeSearchResult(), isRecursive)
+ return result + "}"
+
+ # We need equality to work so that dicts of these work:
+ def __eq__(self, other):
+ # print "eq: self:%s"%self
+ # print " other:%s"%other
+ if len(self.__list) != len(other.__list):
+ # print "nonequal length"
+ return False
+ else:
+ for i in range(len(self.__list)):
+ if self.__list[i] != other.__list[i]:
+ return False
+ # print True
+ return True
+
+ def append(self, predicate, isRecursive):
+ assert predicate
+ self.__list.append((predicate, isRecursive))
+
+ def __iter__(self):
+ return iter(self.__list)
+
+ def length(self):
+ return len(self.__list)
+
+ def makeScriptMethodCall(self):
+ """
+ Used by the recording system.
+
+ Generate the Python source code that will carry out this search.
+ """
+ result = ""
+ for (predicate, isRecursive) in self.__list:
+ # print predicate
+ # print self.generateVariableName(predicate)
+ result += "." + predicate.makeScriptMethodCall(isRecursive)
+ return result
+
+ def getRelativePath(self, other):
+ """
+ Given another SearchPath instance, if the other is 'below' this
+ one, return a SearchPath that describes how to reach it relative
+ to this one (a copy of the second part of the list). Otherwise
+ return None.
+ """
+ for i in range(len(self.__list)):
+ if self.__list[i] != other.__list[i]:
+ break
+ if i > 0:
+ # Slice from this point to the end:
+ result = SearchPath()
+ result.__list = other.__list[i + 1:]
+
+ if False:
+ print("....................")
+ print("from %s" % self)
+ print("to %s" % other)
+ print("i=%s" % i)
+ print("relative path %s" % result)
+ print("....................")
+
+ return result
+ else:
+ return None
+
+ def getPrefix(self, n):
+ """
+ Get the first n components of this instance as a new instance
+ """
+ result = SearchPath()
+ for i in range(n):
+ result.__list.append(self.__list[i])
+ return result
+
+ def getPredicate(self, i):
+ (predicate, isRecursive) = self.__list[i]
+ return predicate
diff --git a/dogtail/path.pyc b/dogtail/path.pyc
new file mode 100644
index 0000000000..610858fbd0
--- /dev/null
+++ b/dogtail/path.pyc
Binary files differ
diff --git a/dogtail/predicate.py b/dogtail/predicate.py
new file mode 100755
index 0000000000..aa100e1f05
--- /dev/null
+++ b/dogtail/predicate.py
@@ -0,0 +1,443 @@
+"""Predicates that can be used when searching for nodes.
+
+Author: David Malcolm <dmalcolm@redhat.com>"""
+__author__ = 'David Malcolm <dmalcolm@redhat.com>'
+
+from i18n import TranslatableString
+from gi.repository import GLib
+from time import sleep
+from logging import debugLogger as logger
+from config import config
+
+def stringMatches(scriptName, reportedName):
+ assert isinstance(scriptName, TranslatableString)
+
+ return scriptName.matchedBy(reportedName)
+
+
+def makeScriptRecursiveArgument(isRecursive, defaultValue):
+ if isRecursive == defaultValue:
+ return ""
+ else:
+ return ", recursive=%s" % isRecursive
+
+
+def makeCamel(string):
+ """
+ Convert string to camelCaps
+ """
+ string = str(string)
+ # FIXME: this function is probably really fragile, lots of difficult cases
+ # here
+
+ # Sanitize string, replacing bad characters with spaces:
+ for char in ":;!@#$%^&*()-+=_~`\\/?|[]{}<>,.\t\n\r\"'":
+ string = string.replace(char, " ")
+ words = string.strip().split(" ")
+ for word in words:
+ word.strip
+ result = ""
+ firstWord = True
+ for word in words:
+ lowercaseWord = word.lower()
+ if firstWord:
+ result += lowercaseWord
+ firstWord = False
+ else:
+ result += lowercaseWord.capitalize()
+ return result
+
+
+class Predicate(object):
+
+ """Abstract base class representing a predicate function on nodes.
+
+ It's more than just a function in that it has data and can describe itself"""
+
+ def satisfiedByNode(self, node):
+ """Pure virtual method returning a boolean if the predicate is satisfied by the node"""
+ raise NotImplementedError
+
+ def describeSearchResult(self, node):
+ raise NotImplementedError
+
+ def makeScriptMethodCall(self, isRecursive):
+ """
+ Method to generate a string containing a (hopefully) readable search
+ method call on a node (to be used when generating Python source code in
+ the event recorder)
+ """
+ raise NotImplementedError
+
+ def makeScriptVariableName(self):
+ """
+ Method to generate a string containing a (hopefully) readable name
+ for a Node instance variable that would be the result of a search on
+ this predicate (to be used when generating Python source code in the
+ event recorder).
+ """
+ raise NotImplementedError
+
+ def __eq__(self, other):
+ """
+ Predicates are considered equal if they are of the same subclass and
+ have the same data
+ """
+ # print "predeq: self:%s"%self
+ # print " other:%s"%other
+ # print "predeq: selfdict:%s"%self.__dict__
+ # print " otherdict:%s"%other.__dict__
+
+ if type(self) != type(other):
+ return False
+ else:
+ return self.__dict__ == other.__dict__
+
+
+class IsAnApplicationNamed(Predicate):
+
+ """Search subclass that looks for an application by name"""
+
+ def __init__(self, appName):
+ self.appName = TranslatableString(appName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ try:
+ return node.roleName == 'application' and stringMatches(self.appName, node.name)
+ except GLib.GError as e:
+ if 'name :1.0 was not provided' in e.message:
+ logger.log("Dogtail: warning: omiting possibly broken at-spi application record")
+ return False
+ else:
+ try:
+ sleep(config.defaults['searchWarningThreshold'])
+ return node.roleName == 'application' and stringMatches(self.appName, node.name)
+ except GLib.GError:
+ logger.log("Dogtail: warning: application may be hanging")
+ return False
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return '%s application' % self.appName
+
+ def makeScriptMethodCall(self, isRecursive):
+ # ignores the isRecursive parameter
+ return "application(%s)" % self.appName
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.appName) + "App"
+
+
+class GenericPredicate(Predicate):
+
+ """SubtreePredicate subclass that takes various optional search fields"""
+
+ def __init__(self, name=None, roleName=None, description=None, label=None, debugName=None):
+ if name:
+ self.name = TranslatableString(name)
+ else:
+ self.name = None
+ self.roleName = roleName
+ self.description = description
+ if label:
+ self.label = TranslatableString(label)
+ else:
+ self.label = None
+
+ if debugName:
+ self.debugName = debugName
+ else:
+ if label:
+ self.debugName = "labelled '%s'" % self.label
+ else:
+ self.debugName = "child with"
+ if name:
+ self.debugName += " name=%s" % self.name
+ if roleName:
+ self.debugName += " roleName='%s'" % roleName
+ if description:
+ self.debugName += " description='%s'" % description
+ assert self.debugName
+
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ # labelled nodes are handled specially:
+ if self.label:
+ # this reverses the search; we're looking for a node with LABELLED_BY
+ # and then checking the label, rather than looking for a label and
+ # then returning whatever LABEL_FOR targets
+ if node.labeller:
+ return stringMatches(self.label, node.labeller.name)
+ else:
+ return False
+ else:
+ # Ensure the node matches any criteria that were set:
+ try:
+ if self.name:
+ if not stringMatches(self.name, node.name):
+ return False
+ if self.roleName:
+ if self.roleName != node.roleName:
+ return False
+ if self.description:
+ if self.description != node.description:
+ return False
+ except GLib.GError as e:
+ if 'name :1.0 was not provided' in e.message:
+ logger.log("Dogtail: warning: omiting possibly broken at-spi application record")
+ return False
+ else:
+ raise e
+ return True
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return self.debugName
+
+ def makeScriptMethodCall(self, isRecursive):
+ if self.label:
+ args = "label=%s" % self.label
+ else:
+ args = ""
+ if self.name:
+ print(self.name)
+ args += " name=%s" % self.name
+ if self.roleName:
+ args += " roleName='%s'" % self.roleName
+ if self.description:
+ args += " description='%s'" % self.description
+ return "child(%s%s)" % (args, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ if self.label:
+ return makeCamel(self.label) + "Node"
+ else:
+ if self.name:
+ return makeCamel(self.name) + "Node"
+ if self.roleName:
+ return makeCamel(self.roleName) + "Node"
+ if self.description:
+ return makeCamel(self.description) + "Node"
+
+
+class IsNamed(Predicate):
+
+ """Predicate subclass that looks simply by name"""
+
+ def __init__(self, name):
+ self.name = TranslatableString(name)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ return stringMatches(self.name, node.name)
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return "named %s" % self.name
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "child(name=%s%s)" % (self.name, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.name) + "Node"
+
+
+class IsAWindowNamed(Predicate):
+
+ """Predicate subclass that looks for a top-level window by name"""
+
+ def __init__(self, windowName):
+ self.windowName = TranslatableString(windowName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ return node.roleName == 'frame' and stringMatches(self.windowName, node.name)
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return "%s window" % self.windowName
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "window(%s%s)" % (self.windowName, makeScriptRecursiveArgument(isRecursive, False))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.windowName) + "Win"
+
+
+class IsAWindow(Predicate):
+
+ """Predicate subclass that looks for top-level windows"""
+
+ def __init__(self):
+ self.satisfiedByNode = lambda node: node.roleName == 'frame'
+
+ def describeSearchResult(self):
+ return "window"
+
+
+class IsADialogNamed(Predicate):
+
+ """Predicate subclass that looks for a top-level dialog by name"""
+
+ def __init__(self, dialogName):
+ self.dialogName = TranslatableString(dialogName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ return node.roleName == 'dialog' and stringMatches(self.dialogName, node.name)
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return '%s dialog' % self.dialogName
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "dialog(%s%s)" % (self.dialogName, makeScriptRecursiveArgument(isRecursive, False))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.dialogName) + "Dlg"
+
+
+class IsLabelledBy(Predicate):
+
+ """Predicate: is this node labelled by another node"""
+ pass
+
+
+class IsLabelledAs(Predicate):
+
+ """Predicate: is this node labelled with the text string (i.e. by another node with that as a name)"""
+
+ def __init__(self, labelText):
+ self.labelText = TranslatableString(labelText)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = self._genCompareFunc()
+
+ def _genCompareFunc(self):
+ def satisfiedByNode(node):
+ # FIXME
+ if node.labeller:
+ return stringMatches(self.labelText, node.labeller.name)
+ else:
+ return False
+ return satisfiedByNode
+
+ def describeSearchResult(self):
+ return 'labelled %s' % self.labelText
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "child(label=%s%s)" % (self.labelText, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.labelText) + "Node"
+
+
+class IsAMenuNamed(Predicate):
+
+ """Predicate subclass that looks for a menu by name"""
+
+ def __init__(self, menuName):
+ self.menuName = TranslatableString(menuName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'menu' and \
+ stringMatches(self.menuName, node.name)
+
+ def describeSearchResult(self):
+ return '%s menu' % (self.menuName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "menu(%s%s)" % (self.menuName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.menuName) + "Menu"
+
+
+class IsAMenuItemNamed(Predicate):
+
+ """Predicate subclass that looks for a menu item by name"""
+
+ def __init__(self, menuItemName):
+ self.menuItemName = TranslatableString(menuItemName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: \
+ node.roleName.endswith('menu item') and \
+ stringMatches(self.menuItemName, node.name)
+
+ def describeSearchResult(self):
+ return '%s menuitem' % (self.menuItemName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "menuItem(%s%s)" % (self.menuItemName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.menuItemName) + "MenuItem"
+
+
+class IsATextEntryNamed(Predicate):
+
+ """Predicate subclass that looks for a text entry by name"""
+
+ def __init__(self, textEntryName):
+ self.textEntryName = TranslatableString(textEntryName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'text' and \
+ stringMatches(self.textEntryName, node.name)
+
+ def describeSearchResult(self):
+ return '%s textentry' % (self.textEntryName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "textentry(%s%s)" % (self.textEntryName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.textEntryName) + "Entry"
+
+
+class IsAButtonNamed(Predicate):
+
+ """Predicate subclass that looks for a button by name"""
+
+ def __init__(self, buttonName):
+ self.buttonName = TranslatableString(buttonName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'push button' \
+ and stringMatches(self.buttonName, node.name)
+
+ def describeSearchResult(self):
+ return '%s button' % (self.buttonName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "button(%s%s)" % (self.buttonName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.buttonName) + "Button"
+
+
+class IsATabNamed(Predicate):
+
+ """Predicate subclass that looks for a tab by name"""
+
+ def __init__(self, tabName):
+ self.tabName = TranslatableString(tabName)
+ self.debugName = self.describeSearchResult()
+ self.satisfiedByNode = lambda node: node.roleName == 'page tab' and \
+ stringMatches(self.tabName, node.name)
+
+ def describeSearchResult(self):
+ return '%s tab' % (self.tabName)
+
+ def makeScriptMethodCall(self, isRecursive):
+ return "tab(%s%s)" % (self.tabName, makeScriptRecursiveArgument(isRecursive, True))
+
+ def makeScriptVariableName(self):
+ return makeCamel(self.tabName) + "Tab"
diff --git a/dogtail/predicate.pyc b/dogtail/predicate.pyc
new file mode 100644
index 0000000000..f72e74c105
--- /dev/null
+++ b/dogtail/predicate.pyc
Binary files differ
diff --git a/dogtail/procedural.py b/dogtail/procedural.py
new file mode 100755
index 0000000000..c87ae86181
--- /dev/null
+++ b/dogtail/procedural.py
@@ -0,0 +1,455 @@
+"""
+Dogtail's procedural UI
+All the classes here are intended to be single-instance, except for Action.
+"""
+__author__ = 'Zack Cerza <zcerza@redhat.com>'
+#
+#
+# WARNING: Here There Be Dragons (TM) #
+#
+# If you don't understand how to use this API, you almost certainly don't #
+# want to read the code first. We make use of some very non-intuitive #
+# features of Python in order to make the API very simplistic. Therefore, #
+# you should probably only read this code if you're already familiar with #
+# some of Python's advanced features. You have been warned. ;) #
+#
+#
+
+import tree
+import predicate
+from config import config
+from utils import Lock
+import rawinput
+
+#FocusError = "FocusError: %s not found"
+
+
+class FocusError(Exception):
+ pass
+
+import errors
+
+
+def focusFailed(pred):
+ errors.warn('The requested widget could not be focused: %s' %
+ pred.debugName)
+
+ENOARGS = "At least one argument is needed"
+
+
+class FocusBase(object):
+
+ """
+ The base for every class in the module. Does nothing special, really.
+ """
+ node = None
+
+ def __getattr__(self, name):
+ # Fold all the Node's AT-SPI properties into the Focus object.
+ try:
+ return getattr(self.node, name)
+ except AttributeError:
+ raise AttributeError(name)
+
+ def __setattr__(self, name, value):
+ # Fold all the Node's AT-SPI properties into the Focus object.
+ if name == 'node':
+ setattr(self.__class__, name, value)
+ else:
+ try:
+ setattr(self.node, name, value)
+ except AttributeError:
+ raise AttributeError(name)
+
+
+class FocusApplication (FocusBase):
+
+ """
+ Keeps track of which application is currently focused.
+ """
+ desktop = tree.root
+
+ def __call__(self, name):
+ """
+ Search for an application that matches and refocus on the given name.
+ """
+ try:
+ pred = predicate.IsAnApplicationNamed(name)
+ app = self.desktop.findChild(
+ pred, recursive=False, retry=False)
+ except tree.SearchError:
+ if config.fatalErrors:
+ raise FocusError(name)
+ else:
+ focusFailed(pred)
+ return False
+ if app:
+ FocusApplication.node = app
+ FocusDialog.node = None
+ FocusWindow.node = None
+ FocusWidget.node = None
+ return True
+
+
+class FocusDesktop (FocusBase):
+
+ """
+ This isn't used yet, and may never be used.
+ """
+ pass
+
+
+class FocusWindow (FocusBase):
+
+ """
+ Keeps track of which window is currently focused.
+ """
+
+ def __call__(self, name):
+ """
+ Search for a dialog that matches the given name and refocus on it.
+ """
+ result = None
+ pred = predicate.IsAWindowNamed(name)
+ try:
+ result = FocusApplication.node.findChild(
+ pred, requireResult=False, recursive=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWindow.node = result
+ FocusDialog.node = None
+ FocusWidget.node = None
+ else:
+ if config.fatalErrors:
+ raise FocusError(pred.debugName)
+ else:
+ focusFailed(pred)
+ return False
+ return True
+
+
+class FocusDialog (FocusBase):
+
+ """
+ Keeps track of which dialog is currently focused.
+ """
+
+ def __call__(self, name):
+ """
+ Search for a dialog that matches the given name and refocus on it.
+ """
+ result = None
+ pred = predicate.IsADialogNamed(name)
+ try:
+ result = FocusApplication.node.findChild(
+ pred, requireResult=False, recursive=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusDialog.node = result
+ FocusWidget.node = None
+ else:
+ if config.fatalErrors:
+ raise FocusError(pred.debugName)
+ else:
+ focusFailed(pred)
+ return False
+ return True
+
+
+class FocusWidget (FocusBase):
+
+ """
+ Keeps track of which widget is currently focused.
+ """
+
+ def findByPredicate(self, pred):
+ result = None
+ try:
+ result = FocusWidget.node.findChild(
+ pred, requireResult=False, retry=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWidget.node = result
+ else:
+ try:
+ result = FocusDialog.node.findChild(
+ pred, requireResult=False, retry=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWidget.node = result
+ else:
+ try:
+ result = FocusWindow.node.findChild(
+ pred, requireResult=False, retry=False)
+ except AttributeError:
+ pass
+ if result:
+ FocusWidget.node = result
+ else:
+ try:
+ result = FocusApplication.node.findChild(
+ pred, requireResult=False, retry=False)
+ if result:
+ FocusWidget.node = result
+ except AttributeError:
+ if config.fatalErrors:
+ raise FocusError(pred)
+ else:
+ focusFailed(pred)
+ return False
+
+ if result is None:
+ FocusWidget.node = result
+ if config.fatalErrors:
+ raise FocusError(pred.debugName)
+ else:
+ focusFailed(pred)
+ return False
+ return True
+
+ def __call__(self, name='', roleName='', description=''):
+ """
+ If name, roleName or description are specified, search for a widget that matches and refocus on it.
+ """
+ if not name and not roleName and not description:
+ raise TypeError(ENOARGS)
+
+ # search for a widget.
+ pred = predicate.GenericPredicate(name=name,
+ roleName=roleName, description=description)
+ return self.findByPredicate(pred)
+
+
+class Focus (FocusBase):
+
+ """
+ The container class for the focused application, dialog and widget.
+ """
+
+ def __getattr__(self, name):
+ raise AttributeError(name)
+
+ def __setattr__(self, name, value):
+ if name in ('application', 'dialog', 'widget', 'window'):
+ self.__dict__[name] = value
+ else:
+ raise AttributeError(name)
+
+ desktop = tree.root
+ application = FocusApplication()
+ app = application # shortcut :)
+ dialog = FocusDialog()
+ window = FocusWindow()
+ frame = window
+ widget = FocusWidget()
+
+ def button(self, name):
+ """
+ A shortcut to self.widget.findByPredicate(predicate.IsAButtonNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsAButtonNamed(name))
+
+ def icon(self, name):
+ """
+ A shortcut to self.widget(name, roleName = 'icon')
+ """
+ return self.widget(name=name, roleName='icon')
+
+ def menu(self, name):
+ """
+ A shortcut to self.widget.findByPredicate(predicate.IsAMenuNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsAMenuNamed(name))
+
+ def menuItem(self, name):
+ """
+ A shortcut to self.widget.findByPredicate(predicate.IsAMenuItemNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsAMenuItemNamed(name))
+
+ def table(self, name=''):
+ """
+ A shortcut to self.widget(name, roleName 'table')
+ """
+ return self.widget(name=name, roleName='table')
+
+ def tableCell(self, name=''):
+ """
+ A shortcut to self.widget(name, roleName 'table cell')
+ """
+ return self.widget(name=name, roleName='table cell')
+
+ def text(self, name=''):
+ """
+ A shortcut to self.widget.findByPredicate(IsATextEntryNamed(name))
+ """
+ return self.widget.findByPredicate(predicate.IsATextEntryNamed(name))
+
+
+class Action (FocusWidget):
+
+ """
+ Aids in executing AT-SPI actions, refocusing the widget if necessary.
+ """
+
+ def __init__(self, action):
+ """
+ action is a string with the same name as the AT-SPI action you wish to execute using this class.
+ """
+ self.action = action
+
+ def __call__(self, name='', roleName='', description='', delay=config.actionDelay):
+ """
+ If name, roleName or description are specified, first search for a widget that matches and refocus on it.
+ Then execute the action.
+ """
+ if name or roleName or description:
+ FocusWidget.__call__(
+ self, name=name, roleName=roleName, description=description)
+ self.node.doActionNamed(self.action)
+
+ def __getattr__(self, attr):
+ return getattr(FocusWidget.node, attr)
+
+ def __setattr__(self, attr, value):
+ if attr == 'action':
+ self.__dict__[attr] = value
+ else:
+ setattr(FocusWidget, attr, value)
+
+ def button(self, name):
+ """
+ A shortcut to self(name, roleName = 'push button')
+ """
+ self.__call__(name=name, roleName='push button')
+
+ def menu(self, name):
+ """
+ A shortcut to self(name, roleName = 'menu')
+ """
+ self.__call__(name=name, roleName='menu')
+
+ def menuItem(self, name):
+ """
+ A shortcut to self(name, roleName = 'menu item')
+ """
+ self.__call__(name=name, roleName='menu item')
+
+ def table(self, name=''):
+ """
+ A shortcut to self(name, roleName 'table')
+ """
+ self.__call__(name=name, roleName='table')
+
+ def tableCell(self, name=''):
+ """
+ A shortcut to self(name, roleName 'table cell')
+ """
+ self.__call__(name=name, roleName='table cell')
+
+ def text(self, name=''):
+ """
+ A shortcut to self(name, roleName = 'text')
+ """
+ self.__call__(name=name, roleName='text')
+
+
+class Click (Action):
+
+ """
+ A special case of Action, Click will eventually handle raw mouse events.
+ """
+ primary = 1
+ middle = 2
+ secondary = 3
+
+ def __init__(self):
+ Action.__init__(self, 'click')
+
+ def __call__(self, name='', roleName='', description='', raw=True, button=primary, delay=config.actionDelay):
+ """
+ By default, execute a raw mouse event.
+ If raw is False or if button evaluates to False, just pass the rest of
+ the arguments to Action.
+ """
+ if name or roleName or description:
+ FocusWidget.__call__(
+ self, name=name, roleName=roleName, description=description)
+ if raw and button:
+ # We're doing a raw mouse click
+ Click.node.click(button)
+ else:
+ Action.__call__(
+ self, name=name, roleName=roleName, description=description, delay=delay)
+
+
+class Select (Action):
+
+ """
+ Aids in selecting and deselecting widgets, i.e. page tabs
+ """
+ select = 'select'
+ deselect = 'deselect'
+
+ def __init__(self, action):
+ """
+ action must be 'select' or 'deselect'.
+ """
+ if action not in (self.select, self.deselect):
+ raise ValueError(action)
+ Action.__init__(self, action)
+
+ def __call__(self, name='', roleName='', description='', delay=config.actionDelay):
+ """
+ If name, roleName or description are specified, first search for a widget that matches and refocus on it.
+ Then execute the action.
+ """
+ if name or roleName or description:
+ FocusWidget.__call__(
+ self, name=name, roleName=roleName, description=description)
+ func = getattr(self.node, self.action)
+ func()
+
+
+def type(text):
+ if focus.widget.node:
+ focus.widget.node.typeText(text)
+ else:
+ rawinput.typeText(text)
+
+
+def keyCombo(combo):
+ if focus.widget.node:
+ focus.widget.node.keyCombo(combo)
+ else:
+ rawinput.keyCombo(combo)
+
+
+def run(application, arguments='', appName=''):
+ from utils import run as utilsRun
+ pid = utilsRun(application + ' ' + arguments, appName=appName)
+ focus.application(application)
+ return pid
+
+import os
+# tell sniff not to use auto-refresh while script using this module is running
+# may have already been locked by dogtail.tree
+if not os.path.exists('/tmp/sniff_refresh.lock'): # pragma: no cover
+ sniff_lock = Lock(lockname='sniff_refresh.lock', randomize=False)
+ try:
+ sniff_lock.lock()
+ except OSError:
+ pass # lock was already present from other script instance or leftover from killed instance
+ # lock should unlock automatically on script exit.
+
+focus = Focus()
+click = Click()
+activate = Action('activate')
+openItem = Action('open')
+menu = Action('menu')
+select = Select(Select.select)
+deselect = Select(Select.deselect)
diff --git a/dogtail/rawinput.py b/dogtail/rawinput.py
new file mode 100755
index 0000000000..7100b29bfa
--- /dev/null
+++ b/dogtail/rawinput.py
@@ -0,0 +1,252 @@
+# -*- coding: utf-8 -*-
+"""
+Handles raw input using AT-SPI event generation.
+
+Note: Think of keyvals as keysyms, and keynames as keystrings.
+
+Authors: David Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>
+"""
+
+__author__ = """
+David Malcolm <dmalcolm@redhat.com>,
+Zack Cerza <zcerza@redhat.com>
+"""
+import gi
+
+gi.require_version('Gtk', '3.0')
+gi.require_version('Gdk', '3.0')
+
+from gi.repository import Gdk
+from config import config
+from utils import doDelay
+from logging import debugLogger as logger
+from pyatspi import Registry as registry
+from pyatspi import (KEY_SYM, KEY_PRESS, KEY_PRESSRELEASE, KEY_RELEASE)
+from exceptions import ValueError
+from __builtin__ import unicode, unichr
+
+
+def doTypingDelay():
+ doDelay(config.typingDelay)
+
+
+def checkCoordinates(x, y):
+ if x < 0 or y < 0:
+ raise ValueError(
+ "Attempting to generate a mouse event at negative coordinates: (%s,%s)" % (x, y))
+
+
+def click(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button click at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s click at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sc' % button)
+ doDelay(config.actionDelay)
+
+
+def doubleClick(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button double-click at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s doubleclick at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sd' % button)
+ doDelay()
+
+
+def press(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button press at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s press at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sp' % button)
+ doDelay()
+
+
+def release(x, y, button=1, check=True):
+ """
+ Synthesize a mouse button release at (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse button %s release at (%s,%s)" % (button, x, y))
+ registry.generateMouseEvent(x, y, 'b%sr' % button)
+ doDelay()
+
+
+def absoluteMotion(x, y, mouseDelay=None, check=True):
+ """
+ Synthesize mouse absolute motion to (x,y)
+ """
+ if check:
+ checkCoordinates(x, y)
+ logger.log("Mouse absolute motion to (%s,%s)" % (x, y))
+ registry.generateMouseEvent(x, y, 'abs')
+ if mouseDelay:
+ doDelay(mouseDelay)
+ else:
+ doDelay()
+
+
+def relativeMotion(x, y, mouseDelay=None):
+ logger.log("Mouse relative motion of (%s,%s)" % (x, y))
+ registry.generateMouseEvent(x, y, 'rel')
+ if mouseDelay:
+ doDelay(mouseDelay)
+ else:
+ doDelay()
+
+
+def drag(fromXY, toXY, button=1, check=True):
+ """
+ Synthesize a mouse press, drag, and release on the screen.
+ """
+ logger.log("Mouse button %s drag from %s to %s" % (button, fromXY, toXY))
+
+ (x, y) = fromXY
+ press(x, y, button, check)
+ # doDelay()
+
+ (x, y) = toXY
+ absoluteMotion(x, y, check=check)
+ doDelay()
+
+ release(x, y, button, check)
+ doDelay()
+
+
+def typeText(string):
+ """
+ Types the specified string, one character at a time.
+ Please note, you may have to set a higher typing delay,
+ if your machine misses/switches the characters typed.
+ Needed sometimes on slow setups/VMs typing non-ASCII utf8 chars.
+ """
+ if not isinstance(string, unicode):
+ string = string.decode('utf-8')
+ for char in string:
+ pressKey(char)
+
+keyNameAliases = {
+ 'enter': 'Return',
+ 'esc': 'Escape',
+ 'alt': 'Alt_L',
+ 'control': 'Control_L',
+ 'ctrl': 'Control_L',
+ 'shift': 'Shift_L',
+ 'del': 'Delete',
+ 'ins': 'Insert',
+ 'pageup': 'Page_Up',
+ 'pagedown': 'Page_Down',
+ ' ': 'space',
+ '\t': 'Tab',
+ '\n': 'Return'
+}
+
+
+# TODO: Dead code
+def keySymToUniChar(keySym): # pragma: no cover
+ i = Gdk.keyval_to_unicode(keySym)
+ if i:
+ UniChar = unichr(i)
+ else:
+ UniChar = ''
+ return UniChar
+
+
+def uniCharToKeySym(uniChar):
+ # OK, if it's not actually unicode we can fix that, right?
+ if not isinstance(uniChar, unicode):
+ uniChar = unicode(uniChar, 'utf-8')
+ i = ord(uniChar)
+ keySym = Gdk.unicode_to_keyval(i)
+ return keySym
+
+
+# dead code
+def keySymToKeyName(keySym): # pragma: no cover
+ return Gdk.keyval_name(keySym)
+
+
+def keyNameToKeySym(keyName):
+ keyName = keyNameAliases.get(keyName.lower(), keyName)
+ keySym = Gdk.keyval_from_name(keyName)
+ # various error 'codes' returned for non-recognized chars in versions of GTK3.X
+ if keySym == 0xffffff or keySym == 0x0 or keySym is None:
+ try:
+ keySym = uniCharToKeySym(keyName)
+ except: # not even valid utf-8 char
+ try: # Last attempt run at a keyName ('Meta_L', 'Dash' ...)
+ keySym = getattr(Gdk, 'KEY_' + keyName)
+ except AttributeError:
+ raise KeyError(keyName)
+ return keySym
+
+
+def keyNameToKeyCode(keyName):
+ """
+ Use GDK to get the keycode for a given keystring.
+
+ Note that the keycode returned by this function is often incorrect when
+ the requested keystring is obtained by holding down the Shift key.
+
+ Generally you should use uniCharToKeySym() and should only need this
+ function for nonprintable keys anyway.
+ """
+ keymap = Gdk.Keymap.get_for_display(Gdk.Display.get_default())
+ entries = keymap.get_entries_for_keyval(
+ Gdk.keyval_from_name(keyName))
+ try:
+ return entries[1][0].keycode
+ except TypeError:
+ pass
+
+
+def pressKey(keyName):
+ """
+ Presses (and releases) the key specified by keyName.
+ keyName is the English name of the key as seen on the keyboard. Ex: 'enter'
+ Names are looked up in Gdk.KEY_ If they are not found there, they are
+ looked up by uniCharToKeySym().
+ """
+ keySym = keyNameToKeySym(keyName)
+ registry.generateKeyboardEvent(keySym, None, KEY_SYM)
+ doTypingDelay()
+
+
+def keyCombo(comboString):
+ """
+ Generates the appropriate keyboard events to simulate a user pressing the
+ specified key combination.
+
+ comboString is the representation of the key combo to be generated.
+ e.g. '<Control><Alt>p' or '<Control><Shift>PageUp' or '<Control>q'
+ """
+ strings = []
+ for s in comboString.split('<'):
+ if s:
+ for S in s.split('>'):
+ if S:
+ S = keyNameAliases.get(S.lower(), S)
+ strings.append(S)
+ for s in strings:
+ if not hasattr(Gdk, s):
+ if not hasattr(Gdk, 'KEY_' + s):
+ raise ValueError("Cannot find key %s" % s)
+ modifiers = strings[:-1]
+ finalKey = strings[-1]
+ for modifier in modifiers:
+ code = keyNameToKeyCode(modifier)
+ registry.generateKeyboardEvent(code, None, KEY_PRESS)
+ code = keyNameToKeyCode(finalKey)
+ registry.generateKeyboardEvent(code, None, KEY_PRESSRELEASE)
+ for modifier in modifiers:
+ code = keyNameToKeyCode(modifier)
+ registry.generateKeyboardEvent(code, None, KEY_RELEASE)
+ doDelay()
diff --git a/dogtail/rawinput.pyc b/dogtail/rawinput.pyc
new file mode 100644
index 0000000000..0f4b881fec
--- /dev/null
+++ b/dogtail/rawinput.pyc
Binary files differ
diff --git a/dogtail/sessions.py b/dogtail/sessions.py
new file mode 100755
index 0000000000..8d4bcce6a3
--- /dev/null
+++ b/dogtail/sessions.py
@@ -0,0 +1,231 @@
+import time
+import os
+import pwd
+import errno
+import re
+import subprocess
+import signal
+import tempfile
+import random
+import glob
+from dogtail.config import config
+
+
+def scratchFile(label): # pragma: no cover
+ """Uses tempfile.NamedTemporaryFile() to create a unique tempfile in
+ config.scratchDir, with a filename like:
+ dogtail-headless-<label>.<random junk>"""
+ prefix = "dogtail-headless-"
+ return tempfile.NamedTemporaryFile(prefix="%s%s." % (prefix, label),
+ dir=config.scratchDir)
+
+
+def testBinary(path): # pragma: no cover
+ if (path.startswith(os.path.sep) or
+ path.startswith(os.path.join('.', '')) or
+ path.startswith(os.path.join('..', ''))):
+ if not os.path.exists(path):
+ raise IOError(errno.ENOENT, "No such file", path)
+ if not os.access(path, os.X_OK):
+ raise IOError(errno.ENOEXEC, "Permission denied", path)
+ return True
+
+
+def get_username(): # pragma: no cover
+ return pwd.getpwuid(os.getuid())[0]
+
+
+class Subprocess(object): # pragma: no cover
+
+ def __init__(self, cmdList, environ=None):
+ testBinary(cmdList[0])
+ self.cmdList = cmdList
+ self.environ = environ
+ self._exitCode = None
+
+ def start(self):
+ if self.environ is None:
+ self.environ = os.environ
+ self.popen = subprocess.Popen(
+ self.cmdList, env=self.environ) # , stdout = subprocess.PIPE,
+ # stderr = subprocess.STDOUT, close_fds = True)
+ return self.popen.pid
+
+ def wait(self):
+ return self.popen.wait()
+
+ def stop(self):
+ # The following doesn't exist in python < 2.6, if you can believe it.
+ # self.popen.terminate()
+ os.kill(self.popen.pid, signal.SIGTERM)
+
+ @property
+ def exitCode(self):
+ if self._exitCode is None:
+ self._exitCode = self.wait()
+ return self._exitCode
+
+
+class XServer(Subprocess): # pragma: no cover
+
+ def __init__(self, server="/usr/bin/Xorg",
+ xinitrc="/etc/X11/xinit/Xclients",
+ resolution="1024x768x16"):
+ """resolution is only used with Xvfb."""
+ testBinary(server)
+ self.server = server
+ self._exitCode = None
+ self.xinit = "/usr/bin/xinit"
+ self.display = None
+ self.xinitrc = xinitrc
+ self.resolution = resolution
+
+ @staticmethod
+ def findFreeDisplay():
+ tmp = os.listdir('/tmp')
+ pattern = re.compile('\.X([0-9]+)-lock')
+ usedDisplays = []
+ for file in tmp:
+ match = re.match(pattern, file)
+ if match:
+ usedDisplays.append(int(match.groups()[0]))
+ if not usedDisplays:
+ return ':0'
+ usedDisplays.sort()
+ return ':' + str(usedDisplays[-1] + 1)
+
+ @property
+ def cmdList(self):
+ self.display = self.findFreeDisplay()
+ cmd = []
+ if self.xinit:
+ cmd.append(self.xinit)
+ if self.xinitrc:
+ cmd.append(self.xinitrc)
+ cmd.append('--')
+ cmd.append(self.server)
+ cmd.append(self.display)
+ cmd.extend(['-ac', '-noreset'])
+ if self.server.endswith('Xvfb'):
+ cmd.extend(['-screen', '0', self.resolution])
+ cmd.append('-shmem')
+ return cmd
+
+ def start(self):
+ print(' '.join(self.cmdList))
+ self.popen = subprocess.Popen(self.cmdList)
+ return self.popen.pid
+
+
+class Script(Subprocess): # pragma: no cover
+ pass
+
+
+class Session(object): # pragma: no cover
+
+ cookieName = "DOGTAIL_SESSION_COOKIE"
+
+ def __init__(self, sessionBinary, scriptCmdList=[], scriptDelay=20, logout=True):
+ testBinary(sessionBinary)
+ self.sessionBinary = sessionBinary
+ self.script = Script(scriptCmdList)
+ self.scriptDelay = scriptDelay
+ self.logout = logout
+ self.xserver = XServer()
+ self