Class: Scarpe::ControlInterface

Inherits:
Object
  • Object
show all
Includes:
Test::Helpers, Shoes::Log
Defined in:
lib/scarpe/wasm/control_interface.rb,
lib/scarpe/wasm/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]

Instance Attribute Summary collapse

Instance Method Summary collapse

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/wasm/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/wasm/control_interface.rb', line 21

def do_shutdown
  @do_shutdown
end

#doc_rootObject



61
62
63
64
65
66
67
68
# File 'lib/scarpe/wasm/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



51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/scarpe/wasm/control_interface_test.rb', line 51

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/wasm/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



169
170
171
172
173
174
175
176
177
# File 'lib/scarpe/wasm/control_interface_test.rb', line 169

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



179
180
181
# File 'lib/scarpe/wasm/control_interface_test.rb', line 179

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.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/scarpe/wasm/control_interface_test.rb', line 148

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



138
139
140
141
142
143
144
145
# File 'lib/scarpe/wasm/control_interface_test.rb', line 138

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



102
103
104
105
106
107
# File 'lib/scarpe/wasm/control_interface_test.rb', line 102

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

#assertions_pending?Boolean

Returns:

  • (Boolean)


134
135
136
# File 'lib/scarpe/wasm/control_interface_test.rb', line 134

def assertions_pending?
  !@assertions_pending.empty?
end

#die_after(time) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/scarpe/wasm/control_interface_test.rb', line 20

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/wasm/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



128
129
130
131
132
# File 'lib/scarpe/wasm/control_interface_test.rb', line 128

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.



68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/scarpe/wasm/control_interface_test.rb', line 68

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



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

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

#inspectObject



32
33
34
# File 'lib/scarpe/wasm/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/wasm/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



123
124
125
126
# File 'lib/scarpe/wasm/control_interface_test.rb', line 123

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.



90
91
92
93
94
95
96
97
98
99
100
# File 'lib/scarpe/wasm/control_interface_test.rb', line 90

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/wasm/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



109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/scarpe/wasm/control_interface_test.rb', line 109

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



36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/scarpe/wasm/control_interface_test.rb', line 36

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)


16
17
18
# File 'lib/scarpe/wasm/control_interface_test.rb', line 16

def timed_out?
  @did_time_out
end

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



193
194
195
# File 'lib/scarpe/wasm/control_interface_test.rb', line 193

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?



184
185
186
187
188
189
190
191
# File 'lib/scarpe/wasm/control_interface_test.rb', line 184

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/wasm/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