Class: Scarpe::ControlInterface

Inherits:
Object
  • Object
show all
Includes:
Test::EventedAssertions, Test::Helpers, Shoes::Log
Defined in:
lib/scarpe/wv/control_interface.rb,
lib/scarpe/wv/control_interface_test.rb

Constant Summary collapse

SUBSCRIBE_EVENTS =
[:init, :shutdown, :next_redraw, :every_redraw, :next_heartbeat, :every_heartbeat]
DISPATCH_EVENTS =
[:init, :shutdown, :redraw, :heartbeat]

Constants included from Shoes::Log

Shoes::Log::DEFAULT_COMPONENT, Shoes::Log::DEFAULT_DEBUG_LOG_CONFIG, Shoes::Log::DEFAULT_LOG_CONFIG

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Test::EventedAssertions

#assert_html, #assert_include, #assert_not_include, #return_results

Methods included from Shoes::Log

configure_logger, #log_init, logger

Constructor Details

#initializeControlInterface

The control interface needs to see major system components to hook into their events



24
25
26
27
28
29
30
# File 'lib/scarpe/wv/control_interface.rb', line 24

def initialize
  log_init("WV::ControlInterface")

  @do_shutdown = false
  @event_handlers = {}
  (SUBSCRIBE_EVENTS | DISPATCH_EVENTS).each { |e| @event_handlers[e] = [] }
end

Instance Attribute Details

#do_shutdownObject (readonly)

Returns the value of attribute do_shutdown.



21
22
23
# File 'lib/scarpe/wv/control_interface.rb', line 21

def do_shutdown
  @do_shutdown
end

#doc_rootObject



61
62
63
64
65
66
67
68
# File 'lib/scarpe/wv/control_interface.rb', line 61

def doc_root
  unless @doc_root
    raise "ControlInterface code needs to be wrapped in handlers like on_event(:init) " +
      "to make sure they have access to app, doc_root, wrangler, etc!"
  end

  @doc_root
end

Instance Method Details

#all_wv_widgetsObject

Need to be able to query widgets in test code



55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/scarpe/wv/control_interface_test.rb', line 55

def all_wv_widgets
  known = [doc_root]
  to_check = [doc_root]

  until to_check.empty?
    next_layer = to_check.flat_map(&:children)
    known += next_layer
    to_check = next_layer
  end

  # I don't *think* we'll ever have widget trees that merge back together, but just in case we'll de-dup
  known.uniq
end

#appObject



52
53
54
55
56
57
58
59
# File 'lib/scarpe/wv/control_interface.rb', line 52

def app
  unless @app
    raise "ControlInterface code needs to be wrapped in handlers like on_event(:init) " +
      "to make sure they have access to app, doc_root, wrangler, etc!"
  end

  @app
end

#assert(value, msg = nil) ⇒ Object



173
174
175
176
177
178
179
180
181
# File 'lib/scarpe/wv/control_interface_test.rb', line 173

def assert(value, msg = nil)
  id = start_assertion("#{caller[0]}: #{msg || "Value should be true!"}")

  if value
    pass_assertion(id)
  else
    fail_assertion(id, "Expected true Ruby value: #{value.inspect}")
  end
end

#assert_equal(val1, val2, msg = nil) ⇒ Object



183
184
185
# File 'lib/scarpe/wv/control_interface_test.rb', line 183

def assert_equal(val1, val2, msg = nil)
  assert val1 == val2, (msg || "Expected #{val2.inspect} to equal #{val1.inspect}!")
end

#assert_js(js_code, wait_for: [], timeout: DEFAULT_ASSERTION_TIMEOUT) ⇒ Object

Create a promise to do a JS assertion, normally after other ops have finished.



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/scarpe/wv/control_interface_test.rb', line 152

def assert_js(js_code, wait_for: [], timeout: DEFAULT_ASSERTION_TIMEOUT)
  id = start_assertion(js_code)

  # this isn't a TestPromise, so it doesn't have the additional DSL entries
  promise = wrangler.eval_js_async(js_code, wait_for: wait_for, timeout: timeout)
  promise.on_rejected do
    fail_assertion(id, "JS Eval failed: #{promise.reason.inspect}")
  end
  promise.on_fulfilled do
    ret_val = promise.returned_value
    if ret_val
      pass_assertion(id)
    else
      fail_assertion(id, "Expected true JS value: #{ret_val.inspect}")
    end
  end

  # So we wrap it in a no-op TestPromise, to get the DSL entries.
  TestPromise.new(iface: self, wait_for: [promise]).to_execute {}
end

#assertion_data_as_a_structObject



142
143
144
145
146
147
148
149
# File 'lib/scarpe/wv/control_interface_test.rb', line 142

def assertion_data_as_a_struct
  {
    still_pending: @assertions_pending.size,
    succeeded: @assertions_passed,
    failed: @assertions_failed.size,
    failures: @assertions_failed.values.map { |item| [item[:code], item[:failure_reason]] },
  }
end

#assertions_may_existObject



106
107
108
109
110
111
# File 'lib/scarpe/wv/control_interface_test.rb', line 106

def assertions_may_exist
  @assertions_pending ||= {}
  @assertions_failed ||= {}
  @assertions_passed ||= 0
  @assertion_counter ||= 0
end

#assertions_pending?Boolean

Returns:

  • (Boolean)


138
139
140
# File 'lib/scarpe/wv/control_interface_test.rb', line 138

def assertions_pending?
  !@assertions_pending.empty?
end

#die_after(time) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/scarpe/wv/control_interface_test.rb', line 24

def die_after(time)
  t_start = Time.now
  @die_after = [t_start, time]

  wrangler.periodic_code("scarpeTestTimeout") do |*_args|
    t_delta = (Time.now - t_start).to_f
    if t_delta > time
      @did_time_out = true
      @log.warn("die_after - timed out after #{t_delta.inspect} (threshold: #{time.inspect})")
      return_results(false, "Timed out!")
      app.destroy
    end
  end
end

#dispatch_event(event, *args, **keywords) ⇒ Object

Send out the specified event



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
# File 'lib/scarpe/wv/control_interface.rb', line 96

def dispatch_event(event, *args, **keywords)
  @log.debug("CTL event #{event.inspect} #{args.inspect} #{keywords.inspect}")

  unless DISPATCH_EVENTS.include?(event)
    raise "Illegal dispatch of event #{event.inspect}! Valid values are: #{DISPATCH_EVENTS.inspect}"
  end

  if @do_shutdown
    @log.debug("CTL: Shutting down - not dispatching #{event}!")
    return
  end

  if event == :redraw
    dumb_dispatch_event(:every_redraw, *args, **keywords)

    # Next redraw is interesting. We can add new handlers
    # when dispatching a next_redraw handler. But we want
    # each handler to run only once.
    handlers = @event_handlers[:next_redraw]
    dumb_dispatch_event(:next_redraw, *args, **keywords)
    @event_handlers[:next_redraw] -= handlers
    return
  end

  if event == :heartbeat
    dumb_dispatch_event(:every_heartbeat, *args, **keywords)

    # Next heartbeat is interesting. We can add new handlers
    # when dispatching a next_heartbeat handler. But we want
    # each handler to run only once.
    handlers = @event_handlers[:next_heartbeat]
    dumb_dispatch_event(:next_heartbeat, *args, **keywords)
    @event_handlers[:next_heartbeat] -= handlers
    return
  end

  if event == :shutdown
    @do_shutdown = true
  end

  dumb_dispatch_event(event, *args, **keywords)
end

#fail_assertion(id, fail_message) ⇒ Object



132
133
134
135
136
# File 'lib/scarpe/wv/control_interface_test.rb', line 132

def fail_assertion(id, fail_message)
  item = @assertions_pending.delete(id)
  item[:fail_message] = fail_message
  @assertions_failed[id] = item
end

#find_wv_widgets(*specifiers) ⇒ Object

Shoes doesn't name widgets. We aren't guaranteed that the Shoes widgets are even in the same process, since we have the Relay display service for Webview. So mostly we can look by display service class.



72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/scarpe/wv/control_interface_test.rb', line 72

def find_wv_widgets(*specifiers)
  widgets = all_wv_widgets

  specifiers.each do |spec|
    if spec.is_a?(Class)
      widgets.select! { |w| spec === w }
    else
      raise "I don't know how to search for widgets by #{spec.inspect}!"
    end
  end

  widgets
end

#fully_updated(wait_for: []) ⇒ Object



201
202
203
# File 'lib/scarpe/wv/control_interface_test.rb', line 201

def fully_updated(wait_for: [])
  wrangler.promise_dom_fully_updated
end

#inspectObject



32
33
34
# File 'lib/scarpe/wv/control_interface.rb', line 32

def inspect
  "<#ControlInterface>"
end

#on_event(event, &block) ⇒ Object

On recognised events, this sets a handler for that event



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/scarpe/wv/control_interface.rb', line 83

def on_event(event, &block)
  unless SUBSCRIBE_EVENTS.include?(event)
    raise "Illegal subscribe to event #{event.inspect}! Valid values are: #{SUBSCRIBE_EVENTS.inspect}"
  end

  @unsub_id ||= 0
  @unsub_id += 1

  @event_handlers[event] << { handler: block, unsub: @unsub_id }
  @unsub_id
end

#pass_assertion(id) ⇒ Object



127
128
129
130
# File 'lib/scarpe/wv/control_interface_test.rb', line 127

def pass_assertion(id)
  @assertions_pending.delete(id)
  @assertions_passed += 1
end

#return_when_assertions_doneObject

Note that we do not extract this assertions library to use elsewhere because it's very focused on evented assertions that start and stop over a period of time. Instantaneous procedural asserts don't want to use this API.



94
95
96
97
98
99
100
101
102
103
104
# File 'lib/scarpe/wv/control_interface_test.rb', line 94

def return_when_assertions_done
  assertions_may_exist

  wrangler.periodic_code("scarpeReturnWhenAssertionsDone") do |*_args|
    if @assertions_pending.empty?
      success = @assertions_failed.empty?
      return_results success, "Assertions #{success ? "succeeded" : "failed"}", assertion_data_as_a_struct
      app.destroy
    end
  end
end

#set_system_components(app:, doc_root:, wrangler:) ⇒ Object

This should get called once, from Shoes::App



37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/scarpe/wv/control_interface.rb', line 37

def set_system_components(app:, doc_root:, wrangler:)
  unless app && wrangler
    @log.error("False app passed to set_system_components!") unless app
    @log.error("False wrangler passed to set_system_components!") unless wrangler
    raise "Must pass non-nil app and wrangler to ControlInterface#set_system_components!"
  end
  @app = app
  @doc_root = doc_root # May be nil at this point
  @wrangler = wrangler

  @wrangler.control_interface = self

  @wrangler.on_every_redraw { self.dispatch_event(:redraw) }
end

#start_assertion(code) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/scarpe/wv/control_interface_test.rb', line 113

def start_assertion(code)
  assertions_may_exist

  this_assertion = @assertion_counter
  @assertion_counter += 1

  @assertions_pending[this_assertion] = {
    id: this_assertion,
    code: code,
  }

  this_assertion
end

#test_metadataObject

This is returned alongside the actual results automatically



40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/scarpe/wv/control_interface_test.rb', line 40

def 
  data = {}
  if @die_after
    t_delta = (Time.now - @die_after[0]).to_f
    data["die_after"] = {
      t_start: @die_after[0].to_s,
      threshold: @die_after[1],
      passed: t_delta,
    }
  end
  data
end

#timed_out?Boolean

Returns:

  • (Boolean)


20
21
22
# File 'lib/scarpe/wv/control_interface_test.rb', line 20

def timed_out?
  @did_time_out
end

#with_js_dom_html(wait_for: [], timeout: DEFAULT_ASSERTION_TIMEOUT, &block) ⇒ Object



197
198
199
# File 'lib/scarpe/wv/control_interface_test.rb', line 197

def with_js_dom_html(wait_for: [], timeout: DEFAULT_ASSERTION_TIMEOUT, &block)
  with_js_value("document.getElementById('wrapper-wvroot').innerHTML", wait_for: wait_for, timeout: timeout, &block)
end

#with_js_value(js_code, wait_for: [], timeout: DEFAULT_ASSERTION_TIMEOUT, &block) ⇒ Object

How do we signal an error?



188
189
190
191
192
193
194
195
# File 'lib/scarpe/wv/control_interface_test.rb', line 188

def with_js_value(js_code, wait_for: [], timeout: DEFAULT_ASSERTION_TIMEOUT, &block)
  raise "Must give a block to with_js_value!" unless block

  js_promise = wrangler.eval_js_async(js_code, wait_for: wait_for, timeout: timeout)
  ruby_promise = TestPromise.new(iface: self, wait_for: [js_promise])
  ruby_promise.to_execute(&block)
  ruby_promise
end

#wranglerObject



70
71
72
73
74
75
76
77
# File 'lib/scarpe/wv/control_interface.rb', line 70

def wrangler
  unless @wrangler
    raise "ControlInterface code needs to be wrapped in handlers like on_event(:init) " +
      "to make sure they have access to app, doc_root, wrangler, etc!"
  end

  @wrangler
end