summaryrefslogtreecommitdiffstats
path: root/features/support/helpers/sikuli_helper.rb
blob: ab0b9dc570cfdc90b3746a9a13dba69e7a986243 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
require 'rjb'
require 'rjbextension'
$LOAD_PATH << ENV['SIKULI_HOME']
begin
  require 'sikulixapi.jar'
  USING_SIKULIX = true
rescue LoadError
  require 'sikuli-script.jar'
  USING_SIKULIX = false
end
Rjb::load

def using_sikulix?
  USING_SIKULIX
end

package_members = [
                   "java.io.FileOutputStream",
                   "java.io.PrintStream",
                   "java.lang.System",
                   "org.sikuli.script.Finder",
                   "org.sikuli.script.Key",
                   "org.sikuli.script.KeyModifier",
                   "org.sikuli.script.Location",
                   "org.sikuli.script.Match",
                   "org.sikuli.script.Pattern",
                   "org.sikuli.script.Region",
                   "org.sikuli.script.Screen",
                  ]

if using_sikulix?
  package_members << "org.sikuli.basics.Settings"
  package_members << "org.sikuli.script.ImagePath"
else
  package_members << "org.sikuli.script.Settings"
end

translations = Hash[
                    "org.sikuli.script", "Sikuli",
                    "org.sikuli.basics", "Sikuli",
                    "java.lang", "Java::Lang",
                    "java.io", "Java::Io",
                   ]

for p in package_members
  imported_class = Rjb::import(p)
  package, ignore, class_name = p.rpartition(".")
  next if ! translations.include? package
  mod_name = translations[package]
  mod = mod_name.split("::").inject(Object) do |parent_obj, child_name|
    if parent_obj.const_defined?(child_name, false)
      parent_obj.const_get(child_name, false)
    else
      child_obj = Module.new
      parent_obj.const_set(child_name, child_obj)
    end
  end
  mod.const_set(class_name, imported_class)
end

# Bind Java's stdout to debug_log() via our magical pseudo fifo
# logger.
def bind_java_to_pseudo_fifo_logger
  file_output_stream = Java::Io::FileOutputStream.new(DEBUG_LOG_PSEUDO_FIFO)
  print_stream = Java::Io::PrintStream.new(file_output_stream)
  Java::Lang::System.setOut(print_stream)
end

def findfailed_hook(pic)
  pause("FindFailed for: '#{pic}'")
end

# Since rjb imports Java classes without creating a corresponding
# Ruby class (it's just an instance of Rjb_JavaProxy) we can't
# monkey patch any class, so additional methods must be added
# to each Screen object.
#
# All Java classes' methods are immediately available in the proxied
# Ruby classes, but care has to be given to match their type. For a
# list of methods, see: <http://doc.sikuli.org/javadoc/index.html>.
# The type "PRSML" is a union of Pattern, Region, Screen, Match and
# Location.
#
# Also, due to limitations in Ruby's syntax we can't do:
#     def Sikuli::Screen.new
# so we work around it with the following vairable.
sikuli_script_proxy = Sikuli::Screen
$_original_sikuli_screen_new ||= Sikuli::Screen.method :new

# For waitAny()/findAny() we are forced to throw this exception since
# Rjb::throw doesn't block until the Java exception has been received
# by Ruby, so strange things can happen.
class FindAnyFailed < StandardError
end

def sikuli_script_proxy.new(*args)
  s = $_original_sikuli_screen_new.call(*args)

  if $config["SIKULI_RETRY_FINDFAILED"]
    # The usage of `_invoke()` below exemplifies how one can wrap
    # around Java objects' methods when they're imported using RJB. It
    # isn't pretty. The seconds argument is the parameter signature,
    # which can be obtained by creating the intended Java object using
    # RJB, and then calling its `java_methods` method.

    def s.wait(pic, time)
      self._invoke('wait', 'Ljava.lang.Object;D', pic, time)
    rescue FindFailed => e
      findfailed_hook(pic)
      self._invoke('wait', 'Ljava.lang.Object;D', pic, time)
    end

    def s.find(pic)
      self._invoke('find', 'Ljava.lang.Object;', pic)
    rescue FindFailed => e
      findfailed_hook(pic)
      self._invoke('find', 'Ljava.lang.Object;', pic)
    end

    def s.waitVanish(pic, time)
      self._invoke('waitVanish', 'Ljava.lang.Object;D', pic, time)
    rescue FindFailed => e
      findfailed_hook(pic)
      self._invoke('waitVanish', 'Ljava.lang.Object;D', pic, time)
    end

    def s.click(pic)
      self._invoke('click', 'Ljava.lang.Object;', pic)
    rescue FindFailed => e
      findfailed_hook(pic)
      self._invoke('click', 'Ljava.lang.Object;', pic)
    end
  end

  def s.click_point(x, y)
    self.click(Sikuli::Location.new(x, y))
  end

  def s.doubleClick_point(x, y)
    self.doubleClick(Sikuli::Location.new(x, y))
  end

  def s.click_mid_right_edge(pic)
    r = self.find(pic)
    top_right = r.getTopRight()
    x = top_right.getX
    y = top_right.getY + r.getH/2
    self.click_point(x, y)
  end

  def s.wait_and_click(pic, time)
    self.click(self.wait(pic, time))
  end

  def s.wait_and_double_click(pic, time)
    self.doubleClick(self.wait(pic, time))
  end

  def s.wait_and_right_click(pic, time)
    self.rightClick(self.wait(pic, time))
  end

  def s.wait_and_hover(pic, time)
    self.hover(self.wait(pic, time))
  end

  def s.existsAny(images)
    images.each do |image|
      region = self.exists(image)
      return [image, region] if region
    end
    return nil
  end

  def s.findAny(images)
    images.each do |image|
      begin
        return [image, self.find(image)]
      rescue FindFailed
        # Ignore. We deal we'll throw an appropriate exception after
        # having looped through all images and found none of them.
      end
    end
    # If we've reached this point, none of the images could be found.
    raise FindAnyFailed.new("can not find any of the images #{images} on the " +
                            "screen")
  end

  def s.waitAny(images, time)
    Timeout::timeout(time) do
      loop do
        result = self.existsAny(images)
        return result if result
      end
    end
  rescue Timeout::Error
    raise FindAnyFailed.new("can not find any of the images #{images} on the " +
                            "screen")
  end

  def s.hover_point(x, y)
    self.hover(Sikuli::Location.new(x, y))
  end

  def s.hide_cursor
    self.hover_point(self.w, self.h/2)
  end

  s
end

# Configure sikuli
if using_sikulix?
  Sikuli::ImagePath.add("#{Dir.pwd}/features/images/")
else
  java.lang.System.setProperty("SIKULI_IMAGE_PATH",
                               "#{Dir.pwd}/features/images/")
  ENV["SIKULI_IMAGE_PATH"] = "#{Dir.pwd}/features/images/"
end

# ruby and rjb doesn't play well together when it comes to static
# fields (and possibly methods) so we instantiate and access the field
# via objects instead. It actually works inside this file, but when
# it's required from "outside", and the file has been completely
# required, ruby's require method complains that the method for the
# field accessor is missing.
sikuli_settings = Sikuli::Settings.new
sikuli_settings.OcrDataPath = $config["TMPDIR"]
# sikuli_ruby, which we used before, defaulted to 0.9 minimum
# similarity, so all our current images are adapted to that value.
# Also, Sikuli's default of 0.7 is simply too low (many false
# positives).
sikuli_settings.MinSimilarity = 0.9
sikuli_settings.ActionLogs = true
sikuli_settings.DebugLogs = true
sikuli_settings.InfoLogs = true
sikuli_settings.ProfileLogs = false