diff --git a/agent/crawl/watsup/AppControls.py b/agent/crawl/watsup/AppControls.py new file mode 100644 index 0000000..dee6f64 --- /dev/null +++ b/agent/crawl/watsup/AppControls.py @@ -0,0 +1,152 @@ +""" Class and functions to find the windowText and className for a given executable +""" + +# Author : Tim Couper - tim@2wave.net +# Date : 22 July 2004 +# Version : 1.1 +# Copyright : Copyright TAC Software Ltd, under Python-style licence. +# Provided as-is, with no warranty. +# Notes : Requires Python 2.3, win32all, ctypes & watsup package + +import os.path + +from watsup.winGuiAuto import findTopWindows,findControls,dumpWindow,\ + dumpTopWindows +from watsup.utils import dumpHwnd,tupleHwnd +from watsup.launcher import AppThread,terminateApp +from watsup.OrderedDict import OrderedDict +from time import time,sleep +from types import ListType + +THREAD_TIMEOUT_SECS=2.0 +INDENT=2 + +#wstruct is of the form [hwnd,wclass,wtext] or +# [hwnd,wclass,wtext,[winstruct] + +##def printwstruct(wstruct): +## print 'printFormat had trouble with:\n%s' % str(wstruct[3]) +## import pprint +## print '------' +## pprint.pprint(wstruct) +## print '------' + +def printFormat(wstruct,indent=0): + def printwstruct(wstruct): + print 'printFormat had trouble with:\n%s' % str(wstruct[3]) + import pprint + print '------' + pprint.pprint(wstruct) + print '------' + """wstruct is either a wintuple, or a recursive list of wintuples + """ + + # type 1 + if type(wstruct[1]) is not ListType: + print '%s%s: %s' % (' '*indent,wstruct[2],wstruct[1]) + if len(wstruct)>3 and wstruct[3]<>None: + try: + for item in wstruct[3]: + printFormat(item,indent+INDENT) + except: + print_wstruct(wstruct) + # type 2: + else: + for item in wstruct[1]: + printFormat(item,indent+INDENT) + + +class AppControls(object): + def __init__(self,program,wantedText=None,wantedClass=None,selectionFunction=None,verbose=False): + + self.wantedText=wantedText + self.wantedClass=wantedClass + self.selectionFunction=selectionFunction + + self.verbose=verbose + + topHwnds,unwantedHwnds,self.appThread=findAppTopWindows(program,verbose=verbose) #should only be one, but you never know + + self.topHwnds=[] + self.unwantedHwnds=unwantedHwnds + + def run(self): + while self.appThread.isAlive(): + results=findNewTopWindows(self.unwantedHwnds,self.verbose) + if results: + self.process(results) # update the list of topWindows and unwanted TopWindows + + def process(self,results): + for hwnd in results: + + ctHwnds=findControls(hwnd) + if ctHwnds: + # we only add hwnd if there are controlHwnds + # as there may be a form which exists + # as an hwnd, but has not controls materialised yet + self.unwantedHwnds.append(hwnd) + self.write(hwnd) + for ctHwnd in ctHwnds: + self.unwantedHwnds.append(ctHwnd) + + def write(self,hwnd): + h=tupleHwnd(hwnd) + t=[h[0],h[1],h[2],dumpWindow(hwnd)] + printFormat(t) + +def findNewTopWindows(unwantedHwnds=[],verbose=False): + # returns a list of all top windows' hwnds we haven't + # found yet + htuples=dumpTopWindows() + + if verbose: + print '..%d windows found (%d windows in unwanted)' % (len(htuples),len(unwantedHwnds)) + results=[] + for htuple in htuples: + hwnd=htuple[0] + if hwnd not in unwantedHwnds: + if verbose: + print '..adding %s' % dumpHwnd(hwnd) + results.append(hwnd) + + return results + +def findAppTopWindows(program,verbose=False): + """returns the hwnds for the program, along with the hwnds for the + stuff that are to be ignored + """ + + # first we run findTopWindows before launching the program; store the hwnds found + # (note that it doesn't matter if an instance of the program IS running + # as we'll start another one whose hwnds will be new) + + # run findTopWindows, and remove from the list any which are stored + unwantedHwnds=findNewTopWindows() # list of topWindow hwnds that exist + if verbose: + print '..%d original window(s)' % len(unwantedHwnds) + # run the program + appThread=AppThread(program,verbose) + appThread.start() + # give the thread a chance to get going: + results=[] + t=time() + while not results and ((time()-t)None: + for cp in controlParameters: + hwnd=cp.get('hwnd',None) + wantedClass=cp.get('wantedClass',None) + wantedText=cp.get('wantedTest',None) + selectionFunction=cp.get('selectionFunction',None) + clist=self.findControls(hwnd=hwnd, + wantedText=wantedText, + wantedClass=wantedClass, + selectionFunction=selectionFunction) + + self.controls.extend(clist) + + self._mainMenuHandle=None + + def activateMenuItem(self,menuItem): + menuItem.activateMenuItem() + + def terminate(self): + terminateApp(self.hwnd) + +#------------------------------------------------------------------------------- +#Accessors & properties + +### top menu item +## def getMainMenu(self): +## if self._mainMenuHandle: +## return self._mainMenuHandle +## else: +## return getTopMenu(self.hwnd) +## +## mainMenu=property(getMainMenu) +#------------------------------------------------------------------------------- + +class PControlError(Exception): pass +class PControl(PWinControl): + def __init__(self,parent,hwnd=None,wantedText=None,wantedClass=None,selectionFunction=None): + "Constructor takes either hwnd directly, or others in a controlParameter set" + PWinControl.__init__(self,parent) + if hwnd: + self.hwnd=hwnd + else: + try: + self.hwnd=findControl(parent.hwnd, + wantedText=wantedText, + wantedClass=wantedClass, + selectionFunction=selectionFunction, + maxWait=CONTROL_MAX_WAIT_SECS) + except WinGuiAutoError,e: + raise PControlError,e + + +## def addKnowledge(self,attrName): +## knowledge=getKnowledge() +## knowledge.add(self.className,attrName) + + #------------------------------------------------------------------------------- + # general winctrl actions which, if acted upon by a user, might tell us sth about + # this unknown control + + def getItems(self): + # This PControl of unknown class can get items + # at least the user thinks so + #self.addKnowledge('getItems') + res=getComboboxItems(self.hwnd) + if not res: + res=getListboxItems(self.hwnd) + + return res + + def selectItem(self,item): + # This PControl of unknown class can select items + # at least the user thinks so + #self.addKnowledge('selectItem') + res= selectListboxItem(self.hwnd, item) + if not res: + res=selectComboBoxItem(self.hwnd,item) + return res + + def click(self): + # This PControl of unknown class is clickable + # at least the user thinks so + #self.addKnowledge('click') + clickButton(self.hwnd) + + def getCaption(self): + # This PControl of unknown class has a caption, + # at least the user thinks so + #self.addKnowledge('caption') + return self.getEditText() + + def setCheckBox(self, state=3DTrue): + setCheckBox(self.hwnd, state) + + def getCheckBox(self): + return getCheckBox(self.hwnd) + + #------------------------------------------------------------------------------- + #Accessors and properties + + def getText(self): + # returns a list of strings which make up the text + return getEditText(self.hwnd) + + def setText(self,text,append=False): + setEditText(text,append) + + def getClassName(self): + return win32gui.GetClassName(self.hwnd) + + text=property(getText) + className=property(getClassName) + caption=property(getCaption) + items=property(getItems) + + #------------------------------------------------------------------------------- + +class PEdit(PControl): + def __init__(self,parent,hwnd=None,wantedText=None,wantedClass=None,selectionFunction=None): + PControl.__init__(self,parent,hwnd,wantedText,wantedClass,selectionFunction) + + #------------------------------------------------------------------------------- + #Accessors and properties + + def getText(self): + # returns a simple string - PEdit controls only have one value + p=PControl.getText(self) + if p: + return p[0] + else: + return '' + + text=property(getText) + caption=None #undefine the caption property + + #------------------------------------------------------------------------------- + +class PText(PControl): + # multi-line text control + caption=None + +class PComboBox(PControl): + + def selectItem(self,item): + selectComboboxItem(self.hwnd,item) + + #------------------------------------------------------------------------------- + #Accessors and properties + + def getItems(self): + return getComboboxItems(self.hwnd) + + items=property(getItems) + + + #------------------------------------------------------------------------------- + + +class PDelphiComboBox(PComboBox): + # The Delphi Combo box has a control of class Edit within + # it, which contains the text + def __init__(self,parent,hwnd=None,wantedText=None,wantedClass=None,selectionFunction=None): + PControl.__init__(self,parent,hwnd,wantedText,wantedClass,selectionFunction) + self.editCtrl=self.findPControl(wantedClass='Edit') + #------------------------------------------------------------------------------- + #Accessors and properties + + def getText(self): + # get the content from the control Edit: + + return self.editCtrl.getText() + + text=property(getText) + + + #------------------------------------------------------------------------------- + +class PButton(PControl): + + def click(self): + clickButton(self.hwnd) + + #------------------------------------------------------------------------------- + #Accessors and properties + + caption=property(PControl.getText) + + #------------------------------------------------------------------------------- + +class PListBox(PControl): + + def selectItem(self,item): + return selectListboxItem(self.hwnd, item) + + #------------------------------------------------------------------------------- + #Accessors and properties + + def getItems(self): + return getListboxItems(self.hwnd) + + items=property(getItems) + + + #------------------------------------------------------------------------------- + +class PCheckBox(PControl): + + + #------------------------------------------------------------------------------- + #Accessors and properties + + + caption=property(PControl.getText) + + def getCheckStatus(self): + return self.getCheckBox() + + def isChecked(self): + return self.getCheckStatus()#=3D=3D win32con.BST_INDETERMINATE + + def isIndeterminate(self): + return self.getCheckStatus() #=3D=3D win32con.BST_INDETERMINATE + =20 + + def isNotChecked(self): + return self.getCheckStatus() #=3D=3D win32con.BST_UNCHECKED + =20 + + def setChecked(self): + setCheckBox(hwnd, state = True) + + def setUnChecked(self): + setCheckBox(hwnd, state = False) + + + #------------------------------------------------------------------------------- +KNOWN_CLASSES={'TEdit': PEdit, + 'TComboBox': PDelphiComboBox, + 'ComboBox': PComboBox, + 'TButton': PButton, + 'TListBox': PListBox, + 'ListBox': PListBox, + 'CheckBox': PCheckBox, + 'TCheckBox': PCheckBox, + + } + \ No newline at end of file diff --git a/agent/crawl/watsup/__init__.py b/agent/crawl/watsup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agent/crawl/watsup/__init__.pyc b/agent/crawl/watsup/__init__.pyc new file mode 100644 index 0000000..68a487e Binary files /dev/null and b/agent/crawl/watsup/__init__.pyc differ diff --git a/agent/crawl/watsup/docs/WatsupIE methods.doc b/agent/crawl/watsup/docs/WatsupIE methods.doc new file mode 100644 index 0000000..ae7b75c Binary files /dev/null and b/agent/crawl/watsup/docs/WatsupIE methods.doc differ diff --git a/agent/crawl/watsup/docs/html/code/example1.py b/agent/crawl/watsup/docs/html/code/example1.py new file mode 100644 index 0000000..a261280 --- /dev/null +++ b/agent/crawl/watsup/docs/html/code/example1.py @@ -0,0 +1,10 @@ +from watsup.winGuiAuto import findTopWindow,findControl,setEditText +from time import sleep +# Locate notepad's edit area, and enter various bits of text. + +notepadWindow = findTopWindow(wantedClass='Notepad') +editArea = findControl(notepadWindow,wantedClass="Edit") +setEditText(editArea, "Hello, again!") +sleep(0.8) +setEditText(editArea, " You still there?",True) + diff --git a/agent/crawl/watsup/docs/html/code/example2.py b/agent/crawl/watsup/docs/html/code/example2.py new file mode 100644 index 0000000..a6bf70c --- /dev/null +++ b/agent/crawl/watsup/docs/html/code/example2.py @@ -0,0 +1,27 @@ +from watsup.winGuiAuto import findControl,setEditText, findTopWindow,clickButton +import os +import os.path + +FILENAME='atestfile.txt' + +def main(): + # delete any occurrence of this file from the disk + if os.path.exists(FILENAME): + os.remove(FILENAME) + + form=findTopWindow(wantedText='Simple Form') + button=findControl(form,wantedText='Create file') + editbox=findControl(form,wantedClass='TEdit') + + # enter a filename: + setEditText(editbox,[FILENAME]) + clickButton(button) + + # now check that the file is there + if os.path.exists(FILENAME): + print 'file %s is present' %FILENAME + else: + print "file %s isn't there" % FILENAME + +if __name__=='__main__': + main() \ No newline at end of file diff --git a/agent/crawl/watsup/docs/html/code/example3.py b/agent/crawl/watsup/docs/html/code/example3.py new file mode 100644 index 0000000..6f8cb1c --- /dev/null +++ b/agent/crawl/watsup/docs/html/code/example3.py @@ -0,0 +1,17 @@ +from watsup.launcher import launchApp,terminateApp +from watsup.winGuiAuto import findTopWindows +import example2 + +# find an instance of SimpleForm. If one isn't there, launch it +forms=findTopWindows(wantedText='Simple Form') +if forms: + form=forms[0] +else: + form=launchApp('simple.exe',wantedText='Simple Form') + +example2.main() + +# and terminate the form +terminateApp(form) + + diff --git a/agent/crawl/watsup/docs/html/code/example4.py b/agent/crawl/watsup/docs/html/code/example4.py new file mode 100644 index 0000000..8e82bd8 --- /dev/null +++ b/agent/crawl/watsup/docs/html/code/example4.py @@ -0,0 +1,51 @@ +from watsup.launcher import launchApp,terminateApp +from watsup.winGuiAuto import findTopWindows, findControl,getEditText,clickButton +from watsup.performance import PerformanceCheck,PerformanceCheckError +from time import sleep,time + +def main(myExecutable,myWantedText): + # find an instance of SimpleForm. If one isn't there, launch it + forms=findTopWindows(wantedText=myWantedText) + if forms: + form=forms[0] + else: + form=launchApp(myExecutable,wantedText=myWantedText) + + button=findControl(form,wantedText='Click me') + editbox=findControl(form,wantedClass='TEdit') + + #start a performance check instance + p=PerformanceCheck() + + clickButton(button) + + # belts and braces to avoid infinite waiting! + maxWaitTime=2.0 + startTime=time() + + while time()-startTime + + +Windows Application Test System Using Python + + + + + + +

WATSUP - Windows Application Test System Using Python

+ +The WATSUP toolkit is designed to allow the automated test of Windows applications. +The system uses the "object-based" mechanism for identifying and invoking actions +on controls and menu items. +

So much has been written about the scope, robustness, scalability and outstanding + usability of Python that I'll go no further with it here, only to say that if + you haven't yet had a chance to use this comprehensive, open source language, + don't miss out on the opportunity to take a serious look at it!

+ +

The examples in this document assume a basic familiarity with Python.

+ + + +

Functional Tests

+ +Testers/developers write automated functional tests which follow a prescriptive, +possibly branching, possibly dynamic "user workflow". The script can check for +changes in the gui itself, operating system environment, file system, database +table and records, network, internet or extranet urls/pages/web services ... - in +fact anywhere that there could be changes. +

+Examination of the functions in module autoWinGui.py within the watsup package shows the variety of windows control items +that can be checked/modified. These include: +

+

The system also provides tools for finding windows by caption and/or class, + controls by text/caption and/or class, and menu items by text or position. (One + of the aspirations of this project is to continue to extend the list to include + as many controls as possible) .

+ +

Example 1 - automated writing on Notepad

+

Here's a simple example of the control over applications that you can have with watsup. +First, launch notepad from:

Windows Start Menu - All Programs - Accessories - Notepad

+Then run the following script (Example 1) + +
+from watsup.winGuiAuto import findTopWindow,findControl,setEditText
+from time import sleep  
+# Locate notepad's edit area, and enter various bits of text.
+
+notepadWindow = findTopWindow(wantedClass='Notepad')
+editArea = findControl(notepadWindow,wantedClass="Edit") 
+setEditText(editArea, "Hello, again!")  
+sleep(0.8) 
+setEditText(editArea, " You still there?",True) 
+ 
+ Finally, close notepad.

+ +

Example 2 - testing a simple example

+ +In functional tests, the tester wants to ensure that +the cause - invoking a sequence of windows events (button clicks, menu item activation) +has the predicted effect of, for example, a change in a value of a sindows control, +the creation of a file, or the entry of a new database record. +See the directory watsup/examples/simple directory for the executable simple.exe. + +If you run the application, you see a button and a text box. +Enter a valid filename into the box, say xyz, and +click the button; +after the file is created, a message box appears containing a success message, +and investigation of the directory watsup/examples/simple will show a file +called 'xyz.txt' has been created (or overwritten). + +

Now let's script a test to automate this functionality.

+ +First find and launch the application. +Then run the following script (Example 2) +
+from watsup.winGuiAuto import findControl,setEditText, findTopWindow,clickButton
+import os
+import os.path
+
+FILENAME='atestfile.txt'
+
+def main():
+    # delete any occurrence of this file from the disk
+    if os.path.exists(FILENAME):
+        os.remove(FILENAME)
+        
+    form=findTopWindow(wantedText='Simple Form')
+    button=findControl(form,wantedText='Create file')
+    editbox=findControl(form,wantedClass='TEdit')
+    
+    # enter a filename:
+    setEditText(editbox,[FILENAME])
+    print 'clicking button to create file'
+		clickButton(button)
+    
+    # now check that the file is there
+    if os.path.exists(FILENAME):
+        print 'file %s is present' %FILENAME
+    else:
+        print "file %s isn't there" % FILENAME     
+
+if __name__=='__main__':
+    main()    
+
+
+
+
+ + +

Example 3 - automating program launch and termination

+ +

It's a bit tedious having to start and close the application each time. +Example 3 launches the application, +if it isn't already running, and terminates it on +completion of the test

+ +
+from watsup.launcher import launchApp,terminateApp
+from watsup.winGuiAuto import findTopWindows                            
+import example2
+
+# find an instance of SimpleForm. If one isn't there, launch it
+
+forms=findTopWindows(wantedText='Simple Form')
+if forms:
+    form=forms[0]
+else:
+    form=launchApp('simple.exe',wantedText='Simple Form')    
+
+example2.main()
+
+# and terminate the form
+terminateApp(form)    
+
+
+ +launchApp starts the application in a separate thread, +and looks for a window with caption containing "Simple Form", +returning the window handle of the recovered form. + +terminateApp attempts to close the form, by trying to activate menu item File-Exit, or, failing that, +sending Alt + F4. + +

Example 4 - finding windows and controls

+ +

In building scripts, we need to be able to find the class and/or text of the many windows and controls +to be investigated or invoked.

+ +

In the tools directory, there's a tool - ShowWindows.bat - to assist us with this.

+Show Windows 1 + + +

Clicking the "Register" button persists information about +the existing windows running on the system (and it tells you how many, FYI). +Clicking the "Find new" button will report all non-trivial windows and all their +constituent controls which have appeared in the windows environment +since the last "Register" click. + +So to test our program simple.exe, launch ShowWindows, click Register. +Then launch simple.exe and +and click the Find New button. +The associated text box shows n/m, where m is the total number of new windows found, +and n is the number of those which are significant (ie have any controls) and are reported.

+ +Show Windows 2 + + +

Performance Tests

+ +

Performance tests, in this definition, are single-client scripts, +which are similar in operation to the functional tests above, +but for which certain steps of the tests +must either be done within an acceptable timeframe ("CHECKING") +and/or the time taken for those steps +should be recorded for subsequent analysis ("RECORDING").

+ +

WATSUP provides a simple mechanism to add such tests to existing functional tests. +In examples 4a & 4b, we launch almost identical applications, +perform.exe and perform2.exe respectively. + When the button is clicked, the text "Finished" is written +to the edit box. The difference between the programs is that the former is + coded to wait for 1/2 second before "Finished" +appears; in the latter case, the delay is 1.5 seconds.

+ +

In both cases, we are setting a performance test that the process +take no more than 1 second. +Clearly, we should expect example 4a to be ok and example 4b to fail. +

+ + +

So we have Example 4a

+
+from example4 import main
+
+main('perform.exe','Performance Form 1')
+
+
+

and Example 4b

+
+from example4 import main
+
+main('perform2.exe','Performance Form 2')
+
+
+ +

which reference Example 4:

+
+
+from watsup.launcher import launchApp,terminateApp 
+from watsup.winGuiAuto import findTopWindows, findControl,getEditText,clickButton  
+from watsup.performance import PerformanceCheck,PerformanceCheckError  
+from time import sleep,time                      
+
+def main(myExecutable,myWantedText):
+    # find an instance of SimpleForm. If one isn't there, launch it
+    forms=findTopWindows(wantedText=myWantedText)
+    if forms:
+        form=forms[0]
+    else:
+        form=launchApp(myExecutable,wantedText=myWantedText)       
+    
+    button=findControl(form,wantedText='Click me')
+    editbox=findControl(form,wantedClass='TEdit')
+
+    #start a performance check instance
+    p=PerformanceCheck()
+    
+    clickButton(button)    
+    
+    # belts and braces to avoid infinite waiting!
+    maxWaitTime=2.0
+    startTime=time()
+    
+    while time()-startTime<maxWaitTime:
+        t=getEditText(editbox)
+        if t:
+            break
+        else:
+            sleep(0.1)
+    else:
+        raise Exception,'Failed to get value after maxWaitTime of %s secs' % maxWaitTime        
+    
+    try:
+        try:
+           #do the check/recording step, identifying this step with the wantedtext
+            p.check(myWantedText,1.0)    
+        except PerformanceCheckError,e:
+            print '** Failed: %s' % e
+            
+    # and terminate the form
+    finally:
+        terminateApp(form)      
+
+if __name__=='__main__':
+    print ' please run example4a or 4b'
+
+
+ +

Key points in example4.py

+ +

Immediately prior to clicking the button, we establish a PerformanceCheck instance, +which, among other things, establises a timer. +Every time the check() method is called on a PerformanceCheck instance, the following occurs:

+ +

Also if we are CHECKING (the default condition), +that time is checked against the number of seconds added as the +second parameter. +If the elapsed time exceeds the value of the 2nd parameter, an exception is raised.

+ +

So in example4.py, we see that the check method requires a 1 second acceptance. +Hence example4a succeeds and example4b fails. +

+ +

Regression Testing

+ +

Functional and performance scripts such as those above are +immediately useable within a test framework - +the excellent python unit test framework is highly recommended +(unittest.py, which comes along with your python installation). +This then enables the tester to develop complete regression tests +involving any combination of Functional & Performance testing.

+

For an example of the use of functional and performance tests within +the unit test framework, run the program framework.bat in the tools subdirectory. +This will launch a nice user interface from which sets of test cases can be run: +

+ + +

Select the File menu option, and you have the option to load files which +contain testcases, or directories/directory trees. If select the "Load files" +option, and navigate up one directory, and then down through examples and the unittests +directories, you should find, and select, exampleTests.py. +The Framework program examines the selected file(s) and extracts the TestCases, +presents in the top frame

+ + + +

Selecting Menu option Testing - Run Tests (or the long button labelled Run Tests), + will cause each test to be run; success or failure is shown in the lower frame

+ + +

For more information on python's unit test module, refer to the python documentation. +Finally, it is worth noting that this framework will work with any unit tests, not just +those specifically testing windows application, so you can test +elements of the logic that you have written in other applications. + +

Downloads

+ +Download watsup here +

(This package should be unzipped in the site-packages directory in your python installation)

+ +

Dependencies

+ + +

Credits

+ +

The framework test tool was built from parts in unittestgui.py by Chris Liechti. +Much of the core functionality in WATSUP derives from the important work by + Simon Brunning, + who, in his module winGuiAuto.py provided concise, accessible mechanisms to + access and "click" windows controls & menus; Simon's work recognises the huge + contribution that Mark Hammond has made to the python community (and wider) + in providing pywin32, comprehensive win32 API modules for python. +

+ +Dr Tim Couper
+timc@tizmoi.net + + + diff --git a/agent/crawl/watsup/docs/images/tizmoi.jpg b/agent/crawl/watsup/docs/images/tizmoi.jpg new file mode 100644 index 0000000..eccd841 Binary files /dev/null and b/agent/crawl/watsup/docs/images/tizmoi.jpg differ diff --git a/agent/crawl/watsup/docs/tac.css b/agent/crawl/watsup/docs/tac.css new file mode 100644 index 0000000..3ca6dbd --- /dev/null +++ b/agent/crawl/watsup/docs/tac.css @@ -0,0 +1,7 @@ +body {font-family: "Trebuchet MS", Verdana, sans-serif} +code {font: Courier New; size 12} +h1 {color: navy} +h2 {color: navy} +h3 {color: navy} +h4 {color: navy} + diff --git a/agent/crawl/watsup/launcher.py b/agent/crawl/watsup/launcher.py new file mode 100644 index 0000000..e34441e --- /dev/null +++ b/agent/crawl/watsup/launcher.py @@ -0,0 +1,116 @@ +""" launch & terminate a windows application +""" + +# Author : Tim Couper - tim@2wave.net +# Date : 22 July 2004 +# Version : 1.0 +# Copyright : Copyright TAC Software Ltd, under Python-style licence. +# Provided as-is, with no warranty. +# Notes : Requires Python 2.3, win32all, ctypes, SendKeys & winGuiAuto + + +import os.path +import threading +import win32gui +#import win32api +#import win32con +import SendKeys +from watsup.utils import WatsupError +from watsup.winGuiAuto import findTopWindow,findTopWindows, \ + WinGuiAutoError,activateMenuItem + +MENU_TERMINATIONS=[('file', 'exit'),('file', 'quit')] + +def launchApp(program,wantedText=None,wantedClass=None,verbose=False): + global p + p=AppThread(program,verbose) + p.start() + try: + return findTopWindow(wantedText=wantedText,wantedClass=wantedClass) + except WinGuiAutoError,e: + pass + + # find all the Windows that are lurking about, and terminate them + # this I can't do until terminateApp can find a window and bring it to focus + # for now, find one top window and return that; if there are none, + # it should raise an exception + try: + return findTopWindows(wantedText=wantedText,wantedClass=wantedClass)[0] + except IndexError: + raise WatsupError,'Failed to find any windows' + +class AppThread(threading.Thread): + def __init__(self,program,verbose=False): + threading.Thread.__init__(self,name=program) + self.program=program + self.verbose=verbose + + def run(self): + """main control loop""" + #check the given program exists, and is a file: + # if the program has no type, make it .exe: + if os.path.splitext(self.program)[1]=='': + prog='%s.exe' % self.program + else: + prog=self.program + + prog=os.path.abspath(prog) + + if os.path.isfile(prog): + # launch the new application in a separate thread + if self.verbose: + print '..launching "%s"' % prog + os.system('"%s"' % prog) + + else: + print 'AppThread: program not found: "%s"\n' % prog + + if self.verbose: + print '..terminating "%s"' % prog + + +def terminateApp(hwnd=None): + """Terminate the application by: + If there's a file-exit menu, click that + If there's a file-Quit menu, click that + Otherwise, send Alt-F4 to the current active window +** it's the calling program responsibility to get the right active window + """ + if hwnd: + for fileExit in MENU_TERMINATIONS: + try: + activateMenuItem(hwnd, fileExit) + return + except WinGuiAutoError: + pass + + # blast the current window with ALT F4 .. + SendKeys.SendKeys("%{F4}") + #win32gui.PumpWaitingMessages() +## from watsup.winGuiAuto import findControl,findTopWindows +## topHs=findTopWindows(wantedText='MyTestForm') +## for topH in topHs: +## #hwnd=findControl(topH) +## a=win32gui.findWindow(topH) +## print a, topH +## win32gui.SetFocus(topH) +## win32gui.PumpWaitingMessages() +## SendKeys.SendKeys("%{F4}") +## + +## if not hwnd and (wantedText or wantedClass): +## hwnds=findTopWindows(wantedText=wantedText,wantedClass=wantedClass) +## elif hwnd: +## hwnds=[hwnd] +## +## if hwnds: +## for hwnd in hwnds: +## #win32gui.SetFocus(hwnd) +## #win32gui.SetActiveWindow(hwnd) +## # mm the above don;t work, perhaps because the +## # window is actually to be found in another thread +## +## SendKeys.SendKeys("%{F4}") + +if __name__=='__main__': + pass \ No newline at end of file diff --git a/agent/crawl/watsup/performance.py b/agent/crawl/watsup/performance.py new file mode 100644 index 0000000..8417f58 --- /dev/null +++ b/agent/crawl/watsup/performance.py @@ -0,0 +1,181 @@ +# Author : Tim Couper - timc@tizmoi.net +# Date : 22 July 2004 +# Version : 1.0 +# Copyright : Copyright TAC Software Ltd, under Python-style licence. +# Provided as-is, with no warranty. + +from time import time,ctime +import sys +import cPickle + +#------------------------------------------------------------------------------- +# hidden globals + +_PerformanceDataThisRun=None +_PerformanceDataStore=None + +_doPerformanceChecks=True +_recordPerformanceData=False + +#------------------------------------------------------------------------------- +# externals + +def isRecording(): + return _recordPerformanceData + +def isChecking(): + return _doPerformanceChecks + +def doRecording(doIt=True): + global _recordPerformanceData + _recordPerformanceData=doIt + +def doChecking(doIt=True): + global _doPerformanceChecks + _doPerformanceChecks=doIt + +def onOff(bool): + if bool: + return 'On' + else: + return 'Off' + +def getPerformanceDataThisRun(): + "returns the Performance data for this run of the tests" + global _PerformanceDataThisRun + if _PerformanceDataThisRun==None: + # create an instance, and load up what was already there + _PerformanceDataThisRun=PerformanceDataThisRun() + return _PerformanceDataThisRun + +def getPerformanceDataStore(): + "returns the all Performance data for all runs of the tests" + global _PerformanceDataStore + if _PerformanceDataStore==None: + # create an instance, and load up what was already there + _PerformanceDataStore=PerformanceDataStore() + try: + _PerformanceDataStore=cPickle.load(open(_PerformanceDataStore.filename)) + except IOError,EOFError: + pass + return _PerformanceDataStore + +PERFORMANCE_STORE_FILENAME='PerformanceDataStore.dat' + +class PerformanceDataCollectionError(Exception): pass +class PerformanceDataCollection(dict): + def __str__(self): + lines=[] + keys=self.keys() + keys.sort() + for msg in keys: + lines.append(str(msg)) + pdiList=self[msg] + pdiList.sort() + for pdi in pdiList: + lines.append(' %s' % pdi) + return '\n'.join(lines) + +#class PerformanceDataThisRunError(Exception): pass +class PerformanceDataThisRun(PerformanceDataCollection): pass + +class PerformanceDataStoreError(Exception): pass +class PerformanceDataStore(PerformanceDataCollection): + 'performs persistent store of PerformanceDataItems' + def __init__(self, filename=PERFORMANCE_STORE_FILENAME): + self.filename=filename + + def addItem(self,msg,pdi): + 'adds a pdi item to items, in the right place' + # msg must be immutable + if isinstance(pdi,PerformanceDataItem): + if not self.has_key(msg): + self[msg]=[] + self[msg].append(pdi) + else: + e='addItem can only accept PerformanceDataItem instances (%s: %s)' % (msg,str(pdi)) + raise PerformanceDataStoreError,e + +#------------------------------------------------------------------------------- + +class PerformanceCheckError(AssertionError): pass + +class PerformanceCheck(object): + + def __init__(self): + self.reset() + + def reset(self): + self.startTime=time() + + def check(self,msg,maxDelay=None): + res=time()-self.startTime + + # if we're storing the data + if isRecording(): + pdi=PerformanceDataItem(self.startTime,res,maxDelay) + pd=getPerformanceDataThisRun() + if not pd.has_key(msg): + pd[msg]=[] #list of PerformanceData instances, we hope + + # add it to the current data being processed + pd[msg].append(pdi) + + # and add it to the store that we're going to persist + pds=getPerformanceDataStore() + pds.addItem(msg,pdi) + cPickle.dump(pds,open(pds.filename,'w')) + + # if we're acting on performance tests: + if isChecking() and maxDelay<>None and (res>float(maxDelay)): + res=round(res,2) + msg='%s [Delay > %s secs (%s secs)]' % (msg,maxDelay,res) + raise PerformanceCheckError,msg + +class PerformanceDataItem(object): + "holds a result (as elapsed time value in secs) and the time of invocation" + def __init__(self,startTime,elapsedTime,maxDelay=None): + self.startTime=startTime # time of run + self.elapsedTime=round(elapsedTime,2) # elapsed time in secs + self.maxDelay=maxDelay # user defined delay for check + # if None, then there is no upper threshhold + + def __str__(self): + if self.maxDelay==None: + ok='OK' + md='' + else: + if self.elapsedTime<=self.maxDelay: + ok='OK' + else: + ok='Fail' + md= ' (%s)' % self.maxDelay + + return '%s: %s%s %s' % (ctime(self.startTime), self.elapsedTime, md, ok) + + def __cmp__(self,other): + if self.startTimeother.startTime: + return 1 + else: + return 0 + + +def nicePrint(filename=sys.stdout): + def wout(f,text): + f.write('%s\n' % text) + + if filename==sys.stdout: + f=sys.stdout + else: + f=open(filename,'w') + +## from watsup.timedunittest import getPerformanceDataStore,getPerformanceDataThisRun, \ +## isRecording + if isRecording(): + for func in (getPerformanceDataThisRun,getPerformanceDataStore): + fn=func.__name__ + wout(f,'\n%s\n%s\n' % (fn,'-'*len(fn))) + wout(f,str(func())) + diff --git a/agent/crawl/watsup/timedunittest.py b/agent/crawl/watsup/timedunittest.py new file mode 100644 index 0000000..6413fc6 --- /dev/null +++ b/agent/crawl/watsup/timedunittest.py @@ -0,0 +1,279 @@ +# Author : Tim Couper - tim@2wave.net +# Date : 22 July 2004 +# Version : 1.0 +# Copyright : Copyright TAC Software Ltd, under Python-style licence. +# Provided as-is, with no warranty. + +from unittest import TestCase +from time import time,sleep,ctime +import sys + +if sys.platform=='win32': + import win32gui + def pump(): + # clear down any waiting messages (it all helps) + win32gui.PumpWaitingMessages() +else: + pump=None + +###------------------------------------------------------------------------------- +### hidden globals +## +##_PerformanceDataThisRun=None +##_PerformanceDataStore=None +##_doPerformanceChecks=True +##_recordPerformanceData=True +###------------------------------------------------------------------------------- +## +###------------------------------------------------------------------------------- +### externals +## +##def isRecording(): +## return _recordPerformanceData +## +##def isChecking(): +## return _doPerformanceChecks +## +##def doRecording(doIt=True): +## global _recordPerformanceData +## _recordPerformanceData=doIt +## +##def doChecking(doIt=True): +## global _doPerformanceChecks +## _doPerformanceChecks=doIt +## +##def getPerformanceDataThisRun(): +## "returns the Performance data for this run of the tests" +## global _PerformanceDataThisRun +## if _PerformanceDataThisRun==None: +## # create an instance, and load up what was already there +## _PerformanceDataThisRun=PerformanceDataThisRun() +## return _PerformanceDataThisRun +## +##def getPerformanceDataStore(): +## "returns the all Performance data for all runs of the tests" +## global _PerformanceDataStore +## if _PerformanceDataStore==None: +## # create an instance, and load up what was already there +## _PerformanceDataStore=PerformanceDataStore() +## try: +## _PerformanceDataStore=cPickle.load(open(_PerformanceDataStore.filename)) +## except IOError,EOFError: +## pass +## return _PerformanceDataStore +## +##PERFORMANCE_STORE_FILENAME='PerformanceDataStore.dat' +## +##class PerformanceDataCollectionError(Exception): pass +##class PerformanceDataCollection(dict): +## def __str__(self): +## lines=[] +## keys=self.keys() +## keys.sort() +## for msg in keys: +## lines.append(str(msg)) +## pdiList=self[msg] +## pdiList.sort() +## for pdi in pdiList: +## lines.append(' %s' % pdi) +## return '\n'.join(lines) +## +###class PerformanceDataThisRunError(Exception): pass +##class PerformanceDataThisRun(PerformanceDataCollection): pass +## +##class PerformanceDataStoreError(Exception): pass +##class PerformanceDataStore(PerformanceDataCollection): +## 'performs persistent store of PerformanceDataItems' +## def __init__(self, filename=PERFORMANCE_STORE_FILENAME): +## self.filename=filename +## +## def addItem(self,msg,pdi): +## 'adds a pdi item to items, in the right place' +## # msg must be immutable +## if isinstance(pdi,PerformanceDataItem): +## if not self.has_key(msg): +## self[msg]=[] +## self[msg].append(pdi) +## else: +## e='addItem can only accept PerformanceDataItem instances (%s: %s)' % (msg,str(pdi)) +## raise PerformanceDataStoreError,e +## +###------------------------------------------------------------------------------- +## +##class PerformanceCheckError(AssertionError): pass +## +##class PerformanceCheck(object): +## +## def __init__(self): +## self.reset() +## +## def reset(self): +## self.startTime=time() +## +## def check(self,msg,maxDelay=None): +## res=time()-self.startTime +## +## # if we're storing the data +## if isRecording(): +## pdi=PerformanceDataItem(self.startTime,res,maxDelay) +## pd=getPerformanceDataThisRun() +## if not pd.has_key(msg): +## pd[msg]=[] #list of PerformanceData instances, we hope +## +## # add it to the current data being processed +## pd[msg].append(pdi) +## +## # and add it to the store that we're going to persist +## pds=getPerformanceDataStore() +## pds.addItem(msg,pdi) +## cPickle.dump(pds,open(pds.filename,'w')) +## +## # if we're acting on performance tests: +## if isChecking() and maxDelay<>None and (res>float(maxDelay)): +## res=round(res,2) +## msg='%s [Delay > %s secs (%s secs)]' % (msg,maxDelay,res) +## raise PerformanceCheckError,msg +## +##class PerformanceDataItem(object): +## "holds a result (as elapsed time value in secs) and the time of invocation" +## def __init__(self,startTime,elapsedTime,maxDelay=None): +## self.startTime=startTime # time of run +## self.elapsedTime=round(elapsedTime,2) # elapsed time in secs +## self.maxDelay=maxDelay # user defined delay for check +## # if None, then there is no upper threshhold +## +## def __str__(self): +## if self.maxDelay==None: +## ok='OK' +## md='' +## else: +## if self.elapsedTime<=self.maxDelay: +## ok='OK' +## else: +## ok='Fail' +## md= ' (%s)' % self.maxDelay +## +## return '%s: %s%s %s' % (ctime(self.startTime), self.elapsedTime, md, ok) +## +## def __cmp__(self,other): +## if self.startTimeother.startTime: +## return 1 +## else: +## return 0 +## + +maxWaitSecsDefault=3.0 +retrySecsDefault=0.2 + +class TimedOutError(AssertionError): pass + +class TimedTestCase(TestCase): + # extends the functionality of unittest.TestCase to + # allow multiple attempts at assertion, failif, for + # a specific length of time. If the test is not successful by the time + # that Then it fails + + def __init__(self, methodName='runTest',maxWaitSecsDefault=1.0,retrySecsDefault=0.2): + TestCase.__init__(self,methodName) + self.maxWaitSecsDefault=float(maxWaitSecsDefault) + self.retrySecsDefault=float(retrySecsDefault) + + def doTimeTest(self,func,*args): + # remove the last 2 args, as they're out timing values: + maxWaitSecs=args[-2] + retrySecs=args[-1] + args=args[:-2] + + if maxWaitSecs==None: + maxWaitSecs=self.maxWaitSecsDefault + else: + try: + maxWaitSecs=float(maxWaitSecs) + except TypeError,e: + e='%s (maxWaitSecs "%s")' % (e,maxWaitSecs) + raise TypeError,e + + if retrySecs==None: + retrySecs=self.retrySecsDefault + else: + try: + retrySecs=float(retrySecs) + except TypeError,e: + e='%s (retrySecs "%s")' % (e,retrySecs) + raise TypeError,e + retrySecs=max(0,retrySecs-.001) # allow for the pump,etc below + + t=time() + while (time()-t)findAppControls.py example/example1 + +c:>findAppControls.py -v example/example1 + +- this does it in verbose mode +""" + +# Author : Tim Couper - tim@tizmoi.net +# Date : 1 August 2004 +# Copyright : Copyright TAC Software Ltd, under Python-like licence. +# Provided as-is, with no warranty. +# Notes : Requires watsup + + +from optparse import OptionParser +parser = OptionParser() +parser.add_option("-v", "--verbose", + + action="store_true", dest="verbose", default=False, + + help="print verbose messages") + +(options, args) = parser.parse_args() +if len(args)<1: + print 'Usage: findAppControls.py ' + +else: + from watsup.AppControls import AppControls + try: + print 'passing',args[0] + a=AppControls(args[0],verbose=options.verbose) + a.run() + + except: + import sys + print '\n** %s **\n' % sys.exc_info()[1] + import traceback + traceback.print_tb(sys.exc_info()[2]) + diff --git a/agent/crawl/watsup/tools/fmShowWindows.py b/agent/crawl/watsup/tools/fmShowWindows.py new file mode 100644 index 0000000..96b5a8f --- /dev/null +++ b/agent/crawl/watsup/tools/fmShowWindows.py @@ -0,0 +1,127 @@ +#Boa:Frame:ShowWindows + +# Author : Tim Couper - tim@tizmoi.net +# Date : 1 August 2004 +# Copyright : Copyright TAC Software Ltd, under Python-like licence. +# Provided as-is, with no warranty. +# Notes : Requires watsup, wxPython + +from wxPython.wx import * +from wxPython.lib.anchors import LayoutAnchors + +from watsup.tools.showWindows import findAll,findNew,readPickle +from watsup.winGuiAuto import dumpWindow +from watsup.utils import tupleHwnd +import pprint + +def create(parent): + return ShowWindows(parent) + +[wxID_SHOWWINDOWS, wxID_SHOWWINDOWSFINDNEW, wxID_SHOWWINDOWSNEWWINDOWS, + wxID_SHOWWINDOWSPANEL1, wxID_SHOWWINDOWSREGISTER, wxID_SHOWWINDOWSREGISTERED, + wxID_SHOWWINDOWSTEXT, +] = map(lambda _init_ctrls: wxNewId(), range(7)) + +class ShowWindows(wxFrame): + def _init_ctrls(self, prnt): + # generated method, don't edit + wxFrame.__init__(self, id=wxID_SHOWWINDOWS, name='ShowWindows', + parent=prnt, pos=wxPoint(424, 184), size=wxSize(456, 433), + style=wxMINIMIZE_BOX | wxSTATIC_BORDER | wxCAPTION | wxSYSTEM_MENU, + title='ShowWindows 1.0') + self.SetClientSize(wxSize(448, 399)) + self.SetToolTipString('ShowWindow') + self.Center(wxBOTH) + self.Enable(True) + self.SetSizeHints(-1, -1, -1, -1) + self.SetThemeEnabled(False) + + self.panel1 = wxPanel(id=wxID_SHOWWINDOWSPANEL1, name='panel1', + parent=self, pos=wxPoint(0, 350), size=wxSize(450, 50), + style=wxTAB_TRAVERSAL) + self.panel1.SetConstraints(LayoutAnchors(self.panel1, True, True, False, + False)) + + self.Register = wxButton(id=wxID_SHOWWINDOWSREGISTER, label='Register', + name='Register', parent=self.panel1, pos=wxPoint(32, 13), + size=wxSize(75, 23), style=0) + self.Register.SetToolTipString('Register all windows info') + EVT_BUTTON(self.Register, wxID_SHOWWINDOWSREGISTER, + self.OnRegisterButton) + + self.FindNew = wxButton(id=wxID_SHOWWINDOWSFINDNEW, label='Find New', + name='FindNew', parent=self.panel1, pos=wxPoint(304, 13), + size=wxSize(75, 23), style=0) + EVT_BUTTON(self.FindNew, wxID_SHOWWINDOWSFINDNEW, self.OnFindNewButton) + + self.Text = wxTextCtrl(id=wxID_SHOWWINDOWSTEXT, name='Text', + parent=self, pos=wxPoint(0, 0), size=wxSize(450, 350), + style=wxRAISED_BORDER | wxTE_WORDWRAP | wxTE_MULTILINE, value='') + + self.Registered = wxTextCtrl(id=wxID_SHOWWINDOWSREGISTERED, + name='Registered', parent=self.panel1, pos=wxPoint(110, 16), + size=wxSize(40, 16), style=wxTE_CENTER | wxTE_READONLY, value='') + self.Registered.SetToolTipString('No of windows registered on system') + self.Registered.SetBackgroundColour(wxColour(175, 175, 175)) + + self.NewWindows = wxTextCtrl(id=wxID_SHOWWINDOWSNEWWINDOWS, + name='NewWindows', parent=self.panel1, pos=wxPoint(382, 16), + size=wxSize(40, 16), style=wxTE_CENTER | wxTE_READONLY, value='') + self.NewWindows.SetToolTipString('No of new windows found') + self.NewWindows.SetBackgroundColour(wxColour(175, 175, 175)) + + def __init__(self, parent): + self._init_ctrls(parent) + #load up the last value for your info + ws=readPickle() + if ws: + self.Registered.SetValue(str(len(ws))) + + def OnRegisterButton(self, event): + ws=findAll() + self.Registered.SetBackgroundColour(wxColour(255, 255, 255)) + self.Registered.SetValue(str(len(ws))) + + def OnFindNewButton(self, event): + ws=findNew() + self.NewWindows.SetBackgroundColour(wxColour(255, 255, 255)) + # write the details to the text box + withControls=self.writeNewWindows(ws) + self.NewWindows.SetValue('%s/%d' % (withControls,len(ws))) + + def writeNewWindows(self,ws): + noControls=0 + withControls=0 + txt=[] + for w in ws: + + dw=dumpWindow(w) + if not dw: + noControls+=1 + continue + + t=tupleHwnd(w) + # don't bother with ShowWindows application: + + wclass=t[2] + wtext=t[1] + if wclass=='wxWindowClass' and wtext.startswith('ShowWindows'): + noControls+=1 + continue + + if wtext: + wtext="%s" % wtext + else: + wtext='' + + withControls+=1 + # write the heading window + #txt.append('%d. %s %s' % (withControls,wclass,wtext)) + txt.append('%d. %s' % (withControls,str(list(t)))) + txt.append(pprint.pformat(dw)) + txt.append('') + + self.Text.SetValue('\n'.join(txt)) + + return withControls + diff --git a/agent/crawl/watsup/tools/runTests.ini b/agent/crawl/watsup/tools/runTests.ini new file mode 100644 index 0000000..a528579 --- /dev/null +++ b/agent/crawl/watsup/tools/runTests.ini @@ -0,0 +1,4 @@ +[main] +checking = True +current_directory = C:\Python23\Lib\site-packages\watsup\examples\unittests + diff --git a/agent/crawl/watsup/tools/showWindows.py b/agent/crawl/watsup/tools/showWindows.py new file mode 100644 index 0000000..1aafe9d --- /dev/null +++ b/agent/crawl/watsup/tools/showWindows.py @@ -0,0 +1,34 @@ +# logic for wxShowWindows + +# Author : Tim Couper - timc@tizmoi.net +# Date : 1 August 2004 +# Copyright : Copyright TAC Software Ltd, under Python-like licence. +# Provided as-is, with no warranty. +# Notes : Requires watsup + +import cPickle +from watsup.AppControls import findNewTopWindows +from watsup.winGuiAuto import dumpWindow +from watsup.utils import dumpHwnd +import os.path +from types import ListType + +PICKLE_FILE='findall.pkl' + +def readPickle(pickle_file=PICKLE_FILE): + #reads the list in the pickle file if possible + if os.path.exists(pickle_file): + return cPickle.load(open(pickle_file)) + else: + return [] + +def findAll(pickle_file=PICKLE_FILE): + # get all the top windows: + res=findNewTopWindows() + cPickle.dump(res,open(pickle_file,'w')) + return res + +def findNew(pickle_file=PICKLE_FILE): + # get all the top windows, and return any new ones + olds=readPickle(pickle_file) + return findNewTopWindows(olds) diff --git a/agent/crawl/watsup/tools/watsup_framework.bat b/agent/crawl/watsup/tools/watsup_framework.bat new file mode 100644 index 0000000..2ffdf75 --- /dev/null +++ b/agent/crawl/watsup/tools/watsup_framework.bat @@ -0,0 +1 @@ +python watsup_framework.py \ No newline at end of file diff --git a/agent/crawl/watsup/tools/watsup_framework.py b/agent/crawl/watsup/tools/watsup_framework.py new file mode 100644 index 0000000..f6ff39f --- /dev/null +++ b/agent/crawl/watsup/tools/watsup_framework.py @@ -0,0 +1,653 @@ + +# Author : Tim Couper - timc@tizmoi.net +# Date : 1 August 2004 +# Copyright : Copyright TAC Software Ltd, under Python-like licence. +# Provided as-is, with no warranty. +# Notes : Requires wxPython,watsup +# Based heavily on unittestgui.py by Chris Liechti + +import unittest, os, imp, time, traceback,sys + +## import all of the wxPython GUI package +from wxPython.wx import * +from wxPython.grid import * + +from watsup.performance import doChecking,onOff,isChecking +from watsup.tools.buildSuite import buildSuite +from time import ctime + +BASE_TITLE="Watsup Framework" +INI_FILE='./runTests.ini' + +from ConfigParser import ConfigParser,NoOptionError, NoSectionError + +#specify editor, make it return immediately, otherwise the GUI will wait +#until its closed. on win use "start cmd", on un*x "cmd&" +EDITOR = r'start c:\tools\wscite\scite "%(filename)s" -goto:%(lineno)s' +def starteditor(filename, lineno=1): + if os.path.exists(filename): + os.system(EDITOR % {'filename':filename, 'lineno':lineno}) + else: + wxMessageBox("Cannot locate sourcefile:\n%s" % filename, "Can't start editor...", wxOK) + +#---------------------------------------------------------------- + +class GUITestResult(unittest.TestResult): + """A test result class that can print formatted text results to a stream. + """ + separator1 = '=' * 70 + separator2 = '-' * 70 + + def __init__(self, listview, progress, descriptions, verbosity): + unittest.TestResult.__init__(self) + self.listview = listview + self.showAll = verbosity > 1 + self.descriptions = descriptions + self.progress = progress + self.testdescr = None + self.stream = sys.stdout + + def getDescription(self, test): + if self.descriptions: + return test.shortDescription() or str(test) + else: + return str(test) + + def startTest(self, test): + unittest.TestResult.startTest(self, test) + self.testdescr = self.getDescription(test) + if self.showAll: + self.stream.write(self.getDescription(test)) + self.stream.write(" ... ") + + def _correctFilename(self, filename): + if filename[-4:] in ('.pyc', '.pyo'): + return filename[:-1] + return filename + + def addSuccess(self, test): + unittest.TestResult.addSuccess(self, test) + if self.showAll: + self.stream.write("ok\n") + self.listview.Append( ( + 'Ok', + self.testdescr, + '', + None, + None + ) ) + self.progress.tick() + + def addError(self, test, err): + unittest.TestResult.addError(self, test, err) + if self.showAll: + self.stream.write("ERROR\n") + try: + filename = self._correctFilename(err[2].tb_frame.f_globals['__file__']) + except KeyError: + filename = None + lineno = err[2].tb_lineno + self.listview.Append( ( + 'Error', + self.testdescr, + traceback.format_exception(*err)[-1].rstrip(), + self._exc_info_to_string(err), + (filename, lineno) + ) ) + self.progress.tick() + + def addFailure(self, test, err): + unittest.TestResult.addFailure(self, test, err) + if self.showAll: + self.stream.write("FAIL\n") + try: + filename = self._correctFilename(err[2].tb_frame.f_globals['__file__']) + except KeyError: + filename = None + lineno = err[2].tb_lineno + self.listview.Append( ( + 'Fail', + self.testdescr, + traceback.format_exception(*err)[-1].rstrip(), + self._exc_info_to_string(err), + (filename, lineno) + ) ) + self.progress.tick() + + def printErrors(self): + if self.showAll: + self.stream.write("\n") + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + def printErrorList(self, flavour, errors): + for test, err in errors: + self.stream.write(self.separator1) + self.stream.write("\n%s: %s\n" % (flavour,self.getDescription(test))) + self.stream.write(self.separator2) + self.stream.write("\n%s\n" % err) + +class GUITestRunner: + """A test runner class that displays results in textual form. + + It prints out the names of tests as they are run, errors as they + occur, and a summary of the results at the end of the test run. + """ + def __init__(self, listview, progress, stream=sys.stderr, descriptions=1, verbosity=2): + self.listview = listview + self.progress = progress + self.stream = unittest._WritelnDecorator(stream) + self.descriptions = descriptions + self.verbosity = verbosity + + def _makeResult(self): + return GUITestResult(self.listview, self.progress, self.descriptions, self.verbosity) + + def run(self, test): + "Run the given test case or test suite." + result = self._makeResult() + startTime = time.time() + test(result) + stopTime = time.time() + timeTaken = float(stopTime - startTime) + result.printErrors() + self.stream.writeln(result.separator2) + run = result.testsRun + self.stream.writeln("Ran %d test%s in %.3fs" % + (run, run == 1 and "" or "s", timeTaken)) + self.stream.writeln() + if not result.wasSuccessful(): + self.stream.write("FAILED (") + failed, errored = map(len, (result.failures, result.errors)) + if failed: + self.stream.write("failures=%d" % failed) + if errored: + if failed: self.stream.write(", ") + self.stream.write("errors=%d" % errored) + self.stream.writeln(")") + else: + self.stream.writeln("OK") + + return result + +#---------------------------------------------------------------- + +def lastline(t): + t = t.rstrip() + n = t.rfind('\n') + if n >= 0: + return t[n+1:] + return t + +#---------------------------------------------------------------- + +class ResultView(wxListCtrl): + PM_DETAIL = wxNewId() + PM_LOCATE = wxNewId() + + def __init__(self, parent): + wxListCtrl.__init__(self, parent, -1, + style=wxLC_REPORT|wxSUNKEN_BORDER|wxLC_VIRTUAL|wxLC_VRULES,#|wxLC_HRULES, + size=(500,200)) + + self.InsertColumn(0, '-Ok-') + self.InsertColumn(1, 'Description') + self.InsertColumn(2, 'Details') + + self.SetColumnWidth(0, 40) + w = self.GetSize()[0] - self.GetColumnWidth(0) + self.SetColumnWidth(1, w/2) + self.SetColumnWidth(2, w/2) + + EVT_RIGHT_DOWN(self, self.OnRightClick) + EVT_LIST_ITEM_SELECTED(self, self.GetId(), self.OnItemSelected) + EVT_LIST_ITEM_ACTIVATED(self, self.GetId(), self.OnItemActivated) + + self.red = wxListItemAttr() + self.red.SetBackgroundColour(wxRED) + self.green = wxListItemAttr() + self.green.SetBackgroundColour(wxColour(200,255,200)) #light green + self.orange = wxListItemAttr() + self.orange.SetBackgroundColour(wxColour(255,200,200)) #light red + self.result = [] + self.currentItem = None + + EVT_SIZE(self, self.OnSize) + + #node menu + self.nodemenu = wxMenu() + self.nodemenu.Append(self.PM_DETAIL, "Show Details") + #self.nodemenu.AppendSeparator() + #self.nodemenu.Append(self.PM_LOCATE, "Locate Source") + + EVT_MENU(self, self.PM_DETAIL, self.OnDetail) + #EVT_MENU(self, self.PM_LOCATE, self.OnLocate) + + def OnDetail(self, event=None): + if self.result[self.currentItem][3]: + wxMessageBox(self.result[self.currentItem][3], "Result details", wxOK) + + def OnLocate(self, event=None): + filename, lineno = self.result[self.currentItem][4] + print "locate",filename, lineno + starteditor(filename, lineno) + + def OnSize(self, event): + w = self.GetSize()[0] - self.GetColumnWidth(0) - 30 + if w < 180: w = 180 + self.SetColumnWidth(1, w/2) + self.SetColumnWidth(2, w/2) + + def Append(self, item): + self.result.append(item) + self.SetItemCount(len(self.result)) + #wxYield() + #self.Refresh() + + def DeleteAllItems(self): + self.result = [] + wxListCtrl.DeleteAllItems(self) + + def OnItemSelected(self, event): + self.currentItem = event.m_itemIndex + + def OnItemActivated(self, event): + self.currentItem = event.m_itemIndex + self.OnDetail() + + def OnRightClick(self, event): + pt = event.GetPosition() + item, flags = self.HitTest(pt) + if not flags & wxLIST_HITTEST_NOWHERE: + if self.currentItem is not None: + self.SetItemState(self.currentItem, 0, wxLIST_STATE_SELECTED ) + self.currentItem = item + self.SetItemState(item, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED ) + if self.result[self.currentItem][3]: + self.PopupMenu(self.nodemenu, pt) #display popup menu + + def OnCopyAsText(self, all=0): + res = [] + item = -1; + while 1: + if all: + item = self.GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_DONTCARE); + else: + item = self.GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); + text = '\t'.join(map(str, self.result[item])) + res.append(text) + if item == -1: + break + clip = wxClipboard() + clip.Open() + clip.SetData( wxTextDataObject('\n'.join(res)) ) + clip.Close() + + #--------------------------------------------------- + # These methods are callbacks for implementing the + # "virtualness" of the list... + def OnGetItemText(self, row, col): + #print row, col + try: + return str(self.result[row][col]) + except IndexError: + return '' + + def OnGetItemImage(self, item): + return 0 + + def OnGetItemAttr(self, item): + if self.result[item][0] == 'Error': + return self.red + elif self.result[item][0] == 'Fail': + return self.orange + elif self.result[item][0] == 'Ok': + return self.green + return None + +class TreeView(wxTreeCtrl): + PM_RUNSEL = wxNewId() + PM_LOCATE = wxNewId() + + def __init__(self, *args, **kwargs): + wxTreeCtrl.__init__(self, *args, **kwargs) + EVT_LEFT_DCLICK(self, self.OnLeftDClick) + EVT_RIGHT_DOWN(self, self.OnRightClick) + EVT_RIGHT_UP(self, self.OnRightUp) + EVT_TREE_BEGIN_LABEL_EDIT(self, self.GetId(), self.OnBeginEdit) + EVT_TREE_END_LABEL_EDIT (self, self.GetId(), self.OnEndEdit) + + #node menu +## self.nodemenu = wxMenu() +## self.nodemenu.Append(self.PM_RUNSEL, "Run selected") +## self.nodemenu.AppendSeparator() +## self.nodemenu.Append(self.PM_LOCATE, "Locate Source") +## +## EVT_MENU(self, self.PM_RUNSEL, self.OnRunSel) +## EVT_MENU(self, self.PM_LOCATE, self.OnLocate) + + #don't allow editing of node names + def OnBeginEdit(self, event): event.Veto() + def OnEndEdit(self, event): event.Veto() + + def OnLeftDClick(self, event): +## pt = event.GetPosition(); +## item, flags = self.HitTest(pt) +## #print("OnLeftDClick: %s" % self.GetItemText(item)) +## parent = self.GetItemParent(item) +## #self.SortChildren(parent) + event.Skip() + + def OnRightClick(self, event): + pt = event.GetPosition() + item, flags = self.HitTest(pt) + if not flags & wxTREE_HITTEST_NOWHERE: + self.SelectItem(item) + #self.PopupMenu(self.nodemenu, pt) #display node menu + + def OnRightUp(self, event): + pt = event.GetPosition(); + item, flags = self.HitTest(pt) + #self.tree.EditLabel(item) + + def OnCompareItems(self, item1, item2): + t1 = self.GetItemText(item1) + t2 = self.GetItemText(item2) + #print('compare: ' + t1 + ' <> ' + t2) + return cmp(t1,t2) + + def OnRunSel(self, event): + pass + def OnLocate(self, event): + pass + +#---------------------------------------------------------------- + +class ProgressBar(wxGauge): + def __init__(self, *args, **kwargs): + wxGauge.__init__(self, *args, **kwargs) + self.SetBezelFace(5) + self.SetShadowWidth(5) + self.increment = 1 + + def tick(self): + self.SetValue(self.GetValue() + self.increment) + +#---------------------------------------------------------------- + +class Frame(wxFrame): + ID_BUTTON = wxNewId() + #M_NEW = wxNewId() + M_OPENFILES = wxNewId() + M_OPENDIR = wxNewId() + M_OPENTREE = wxNewId() + M_EXIT = wxNewId() + M_COPYALL = wxNewId() + M_COPYSEL = wxNewId() + M_RUN = wxNewId() + M_CHECKING = wxNewId() + + + def __init__(self, parent, id, title = BASE_TITLE): + + # First, call the base class' __init__ method to create the frame + wxFrame.__init__(self, parent, id, title, wxPoint(100, 100), wxSize(100, 100)) + #variables + self.suite = None + + self.loadConfig() + + #menu + menuBar = wxMenuBar() + menu = wxMenu() + #menu.Append(self.M_NEW, "&New") + menu.Append(self.M_OPENFILES, "&Load Test File(s)") + menu.Append(self.M_OPENDIR, "&Load Test Directory") + menu.Append(self.M_OPENTREE, "&Load Test Directory Tree") + menu.AppendSeparator() + menu.Append(self.M_EXIT, "E&xit") + #EVT_MENU(self, self.M_NEW, self.OnNew) + EVT_MENU(self, self.M_OPENFILES, self.OnMenuOpenFiles) + EVT_MENU(self, self.M_OPENDIR, self.OnMenuOpenDir) + EVT_MENU(self, self.M_OPENTREE, self.OnMenuOpenTree) + EVT_MENU(self, self.M_EXIT, self.OnCloseWindow) + menuBar.Append(menu, "&File"); + + menu = wxMenu() + menu.Append(self.M_COPYALL, "&Copy all results") + menu.Append(self.M_COPYSEL, "&Copy selected results") + EVT_MENU(self, self.M_COPYALL, self.OnCopyAll) + EVT_MENU(self, self.M_COPYSEL, self.OnCopySelection) + menuBar.Append(menu, "&Edit"); + + menu = wxMenu() + menu.Append(self.M_RUN, "&Run all") + menu.AppendSeparator() + menu.AppendCheckItem(self.M_CHECKING,"&Checking") + menu.Check(self.M_CHECKING,isChecking()) + EVT_MENU(self, self.M_RUN, self.OnRun) + EVT_MENU(self, self.M_CHECKING, self.OnChecking) + self.testing_menu=menu + menuBar.Append(menu, "&Testing") + + self.SetMenuBar(menuBar) + + #GUI + panel = wxPanel(self, 0) + + button = wxButton(panel, self.ID_BUTTON, "Run Tests" ) + button.SetDefault() + EVT_BUTTON(panel, self.ID_BUTTON, self.OnRun ) + + self.tree = TreeView(panel, -1, size=(300,250), style=wxTR_HAS_BUTTONS | wxTR_EDIT_LABELS)# | wxTR_MULTIPLE + + self.progress = ProgressBar(panel, -1, 100, style=wxGA_HORIZONTAL|wxGA_SMOOTH) + + self.list = ResultView(panel) + + sizer = wxBoxSizer(wxVERTICAL) + sizer.Add(self.tree, 1, wxEXPAND) + sizer.Add(button, 0, wxEXPAND) + sizer.Add(self.progress, 0, wxEXPAND) + sizer.Add(self.list, 1, wxEXPAND) + + panel.SetAutoLayout(1) + panel.SetSizer(sizer) + sizer.Fit(panel) + + basesizer = wxBoxSizer(wxVERTICAL) + basesizer.Add(panel, 1, wxEXPAND) + self.SetAutoLayout(1) + self.SetSizer(basesizer) + self.Fit() + + #create a statusbar + sb = self.CreateStatusBar(1) + sb.SetStatusWidths([-1]) + self.SetStatusText('Please load file(s), a directory or tree',0) + #update controls + self.OnNew() + + def __del__(self): + # persist any values in .ini file + + cfg=ConfigParser() + cfg.add_section('main') + cfg.set('main','current_directory',self.getCurrentDirectory()) + cfg.set('main','checking',isChecking()) + cfg.write(open(INI_FILE,'w')) + + def loadConfig(self): + # load settings from the config file + + cfg=ConfigParser() + cfg.read(INI_FILE) + + self._currentDirectory='.' + try: + self.setCurrentDirectory(cfg.get('main','current_directory')) + except NoSectionError: # none of the other options will be available + return + except NoOptionError: + pass + + try: + doChecking(cfg.getboolean('main','checking')) + except (NoOptionError, NoSectionError): + doChecking(True) + + + def UpdateTree(self, root=None, testlist=None): + if root is None: + root = self.root + if testlist is None and self.suite: + testlist = self.suite._tests #grmpf accessing a _ member, oh well + + if root and testlist: + for testcase in testlist: + if isinstance(testcase, unittest.TestSuite): + + testname=testcase._tests[0] + #str(testname): atestcasename (module.class) + # we want module.class here, with no brackets + # this should do it + label= str(testname).split('(')[1] + label=label[:-1] + + child = self.tree.AppendItem(root, "%s (%d)" % (label,len(testcase._tests))) + self.tree.SetPyData(child, None) + self.UpdateTree(child, testcase._tests) + self.tree.SortChildren(child) + self.tree.Collapse(child) + else: + + label="%s" % testcase + label=label.split('(')[0] + + child = self.tree.AppendItem(root,label) + self.tree.SetPyData(child, None) + ##self.tree.SetItemImage(child, idx2, wxTreeItemIcon_Expanded) + ##self.tree.SetItemSelectedImage(child, idx3) + + self.tree.Expand(root) + + def OnRun(self, event=None): + """ """ + if self.suite: + runner = GUITestRunner(self.list, self.progress) + self.SetStatusText('Running tests...',0) + ln = self.suite.countTestCases() + self.progress.SetValue(0) + self.progress.SetRange(ln) + self.list.DeleteAllItems() + + result = runner.run(self.suite) + + self.SetStatusText('Ran %d tests, %d errors, %d failures' % (ln, len(result.errors), len(result.failures)), 0) + + else: + self.SetStatusText('No tests found', 0) + + + def OnMenuOpenFiles(self, event=None): + """ """ + + dlg = wxFileDialog(self, "Choose one or more modules", + self.getCurrentDirectory(),"", + 'Python files (*.py)|*.py|All files (*.*)|*.*', + wxOPEN|wxMULTIPLE) + + if dlg.ShowModal() == wxID_OK: +## for path in dlg.GetPaths(): +## log.WriteText('You selected: %s\n' % path) + self.OnNew() + filenames = dlg.GetPaths() #dlg.GetPath() + self.setCurrentDirectory(dlg.GetDirectory()) + self.suite=buildSuite(filenames) + + #print self.suite + dlg.Destroy() + self.UpdateTree() + + def OnMenuOpenDir(self,event=None): + self.loadDirectory('Choose the test directory') + + def OnMenuOpenTree(self,event=None): + self.loadDirectory('Choose the top test directory',True) + + def loadDirectory(self,msg,tree=False): + dlg=wxDirDialog(self,msg, self.getCurrentDirectory()) + if dlg.ShowModal() == wxID_OK: + self.OnNew() + dir = dlg.GetPath() + self.setCurrentDirectory(dir) + self.suite=buildSuite(dir,tree) + #print self.suite + dlg.Destroy() + self.UpdateTree() + + def OnNew(self, event=None): + self.list.DeleteAllItems() + self.tree.DeleteAllItems() + self.root = self.tree.AddRoot("Watsup Tests:") + self.tree.SetPyData(self.root, None) + self.progress.SetValue(0) + self.suite = None + #self.filename = None + + def OnCopySelection(self,event=None): + self.list.OnCopyAsText() + + def OnCopyAll(self,event=None): + self.list.OnCopyAsText(all=1) + + def OnCloseWindow(self, event=None): + self.Destroy() + + + def OnChecking(self,event=None): + # toggle self._checking: + bool=not isChecking() + self.testing_menu.Check(self.M_CHECKING,bool) + doChecking(bool) + self.SetStatusText('Checking %s' % onOff(bool),0) + + def getCurrentDirectory(self): + if os.path.isdir(self._currentDirectory): + return self._currentDirectory + else: + return '.' + + def setCurrentDirectory(self,value): + if self._currentDirectory<>value: + if os.path.isdir(value): + self._currentDirectory=value + # update the title + if value=='.': + text='' + else: + text='(%s)' % value + + self.SetTitle('%s %s'.strip() % (BASE_TITLE,text)) + else: + self._currentDirectory='.' + + +#--------------------------------------------------------------------------- +# Every wxWindows application must have a class derived from wxApp +class App(wxApp): + # wxWindows calls this method to initialize the application + def OnInit(self): + # Create an instance of our customized Frame class + frame = Frame(NULL, -1) + frame.Show(true) + # Tell wxWindows that this is our main window + self.SetTopWindow(frame) + # Return a success flag + return true + +#--------------------------------------------------------------------------- +if __name__ == "__main__": + app = App(0) # Create an instance of the application class + app.MainLoop() # Tell it to start processing events + diff --git a/agent/crawl/watsup/tools/wxShowWindows.py b/agent/crawl/watsup/tools/wxShowWindows.py new file mode 100644 index 0000000..2ec2110 --- /dev/null +++ b/agent/crawl/watsup/tools/wxShowWindows.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +#Boa:App:BoaApp + +# Author : Tim Couper - tim@tizmoi.net +# Date : 1 August 2004 +# Copyright : Copyright TAC Software Ltd, under Python-like licence. +# Provided as-is, with no warranty. +# Notes : Requires watsup,wxPython + +from wxPython.wx import * + +import fmShowWindows + +modules ={'fmShowWindows': [1, 'Main frame of Application', 'fmShowWindows.py']} + +class BoaApp(wxApp): + def OnInit(self): + wxInitAllImageHandlers() + self.main = fmShowWindows.create(None) + self.main.Show() + self.SetTopWindow(self.main) + return True + +def main(): + application = BoaApp(0) + application.MainLoop() + +if __name__ == '__main__': + main() diff --git a/agent/crawl/watsup/utils.py b/agent/crawl/watsup/utils.py new file mode 100644 index 0000000..79c502a --- /dev/null +++ b/agent/crawl/watsup/utils.py @@ -0,0 +1,37 @@ +""" watsup system utilities +""" + +# Author : Tim Couper - timc@tizmoi.net +# Date : 22 July 2004 +# Version : 1.0 +# Copyright : Copyright TAC Software Ltd, under Python-style licence. +# Provided as-is, with no warranty. +# Notes : Requires win32all + +import win32gui +import win32api +import sys + +class WatsupError(Exception): pass + +##def kill(pid): + +## """kill function for Win32""" + +## handle = win32api.OpenProcess(1, 0, pid) + +## return (0 != win32api.TerminateProcess(handle, 0)) + +def dumpHwnd(hwnd): + t=list(tupleHwnd(hwnd)) + t.reverse() + return '%s:"%s" (%d)' % tuple(t) #(win32gui.GetClassName(hwnd),win32gui.GetWindowText(hwnd),hwnd) + +def tupleHwnd(hwnd): + return (hwnd,win32gui.GetWindowText(hwnd),win32gui.GetClassName(hwnd)) + +def pump(): + win32gui.PumpWaitingMessages() + +if __name__=='__main__': + pass \ No newline at end of file diff --git a/agent/crawl/watsup/wie.py b/agent/crawl/watsup/wie.py new file mode 100644 index 0000000..8f5cf37 --- /dev/null +++ b/agent/crawl/watsup/wie.py @@ -0,0 +1,45 @@ +# provides a testing interface to web applications via the PAMIE module + +# Author : Tim Couper - timc@tizmoi.net +# Date : 22 July 2004 +# Version : 1.0 +# Copyright : Copyright TAC Software Ltd, under Python-style licence. +# Provided as-is, with no warranty. + +from cPAMIE import PAMIE + +def findRunningIE(): + from win32com.client import Dispatch + from win32gui import GetClassName + + ShellWindowsCLSID = '{9BA05972-F6A8-11CF-A442-00A0C90A8F39}' + ShellWindows = Dispatch ( ShellWindowsCLSID ) + + # try to get an ie instance from the window + for shellwindow in ShellWindows : + if GetClassName ( shellwindow.HWND ) == 'IEFrame' : + return shellwindow + +class WatsupIE(PAMIE): + def __init__(self,url=None, timeOut=1000, useExistingIfPossible=False): + + self._ie=None + + if useExistingIfPossible: + self._ie=findRunningIE() + + if self._ie: + # this case can only arise if we've located a running IE; + + # the code below should be everything else in PAMIE.__init__, + # apart from instantiation of the new ie instance: + if url: + self._ie.Navigate(url) + else: + self._ie.Navigate('about:blank') + self._timeOut = timeOut + self._ie.Visible = 1 + else: + PAMIE.__init__(self,url,timeOut) + + \ No newline at end of file diff --git a/agent/crawl/watsup/winGuiAuto.py b/agent/crawl/watsup/winGuiAuto.py new file mode 100644 index 0000000..9ae3f4c --- /dev/null +++ b/agent/crawl/watsup/winGuiAuto.py @@ -0,0 +1,1169 @@ +# Module : winGuiAuto.py +# Synopsis : Windows GUI automation utilities +# Programmer : Simon Brunning - simon@brunningonline.net +# Date : 25 June 2003 +# Version : 1.0 +# Copyright : Released to the public domain. Provided as-is, with no warranty. +# Notes : Requires Python 2.3, win32all and ctypes + +# Modifications by Tim Couper - tim@tizmoi.net +# 22 Jul 2004 +# findControls: deduplicates the list to be returned +# findControl: handles win32gui.error from initial call to findControls +# getMenuInfo: improved algorithm for calculating no. of items in a menu +# activateMenuItem: improved algorithm for calculating no. of items in a menu +# +# GLOBALLY corrected spelling: seperator -> separator +# : descendent -> descendant +# added findMenuItem + +'''Windows GUI automation utilities. + +TODO - Until I get around to writing some docs and examples, the tests at the +foot of this module should serve to get you started. + +The standard pattern of usage of winGuiAuto is in three stages; identify a +window, identify a control in the window, trigger an action on the control. + +The end result is always that you wish to have an effect upon some Windows GUI +control. + +The first stage is to identify the window within which the control can be +found. To do this, you can use either findTopWindow or findTopWindows. The +findTopWindow function returns a reference to single window, or throws an +exception if multiple windows or no windows matching the supplied selection +criteria are found. If no matching window is found, immediately, the +findTopWindow function may keep trying to find a match, depending upon suppled +retry aguments. The findTopWindows function returns a list of references to all +windows matching the supplied selection criteria; this list may be empty. The +findTopWindows function will return immediately. + +Usually, specifying caption text, the window's class, or both, will be +sufficient to identify the required window. (Note that the window's class is a +Windows API concept; it has nothing to do with Python classes. See http://msdn.microsoft.com/library/en-us/winui/WinUI/WindowsUserInterface/Windowing/Window_89WindowClasse.asp +for an overview.) Caption text will match if the specified text is *contained* +in the windows' captions; this match is case unsensitive. Window class names +must match identically; the full class name must be matched, and the test is +case sensitive. + +This matching behavior is usually the most useful - but not always. If other +selection behavior is required, a selection function can be passed to the find +functions. + +These returned references are in the form of 'Windows Handles'. All windows and +controls are accessed via a Windows Handle (or hwnd). See +http://msdn.microsoft.com/library/en-us/winui/winui/windowsuserinterface/windowing/windows/aboutwindows.asp +for Microsoft's info-deluge on all things Windows window. + +Having identified the window, the next stage is to identify the control. To do +this, you can use either of the findControl and findControls functions. These +work in almost exactly the same way as the findTopWindow and findTopWindows +functions; they take the hwnd of the top level window within which the required +control should be found, and otherwise take the same arguments as the +findTopWindow and findTopWindows functions, and work in the same ways. + +Having now identified your control, you can now query its state, and send +actions (in the form of Windows messages) to it. It's at this point that you +are most likely to end up having to extend winGuiAuto in some way; the +developer of the GUI that you are trying to drive is free to develop controls +which respond to different messages in different ways. Still, if standard MFC +controls have been used, you may find that winGuiAuto's functions may provide +much or all of what you need - or at least provide a useful template. These +functions (clickButton, clickStatic, doubleClickStatic, getComboboxItems, +getEditText, getListboxItems, selectComboboxItem, selectListboxItem and +setEditText) should be fairly self evident. + +Selection of menu items is a slightly different process: but don't fret; it's +pretty simple. You'll need to identify the hwnd of the top level window that +you want to control using either of the findTopWindow and findTopWindows +functions. You can then call the activateMenuItem function, passing in the top +level window's hwnd and the path to the menu option that you wish to activate. +This path should consist of a list specifiying the path to the required menu +option - see activateMenuItem's docstring for details. + +TODO getMenuInfo + +TODO: dumpTopWindows, dumpWindow, Spy++ and Winspector (http://www.windows-spy.com) +Two if the main difficulties you'll come across while using winGuiAuto will be discovering + the classes of the windows that you want, and where the provided +''' + +import array +import ctypes +import os +import struct +import sys +import time +import win32api +import win32con +import win32gui + +def findTopWindow(wantedText=None, + wantedClass=None, + selectionFunction=None, + maxWait=1, + retryInterval=0.1): + '''Find the hwnd of a top level window. + + You can identify windows using captions, classes, a custom selection + function, or any combination of these. (Multiple selection criteria are + ANDed. If this isn't what's wanted, use a selection function.) + + If no window matching the specified selection criteria is found + immediately, further attempts will be made. The retry interval and maximum + time to wait for a matching window can be specified. + + Arguments: + + wantedText Text which the required window's caption must + *contain*. (Case insensitive match.) + wantedClass Class to which the required window must belong. + selectionFunction Window selection function. Reference to a function + should be passed here. The function should take hwnd + as an argument, and should return True when passed the + hwnd of a desired window. + maxWait The maximum time to wait for a matching window, in + seconds. + retryInterval How frequently to look for a matching window, in + seconds + + Raises: + WinGuiAutoError When no window or multiple windows found. + + Usage examples: # Caption contains "Options" + optDialog = findTopWindow(wantedText="Options") + # Caption *equals* "Notepad" + saveDialog = findTopWindow(selectionFunction=lambda hwnd: + win32gui.GetWindowText(hwnd)=='Notepad') + ''' + topWindows = findTopWindows(wantedText, wantedClass, selectionFunction) + if len(topWindows) > 1: + raise WinGuiAutoError("Multiple top level windows found for wantedText=" + + repr(wantedText) + + ", wantedClass=" + + repr(wantedClass) + + ", selectionFunction=" + + repr(selectionFunction) + + ", maxWait=" + + repr(maxWait) + + ", retryInterval=" + + str(retryInterval)) + elif topWindows: + return topWindows[0] + elif (maxWait-retryInterval) >= 0: + time.sleep(retryInterval) + try: + return findTopWindow(wantedText=wantedText, + wantedClass=wantedClass, + selectionFunction=selectionFunction, + maxWait=maxWait-retryInterval, + retryInterval=retryInterval) + except WinGuiAutoError: + raise WinGuiAutoError("No top level window found for wantedText=" + + repr(wantedText) + + ", wantedClass=" + + repr(wantedClass) + + ", selectionFunction=" + + repr(selectionFunction) + + ", maxWait=" + + repr(maxWait) + + ", retryInterval=" + + str(retryInterval)) + else: + raise WinGuiAutoError("No top level window found for wantedText=" + + repr(wantedText) + + ", wantedClass=" + + repr(wantedClass) + + ", selectionFunction=" + + repr(selectionFunction) + + ", maxWait=" + + repr(maxWait) + + ", retryInterval=" + + str(retryInterval)) + +def findTopWindows(wantedText=None, wantedClass=None, selectionFunction=None): + '''Find the hwnd of top level windows. + + You can identify windows using captions, classes, a custom selection + function, or any combination of these. (Multiple selection criteria are + ANDed. If this isn't what's wanted, use a selection function.) + + Arguments: + wantedText Text which required windows' captions must contain. + wantedClass Class to which required windows must belong. + selectionFunction Window selection function. Reference to a function + should be passed here. The function should take hwnd as + an argument, and should return True when passed the + hwnd of a desired window. + + Returns: A list containing the window handles of all top level + windows matching the supplied selection criteria. + + Usage example: optDialogs = findTopWindows(wantedText="Options") + ''' + results = [] + topWindows = [] + win32gui.EnumWindows(_windowEnumerationHandler, topWindows) + for hwnd, windowText, windowClass in topWindows: + if wantedText and \ + not _normaliseText(wantedText) in _normaliseText(windowText): + continue + if wantedClass and not windowClass == wantedClass: + continue + if selectionFunction and not selectionFunction(hwnd): + continue + results.append(hwnd) + return results + +def dumpTopWindows(): + '''TODO''' + topWindows = [] + win32gui.EnumWindows(_windowEnumerationHandler, topWindows) + return topWindows + +def dumpWindow(hwnd): + '''Dump all controls from a window into a nested list + + Useful during development, allowing to you discover the structure of the + contents of a window, showing the text and class of all contained controls. + + Think of it as a poor man's Spy++. ;-) + + Arguments: The window handle of the top level window to dump. + + Returns A nested list of controls. Each entry consists of the + control's hwnd, its text, its class, and its sub-controls, + if any. + + Usage example: replaceDialog = findTopWindow(wantedText='Replace') + pprint.pprint(dumpWindow(replaceDialog)) + ''' + windows = [] + try: + win32gui.EnumChildWindows(hwnd, _windowEnumerationHandler, windows) + except win32gui.error: + # No child windows + return + windows = [list(window) for window in windows] + for window in windows: + childHwnd, windowText, windowClass = window + window_content = dumpWindow(childHwnd) + if window_content: + window.append(window_content) + + def dedup(thelist): + '''De-duplicate deeply nested windows list.''' + def listContainsSublists(thelist): + return bool([sublist + for sublist in thelist + if isinstance(sublist, list)]) + found=[] + def dodedup(thelist): + todel = [] + for index, thing in enumerate(thelist): + if isinstance(thing, list) and listContainsSublists(thing): + dodedup(thing) + else: + if thing in found: + todel.append(index) + else: + found.append(thing) + todel.reverse() + for todel in todel: + del thelist[todel] + dodedup(thelist) + dedup(windows) + + return windows + +def findControl(topHwnd, + wantedText=None, + wantedClass=None, + selectionFunction=None, + maxWait=1, + retryInterval=0.1): + '''Find a control. + + You can identify a control within a top level window, using caption, class, + a custom selection function, or any combination of these. (Multiple + selection criteria are ANDed. If this isn't what's wanted, use a selection + function.) + + If no control matching the specified selection criteria is found + immediately, further attempts will be made. The retry interval and maximum + time to wait for a matching control can be specified. + + Arguments: + topHwnd The window handle of the top level window in which the + required controls reside. + wantedText Text which the required control's captions must contain. + wantedClass Class to which the required control must belong. + selectionFunction Control selection function. Reference to a function + should be passed here. The function should take hwnd as + an argument, and should return True when passed the + hwnd of the desired control. + maxWait The maximum time to wait for a matching control, in + seconds. + retryInterval How frequently to look for a matching control, in + seconds + + Returns: The window handle of the first control matching the + supplied selection criteria. + + Raises: + WinGuiAutoError When no control or multiple controls found. + + Usage example: optDialog = findTopWindow(wantedText="Options") + okButton = findControl(optDialog, + wantedClass="Button", + wantedText="OK") + ''' + controls = findControls(topHwnd, + wantedText=wantedText, + wantedClass=wantedClass, + selectionFunction=selectionFunction) + # check for None returned: Tim 6 Jul 2004 + if controls==None: + raise WinGuiAutoError("EnumChildWindows failed with win32gui.error " + + repr(topHwnd) + + ", wantedText=" + + repr(wantedText) + + ", wantedClass=" + + repr(wantedClass) + + ", selectionFunction=" + + repr(selectionFunction) + + ", maxWait=" + + repr(maxWait) + + ", retryInterval=" + + str(retryInterval) + ) + + if len(controls) > 1: + raise WinGuiAutoError("Multiple controls found for topHwnd=" + + repr(topHwnd) + + ", wantedText=" + + repr(wantedText) + + ", wantedClass=" + + repr(wantedClass) + + ", selectionFunction=" + + repr(selectionFunction) + + ", maxWait=" + + repr(maxWait) + + ", retryInterval=" + + str(retryInterval)) + elif controls: + return controls[0] + elif (maxWait-retryInterval) >= 0: + time.sleep(retryInterval) + try: + return findControl(topHwnd=topHwnd, + wantedText=wantedText, + wantedClass=wantedClass, + selectionFunction=selectionFunction, + maxWait=maxWait-retryInterval, + retryInterval=retryInterval) + except WinGuiAutoError: + raise WinGuiAutoError("No control found for topHwnd=" + + repr(topHwnd) + + ", wantedText=" + + repr(wantedText) + + ", wantedClass=" + + repr(wantedClass) + + ", selectionFunction=" + + repr(selectionFunction) + + ", maxWait=" + + repr(maxWait) + + ", retryInterval=" + + str(retryInterval)) + else: + raise WinGuiAutoError("No control found for topHwnd=" + + repr(topHwnd) + + ", wantedText=" + + repr(wantedText) + + ", wantedClass=" + + repr(wantedClass) + + ", selectionFunction=" + + repr(selectionFunction) + + ", maxWait=" + + repr(maxWait) + + ", retryInterval=" + + str(retryInterval)) + +def findControls(topHwnd, + wantedText=None, + wantedClass=None, + selectionFunction=None): + '''Find controls. + + You can identify controls using captions, classes, a custom selection + function, or any combination of these. (Multiple selection criteria are + ANDed. If this isn't what's wanted, use a selection function.) + + Arguments: + topHwnd The window handle of the top level window in which the + required controls reside. + wantedText Text which the required controls' captions must contain. + wantedClass Class to which the required controls must belong. + selectionFunction Control selection function. Reference to a function + should be passed here. The function should take hwnd as + an argument, and should return True when passed the + hwnd of a desired control. + + Returns: The window handles of the controls matching the + supplied selection criteria. + + Usage example: optDialog = findTopWindow(wantedText="Options") + def findButtons(hwnd, windowText, windowClass): + return windowClass == "Button" + buttons = findControl(optDialog, wantedText="Button") + ''' + def searchChildWindows(currentHwnd): + results = [] + childWindows = [] + try: + win32gui.EnumChildWindows(currentHwnd, + _windowEnumerationHandler, + childWindows) + except win32gui.error: + # This seems to mean that the control *cannot* have child windows, + # i.e. is not a container. + return + + for childHwnd, windowText, windowClass in childWindows: + descendantMatchingHwnds = searchChildWindows(childHwnd) + if descendantMatchingHwnds: + results += descendantMatchingHwnds + + if wantedText and \ + not _normaliseText(wantedText) in _normaliseText(windowText): + continue + if wantedClass and \ + not windowClass == wantedClass: + continue + if selectionFunction and \ + not selectionFunction(childHwnd): + continue + results.append(childHwnd) + return results + + # deduplicate the returned windows: Tim 6 Jul 2004 + #return searchChildWindows(topHwnd) + + hlist=searchChildWindows(topHwnd) + + if hlist: + # deduplicate the list: + hdict={} + for h in hlist: + hdict[h]='' + return hdict.keys() + else: + return hlist + +def getTopMenu(hWnd): + '''Get a window's main, top level menu. + + Arguments: + hWnd The window handle of the top level window for which the top + level menu is required. + + Returns: The menu handle of the window's main, top level menu. + + Usage example: hMenu = getTopMenu(hWnd) + ''' + return ctypes.windll.user32.GetMenu(ctypes.c_long(hWnd)) + +def activateMenuItem(hWnd, menuItemPath): + '''Activate a menu item + + Arguments: + hWnd The window handle of the top level window whose menu you + wish to activate. + menuItemPath The path to the required menu item. This should be a + sequence specifying the path through the menu to the + required item. Each item in this path can be specified + either as an index, or as a menu name. + + Raises: + WinGuiAutoError When the requested menu option isn't found. + + Usage example: activateMenuItem(notepadWindow, ('file', 'open')) + + Which is exactly equivalent to... + + activateMenuItem(notepadWindow, (0, 1)) + ''' + # By Axel Kowald (kowald@molgen.mpg.de) + # Modified by S Brunning to accept strings in addition to indicies. + + # Top level menu + hMenu = getTopMenu(hWnd) + + # Get top level menu's item count. Is there a better way to do this? + # Yes .. Tim Couper 22 Jul 2004 + + hMenuItemCount=win32gui.GetMenuItemCount(hMenu) + +## for hMenuItemCount in xrange(256): +## try: +## _getMenuInfo(hMenu, hMenuItemCount) +## except WinGuiAutoError: +## break +## hMenuItemCount -= 1 + + # Walk down submenus + for submenu in menuItemPath[:-1]: + try: # submenu is an index + 0 + submenu + submenuInfo = _getMenuInfo(hMenu, submenu) + hMenu, hMenuItemCount = submenuInfo.submenu, submenuInfo.itemCount + except TypeError: # Hopefully, submenu is a menu name + try: + dump, hMenu, hMenuItemCount = _findNamedSubmenu(hMenu, + hMenuItemCount, + submenu) + except WinGuiAutoError: + raise WinGuiAutoError("Menu path " + + repr(menuItemPath) + + " cannot be found.") + + # Get required menu item's ID. (the one at the end). + menuItem = menuItemPath[-1] + try: # menuItem is an index + 0 + menuItem + menuItemID = ctypes.windll.user32.GetMenuItemID(hMenu, + menuItem) + except TypeError: # Hopefully, menuItem is a menu name + try: + subMenuIndex, dump, dump = _findNamedSubmenu(hMenu, + hMenuItemCount, + menuItem) + except WinGuiAutoError: + raise WinGuiAutoError("Menu path " + + repr(menuItemPath) + + " cannot be found.") + menuItemID = ctypes.windll.user32.GetMenuItemID(hMenu, subMenuIndex) + + # Activate + win32gui.PostMessage(hWnd, win32con.WM_COMMAND, menuItemID, 0) + +##def findMenuItems(hWnd,wantedText): +## """Finds menu items whose captions contain the text""" +## hMenu = getTopMenu(hWnd) +## hMenuItemCount=win32gui.GetMenuItemCount(hMenu) +## +## for topItem in xrange(hMenuItemCount): +## + +def getMenuInfo(hWnd, menuItemPath): + '''TODO''' + + # Top level menu + hMenu = getTopMenu(hWnd) + + # Get top level menu's item count. Is there a better way to do this? + # Yes .. Tim Couper 22 Jul 2004 + hMenuItemCount=win32gui.GetMenuItemCount(hMenu) + +## for hMenuItemCount in xrange(256): +## try: +## _getMenuInfo(hMenu, hMenuItemCount) +## except WinGuiAutoError: +## break +## hMenuItemCount -= 1 + submenuInfo=None + + # Walk down submenus + for submenu in menuItemPath: + try: # submenu is an index + 0 + submenu + submenuInfo = _getMenuInfo(hMenu, submenu) + hMenu, hMenuItemCount = submenuInfo.submenu, submenuInfo.itemCount + except TypeError: # Hopefully, submenu is a menu name + try: + submenuIndex, new_hMenu, hMenuItemCount = _findNamedSubmenu(hMenu, + hMenuItemCount, + submenu) + submenuInfo = _getMenuInfo(hMenu, submenuIndex) + hMenu = new_hMenu + except WinGuiAutoError: + raise WinGuiAutoError("Menu path " + + repr(menuItemPath) + + " cannot be found.") + if submenuInfo==None: + raise WinGuiAutoError("Menu path " + + repr(menuItemPath) + + " cannot be found. (Null menu path?)") + + + return submenuInfo + +def _getMenuInfo(hMenu, uIDItem): + '''Get various info about a menu item. + + Arguments: + hMenu The menu in which the item is to be found. + uIDItem The item's index + + Returns: Menu item information object. This object is basically + a 'bunch' + (see http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52308). + It will have useful attributes: name, itemCount, + submenu, isChecked, isDisabled, isGreyed, and + isSeparator + + Raises: + WinGuiAutoError When the requested menu option isn't found. + + Usage example: submenuInfo = _getMenuInfo(hMenu, submenu) + hMenu, hMenuItemCount = submenuInfo.submenu, submenuInfo.itemCount + ''' + # An object to hold the menu info + class MenuInfo(Bunch): + pass + menuInfo = MenuInfo() + + # Menu state + menuState = ctypes.windll.user32.GetMenuState(hMenu, + uIDItem, + win32con.MF_BYPOSITION) + if menuState == -1: + raise WinGuiAutoError("No such menu item, hMenu=" + + str(hMenu) + + " uIDItem=" + + str(uIDItem)) + + # Menu name + menuName = ctypes.c_buffer("\000" * 32) + ctypes.windll.user32.GetMenuStringA(ctypes.c_int(hMenu), + ctypes.c_int(uIDItem), + menuName, ctypes.c_int(len(menuName)), + win32con.MF_BYPOSITION) + menuInfo.name = menuName.value + + # Sub menu info + menuInfo.itemCount = menuState >> 8 + if bool(menuState & win32con.MF_POPUP): + menuInfo.submenu = ctypes.windll.user32.GetSubMenu(hMenu, uIDItem) + else: + menuInfo.submenu = None + + menuInfo.isChecked = bool(menuState & win32con.MF_CHECKED) + menuInfo.isDisabled = bool(menuState & win32con.MF_DISABLED) + menuInfo.isGreyed = bool(menuState & win32con.MF_GRAYED) + menuInfo.isSeparator = bool(menuState & win32con.MF_SEPARATOR) + # ... there are more, but these are the ones I'm interested in thus far. + + return menuInfo + +def clickButton(hwnd): + '''Simulates a single mouse click on a button + + Arguments: + hwnd Window handle of the required button. + + Usage example: okButton = findControl(fontDialog, + wantedClass="Button", + wantedText="OK") + clickButton(okButton) + ''' + _sendNotifyMessage(hwnd, win32con.BN_CLICKED) + +def clickStatic(hwnd): + '''Simulates a single mouse click on a static + + Arguments: + hwnd Window handle of the required static. + + Usage example: TODO + ''' + _sendNotifyMessage(hwnd, win32con.STN_CLICKED) + +def doubleClickStatic(hwnd): + '''Simulates a double mouse click on a static + + Arguments: + hwnd Window handle of the required static. + + Usage example: TODO + ''' + _sendNotifyMessage(hwnd, win32con.STN_DBLCLK) + +def getComboboxItems(hwnd): + '''Returns the items in a combo box control. + + Arguments: + hwnd Window handle for the combo box. + + Returns: Combo box items. + + Usage example: fontCombo = findControl(fontDialog, wantedClass="ComboBox") + fontComboItems = getComboboxItems(fontCombo) + ''' + + return _getMultipleWindowValues(hwnd, + getCountMessage=win32con.CB_GETCOUNT, + getValueMessage=win32con.CB_GETLBTEXT) + +def selectComboboxItem(hwnd, item): + '''Selects a specified item in a Combo box control. + + Arguments: + hwnd Window handle of the required combo box. + item The reqired item. Either an index, of the text of the + required item. + + Usage example: fontComboItems = getComboboxItems(fontCombo) + selectComboboxItem(fontCombo, + random.choice(fontComboItems)) + ''' + try: # item is an index Use this to select + 0 + item + win32gui.SendMessage(hwnd, win32con.CB_SETCURSEL, item, 0) + _sendNotifyMessage(hwnd, win32con.CBN_SELCHANGE) + except TypeError: # Item is a string - find the index, and use that + items = getComboboxItems(hwnd) + itemIndex = items.index(item) + selectComboboxItem(hwnd, itemIndex) + +def getListboxItems(hwnd): + '''Returns the items in a list box control. + + Arguments: + hwnd Window handle for the list box. + + Returns: List box items. + + Usage example: docType = findControl(newDialog, wantedClass="ListBox") + typeListBox = getListboxItems(docType) + ''' + + return _getMultipleWindowValues(hwnd, + getCountMessage=win32con.LB_GETCOUNT, + getValueMessage=win32con.LB_GETTEXT) + +def selectListboxItem(hwnd, item): + '''Selects a specified item in a list box control. + + Arguments: + hwnd Window handle of the required list box. + item The reqired item. Either an index, of the text of the + required item. + + Usage example: docType = findControl(newDialog, wantedClass="ListBox") + typeListBox = getListboxItems(docType) + + # Select a type at random + selectListboxItem(docType, + random.randint(0, len(typeListBox)-1)) + ''' + try: # item is an index Use this to select + 0 + item + win32gui.SendMessage(hwnd, win32con.LB_SETCURSEL, item, 0) + _sendNotifyMessage(hwnd, win32con.LBN_SELCHANGE) + except TypeError: # Item is a string - find the index, and use that + items = getListboxItems(hwnd) + itemIndex = items.index(item) + selectListboxItem(hwnd, itemIndex) + +def getEditText(hwnd): + '''Returns the text in an edit control. + + Arguments: + hwnd Window handle for the edit control. + + Returns Edit control text lines. + + Usage example: pprint.pprint(getEditText(editArea)) + ''' + return _getMultipleWindowValues(hwnd, + getCountMessage=win32con.EM_GETLINECOUNT, + getValueMessage=win32con.EM_GETLINE) + +def setEditText(hwnd, text, append=False): + '''Set an edit control's text. + + Arguments: + hwnd The edit control's hwnd. + text The text to send to the control. This can be a single + string, or a sequence of strings. If the latter, each will + be become a a separate line in the control. + append Should the new text be appended to the existing text? + Defaults to False, meaning that any existing text will be + replaced. If True, the new text will be appended to the end + of the existing text. + Note that the first line of the new text will be directly + appended to the end of the last line of the existing text. + If appending lines of text, you may wish to pass in an + empty string as the 1st element of the 'text' argument. + + Usage example: print "Enter various bits of text." + setEditText(editArea, "Hello, again!") + setEditText(editArea, "You still there?") + setEditText(editArea, ["Here come", "two lines!"]) + + print "Add some..." + setEditText(editArea, ["", "And a 3rd one!"], append=True) + ''' + + # Ensure that text is a list + try: + text + '' + text = [text] + except TypeError: + pass + + # Set the current selection range, depending on append flag + if append: + win32gui.SendMessage(hwnd, + win32con.EM_SETSEL, + -1, + 0) + else: + win32gui.SendMessage(hwnd, + win32con.EM_SETSEL, + 0, + -1) + + # Send the text + win32gui.SendMessage(hwnd, + win32con.EM_REPLACESEL, + True, + os.linesep.join(text)) + +def setCheckBox(hwnd, state = True): + """ + Activates a CheckBox button. + + Inputs - + hwnd - Handle of GUI element + state - Boolean + True - Activate the Checkbox + False - Clear the CheckBox + + Outputs - + Integer -- Result of the Win32gui.SendMessage Command + + Note: There is a 3rd state to a CheckBox. Since it is not + common has been split to another function + setCheckBox_Indeterminate. + """ + win32gui.SendMessage( hwnd, + win32con.BM_SETCHECK, + win32con.BST_CHECKED, + 0 ) + + +def setCheckBox_Indeterminate(hwnd): + """ + Activates a CheckBox button. + + Inputs - + hwnd - Handle of GUI element + + Outputs - + Integer -- Result of the Win32gui.SendMessage Command + + """ + win32gui.SendMessage( hwnd, + win32con.BM_SETCHECK, + win32con.BST_INDETERMINATE, + 0 ) + +def getCheckBox(hwnd): + """ + Returns the status from a CheckBox button. + + Inputs - + hwnd - Handle of GUI element + + Outputs - + 0 - win32Gui send message error + win32con.BST_CHECKED- The Checkbox is checked + win32con.BST_INDETERMINATE - The Checkbox is checked and = + greyed out. + win32con.BST_UNCHECKED- The checkbox is not checked + =20 + """ + value = win32gui.SendMessage( hwnd, + win32con.BM_GETCHECK, + 0, + 0 ) + return value + +def _getMultipleWindowValues(hwnd, getCountMessage, getValueMessage): + ''' + + A common pattern in the Win32 API is that in order to retrieve a + series of values, you use one message to get a count of available + items, and another to retrieve them. This internal utility function + performs the common processing for this pattern. + + Arguments: + hwnd Window handle for the window for which items should be + retrieved. + getCountMessage Item count message. + getValueMessage Value retrieval message. + + Returns: Retrieved items. + ''' + result = [] + + MAX_VALUE_LENGTH = 256 + bufferlength = struct.pack('i', MAX_VALUE_LENGTH) # This is a C style int. + + valuecount = win32gui.SendMessage(hwnd, getCountMessage, 0, 0) + for itemIndex in range(valuecount): + valuebuffer = array.array('c', + bufferlength + + ' ' * (MAX_VALUE_LENGTH - len(bufferlength))) + valueLength = win32gui.SendMessage(hwnd, + getValueMessage, + itemIndex, + valuebuffer) + result.append(valuebuffer.tostring()[:valueLength]) + return result + +def _windowEnumerationHandler(hwnd, resultList): + '''win32gui.EnumWindows() callback. + + Pass to win32gui.EnumWindows() or win32gui.EnumChildWindows() to + generate a list of window handle, window text, window class tuples. + ''' + resultList.append((hwnd, + win32gui.GetWindowText(hwnd), + win32gui.GetClassName(hwnd))) + +def _buildWinLong(high, low): + '''Build a windows long parameter from high and low words. + + See http://support.microsoft.com/support/kb/articles/q189/1/70.asp + ''' + # return ((high << 16) | low) + return int(struct.unpack('>L', + struct.pack('>2H', + high, + low)) [0]) + +def _sendNotifyMessage(hwnd, notifyMessage): + '''Send a notify message to a control.''' + win32gui.SendMessage(win32gui.GetParent(hwnd), + win32con.WM_COMMAND, + _buildWinLong(notifyMessage, + win32api.GetWindowLong(hwnd, + win32con.GWL_ID)), + hwnd) + +def _normaliseText(controlText): + '''Remove '&' characters, and lower case. + + Useful for matching control text.''' + return controlText.lower().replace('&', '') + +def _findNamedSubmenu(hMenu, hMenuItemCount, submenuName): + '''Find the index number of a menu's submenu with a specific name.''' + for submenuIndex in range(hMenuItemCount): + submenuInfo = _getMenuInfo(hMenu, submenuIndex) + if _normaliseText(submenuInfo.name).startswith(_normaliseText(submenuName)): + return submenuIndex, submenuInfo.submenu, submenuInfo.itemCount + raise WinGuiAutoError("No submenu found for hMenu=" + + repr(hMenu) + + ", hMenuItemCount=" + + repr(hMenuItemCount) + + ", submenuName=" + + repr(submenuName)) + +def _dedup(thelist): + '''De-duplicate deeply nested list.''' + found=[] + def dodedup(thelist): + for index, thing in enumerate(thelist): + if isinstance(thing, list): + dodedup(thing) + else: + if thing in found: + del thelist[index] + else: + found.append(thing) + + +class Bunch(object): + '''See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52308''' + + def __init__(self, **kwds): + self.__dict__.update(kwds) + + def __str__(self): + state = ["%s=%r" % (attribute, value) + for (attribute, value) + in self.__dict__.items()] + return '\n'.join(state) + +class WinGuiAutoError(Exception): + pass + +def self_test(): + '''Self test - drives Notepad and WordPad.''' + # I'd like to use unittest here, but I've no idea how to automate these + # tests. As it is, you just have to *watch* the test! ;-) + import pprint, random + + # NT/2K/XP versions of Notepad have a different menu stuctures. + WIN_VERSION_DECODE = {(1, 4, 0): "95", + (1, 4, 10): "98", + (1, 4, 90): "ME", + (2, 4, 0): "NT", + (2, 5, 0): "2K", + (2, 5, 1): "XP"} + win_version = os.sys.getwindowsversion() + win_version = WIN_VERSION_DECODE[win_version[3], win_version[0], win_version[1]] + print "win_version=", win_version + x=raw_input('->') + print "Let's see what top windows we have at the 'mo:" + pprint.pprint(dumpTopWindows()) + x=raw_input('->') + print "Open and locate Notepad" + os.startfile('notepad') + notepadWindow = findTopWindow(wantedClass='Notepad') + x=raw_input('->') + print "Look at some menu item info" + + print 'File menu' + print getMenuInfo(notepadWindow, ('file',)) + print "file|exit" + print getMenuInfo(notepadWindow, ('file', 'exit')) + x=raw_input('->') + print "Open and locate the 'replace' dialogue" + if win_version in ["NT"]: + activateMenuItem(notepadWindow, ['search', 'replace']) + elif win_version in ["2K", "XP"]: + activateMenuItem(notepadWindow, ['edit', 'replace']) + else: + raise Exception("Tests not written for this OS yet. Feel free!") + replaceDialog = findTopWindow(wantedText='Replace', wantedClass="#32770") + x=raw_input('->') + print "Locate the 'find' edit box" + findValue = findControls(replaceDialog, wantedClass="Edit")[0] + x=raw_input('->') + print "Enter some text - and wait long enough for it to be seen" + setEditText(findValue, "Hello, mate!") + time.sleep(.5) + x=raw_input('->') + + print "Locate the 'cancel' button, and click it." + cancelButton = findControl(replaceDialog, + wantedClass="Button", + wantedText="Cancel") + clickButton(cancelButton) + x=raw_input('->') + print "Open and locate the 'font' dialogue" + if win_version in ["NT"]: + activateMenuItem(notepadWindow, ['edit', 'set font']) + elif win_version in ["2K", "XP"]: + activateMenuItem(notepadWindow, ['format', 'font']) + print findTopWindows(wantedText='Font', wantedClass="#32770") + x=raw_input('->') + fontDialog = findTopWindow(wantedText='Font', wantedClass="#32770") + + print "Let's see if dumping works. Dump the 'font' dialogue contents:" + pprint.pprint(dumpWindow(fontDialog)) + x=raw_input('->') + + print "Change the font" + fontCombos = findControls(fontDialog, wantedClass="ComboBox") + print "Find the font selection combo" + for fontCombo in fontCombos: + fontComboItems = getComboboxItems(fontCombo) + if 'Arial' in fontComboItems: + break + x=raw_input('->') + print "Select at random" + selectComboboxItem(fontCombo, random.choice(fontComboItems)) + time.sleep(.5) + + okButton = findControl(fontDialog, wantedClass="Button", wantedText="OK") + clickButton(okButton) + x=raw_input('->') + + print "Locate notepad's edit area, and enter various bits of text." + editArea = findControl(notepadWindow,wantedClass="Edit") + setEditText(editArea, "Hello, again!") + time.sleep(.5) + setEditText(editArea, "You still there?") + time.sleep(.5) + setEditText(editArea, ["Here come", "two lines!"]) + time.sleep(.5) + x=raw_input('->') + + print "Add some..." + setEditText(editArea, ["", "And a 3rd one!"], append=True) + time.sleep(.5) + + print "See what's there now:" + pprint.pprint(getEditText(editArea)) + x=raw_input('->') + + print "Exit notepad" + activateMenuItem(notepadWindow, ('file', 'exit')) + time.sleep(.5) + + print "Don't save." + saveDialog = findTopWindow(selectionFunction=lambda hwnd: + win32gui.GetWindowText(hwnd)=='Notepad') + noButton = findControl(saveDialog,wantedClass="Button", wantedText="no") + clickButton(noButton) + x=raw_input('->') + + print "Check you get an exception for non-existent top window" + try: + findTopWindow(wantedText="Banana") + raise Exception("Test failed") + except WinGuiAutoError, winGuiAutoError: + print "Yup, got: ", str(winGuiAutoError) + x=raw_input('->') + print "OK, now we'll have a go with WordPad." + os.startfile('wordpad') + wordpadWindow = findTopWindow(wantedText='WordPad') + x=raw_input('->') + print "Open and locate the 'new document' dialog." + activateMenuItem(wordpadWindow, [0, 0]) + newDialog = findTopWindow(wantedText='New', wantedClass="#32770") + x=raw_input('->') + print "Check you get an exception for non-existent control" + try: + findControl(newDialog, wantedClass="Banana") + raise Exception("Test failed") + except WinGuiAutoError, winGuiAutoError: + print "Yup, got: ", str(winGuiAutoError) + x=raw_input('->') + print "Locate the 'document type' list box" + docType = findControl(newDialog, wantedClass="ListBox") + typeListBox = getListboxItems(docType) + print "getListboxItems(docType)=", typeListBox + x=raw_input('->') + print "Select a type at random" + selectListboxItem(docType, random.randint(0, len(typeListBox)-1)) + time.sleep(.5) + clickButton(findControl(newDialog, wantedClass="Button", wantedText="OK")) + x=raw_input('->') + print "Check you get an exception for non-existent menu path" + try: + activateMenuItem(wordpadWindow, ('not', 'there')) + raise Exception("Test failed") + except WinGuiAutoError, winGuiAutoError: + print "Yup, got: ", str(winGuiAutoError) + x=raw_input('->') + print "Check you get an exception for non-existent menu item" + try: + activateMenuItem(wordpadWindow, ('file', 'missing')) + raise Exception("Test failed") + except WinGuiAutoError, winGuiAutoError: + print "Yup, got: ", str(winGuiAutoError) + x=raw_input('->') + print "Exit wordpad" + activateMenuItem(wordpadWindow, ('file', 'exit')) + x=raw_input('->') + print "Err, that's it." + +if __name__ == '__main__': + self_test() \ No newline at end of file diff --git a/agent/crawl/watsup/winGuiAuto.pyc b/agent/crawl/watsup/winGuiAuto.pyc new file mode 100644 index 0000000..758b1c8 Binary files /dev/null and b/agent/crawl/watsup/winGuiAuto.pyc differ