[HowTo] make scripts fast and robust against intermittent FindFailed exceptions

Created by RaiMan on on 2011-05-20
Keywords:
Last updated by:
RaiMan on on 2011-06-24

motivated by question https://answers.launchpad.net/sikuli/+question/161233

Mel faced intermittent FindFailed's and ask how to resolve this issue.

*** Possible reason 1:
When you click on something, it takes some time until the target application has changed the screen content so your next find operation finds on the screen, what you expect. These times may vary, since there is always something in the background, that consumes resources, even if Sikuli and your target application are "alone" on the machine. This gets even worse, when your target app is a web app running in a browser and is dependent on something that has to be loaded from the web.
conclusion: the standard waiting time of 3 seconds for the visual object to appear on the screen might be too short in some cases.

*** Possible reason 2:
In some applications the background of an element you want to search is different over the time. So if too much of this changing background is captured together with what you really want, the score of the find operation might stay below the standard minimum similarity of 0.7.

In both cases, the standard reaction of Sikuli script is to abort the script with the exception FindFailed.
(read more: http://sikuli.org/docx/region.html#exception-findfailed)

--- To avoid timing problems, you might increase the waiting time:
read more http://sikuli.org/docx/region.html#Region.setAutoWaitTimeout
e.g. set it to 10 seconds and the timing problems might vanish.

--- for reason 2, it helps to restrict the capture, so as little as possible is captured of the background.

*** More good practices to make scripts robust

*** find one key element and click on others using calculated offsets instead of additional find operations
-- make this launchpad page visible in the browser and scroll it to top so the Sikuli logo is fully visible in the upper left corner
-- goal: be able to selectively click the main menu entries without using find

lines beginning with > have to be entered in your script in IDE

1-- new script in IDE

2-- make up the reference:
>menuOverview = sikuli-logo-captured # capture the logo

3-- in IDE click on the logo thumbnail to open the preview window
4-- in the target offset tab: position the crosshair on menu entry "Overview"
5-- click ok

now the IDE has changed your logo image into a Pattern with a target offset. If we would use it as click(menuOverview), the menu entry "Overview" would be clicked.

Now you could implement all the other menu entries the same way (by duplicating the above line, changing the variable Name and position the target offset accordingly).
An approach like this makes your script already more robust, because if one of the clicks work, all the others work too, because they are based on finding the same image.

But still for each menu entry, a new find operation is necessary (in average 0.5 seconds).

To speed things up, we now calculate the menu positions and use these click points instead.

>clickOverview = find(menuOverview)

now clickOverview contains a Match, whose click point targets the menu "Overview".

since all menu entries are on one line, they share the same y coordinates. Only the x coordinates differ.

to test the matches we use hover() and the slow motion run in the IDE

>hover(clickOverview)

should position the mouse over menu "Overview"

now the next menu entry "code": we calculate somehow the x offset to the right of "Overview".

somehow ;-) Since I am mainly on Mac, I use the builtin screenshot tool, to evaluate pixel coordinates. Any tool, that shows screen coordinates of the current mouse location can be used for this purpose. Or use a meter and calculate the pixels in a calculator based on your screen resolution pixel/inch.

>clickCode = clickOverview.getTarget().right(55)

This is the magic:
getTarget() extracts the clickposition as a Location object (x, y)
and right(55) makes a new Location 55 pixels to the right of this.

>hover(clickCode)

should hover over menu "Code".

and the next menu entry:
>clickBugs = clickCode.right(45)

In the end, we have only one find operation for the Sikuli logo and all clicks immediately happen without an additional find operation: very robust - very fast.

*** use wait/exists for individual waiting and click(getLastMatch()) to click the found match
this solution targets the "possible reason 1" in the above comment #1.
In general you split the click(image) in a wait(image, timeout) or exists(image, timeout) and a click(getLastMatch).
here is an example: faq 1501
Another advantage of this approach: You might implement recovery/corrective actions, in cases, where the workflow might fail if you use a plain click(image).

*** restrict the find operations to the region, where it should find the visual object.
Sikuli makes it very easy, to just start with some clicks and finds. These act on the whole screen.
disadvantages:
- searches last rather long (might be more than 1 second)
- you run the risk, that something is found, that you did not mean (something similar somewhere else on the screen with an even or higher score)
So in general it makes sense, to restrict the find operations to the region on the screen, where your visual objects should be (we call it region of interest (ROI)).
The Sikuli features that support ROI are:

- class Region
e.g. r.click(image) looks for the image inside the region r
every action with respect to that region, has to be qualified with that region:
if r.exists(image): click(r.getLastmatch())

- with region:
all actions in the with block are automatically qualified with region:
reg = someRegion
with reg:
    if exists(image): click(getLastmatch())

- setROI()
makes only sense with the SCREEN region (default region)
example:
find(image) # searches whole screen
setROI(Region(0,0,300,300))
find(image) # searches only in upper left (300x300) of screen
caution: other than with with: it is not obvious when reading a part of a script, that may be a setROI() is active at that point.

The so called spatial operators (http://sikuli.org/docx/region.html#extending-a-region) can be used, to easily make new regions based on existing ones:
example: define the different areas of an application window (pixels are measured somehow ;-):
winSomeApp = App("someApp").window()
th = 20
mh = 50
titleBarSomeApp = winSomeApp.above(1).below(th)
menuBarSomeApp = titleBarSomeApp.below(mh)
contentSomeApp = menuBarSomeApp.below(winSomeApp.h - th - mh)

Now you have three regions, that represent the main areas of your application window - and this works independently from the window size.

more examples can be found in the respective parts of the docs.