Class: Rtml::Test::TmlApplication

Inherits:
Object
  • Object
show all
Includes:
BuiltinVariables
Defined in:
lib/rtml/test/tml_application.rb

Overview

Loads a TML application and performs automated processing when told to do so.

Note that at any given time, the “current” screen has already been processed. For example, calling #current_screen immediately after loading a document will return the first screen that appears in the document, but that screen will have already been processed. When you call #step or #continue, the next screen will be processed, and calling #current_screen again will return the name of that screen.

Whenever user input would be required, the program halts, waiting for you to simulate that input. This class does not provide a direct interface to simulating user interaction; instead you should set the related variables if applicable, and jump directly to the desired screen.

For a proper high-level simulator that allows you to call methods like #follow_link and #swipe_card, see Rtml::Test::Simulator instead.

It is important to bear in mind that for the purposes of this class, “user input” refers to anything that can’t be calculated directly, such as EMV app selection.

Defined Under Namespace

Classes: Receipt, SimulationError, Snapshot

Constant Summary collapse

USER_INPUT_ELEMENTS =

A list of TML elements which generally require user input. This doesn’t count elements that optionally require input such as variant – only those that usually require input such as card parser.

%w(card form submit print)

Constants included from BuiltinVariables

BuiltinVariables::BUILTIN_VARIABLES

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from BuiltinVariables

add_variable, #declare_builtin_variables!

Constructor Details

#initialize(tml) ⇒ TmlApplication

accepts the raw TML document.



32
33
34
35
36
37
38
# File 'lib/rtml/test/tml_application.rb', line 32

def initialize(tml)
  @tml = Hpricot::XML(tml)
  @variable_scope = Rtml::Test::VariableScope.new
  @screenflow = []
  @receipt = Receipt.new
  declare_variables!
end

Instance Attribute Details

#receiptObject (readonly)

Returns the value of attribute receipt.



29
30
31
# File 'lib/rtml/test/tml_application.rb', line 29

def receipt
  @receipt
end

#screenflowObject (readonly)

Returns the value of attribute screenflow.



29
30
31
# File 'lib/rtml/test/tml_application.rb', line 29

def screenflow
  @screenflow
end

#variable_scopeObject (readonly)

Returns the value of attribute variable_scope.



29
30
31
# File 'lib/rtml/test/tml_application.rb', line 29

def variable_scope
  @variable_scope
end

Instance Method Details

#conditions_match?(hash) ⇒ Boolean

Analyzes a hash as returned by #possible_variants and returns true if the conditions match the current application state (mostly involving the current value of TML variables). Conditions based on hotkeys return false since this implies user interaction, and conditions based on timeouts return true since this implies user interaction never took place.

Returns:

  • (Boolean)


218
219
220
221
222
223
224
225
226
227
228
# File 'lib/rtml/test/tml_application.rb', line 218

def conditions_match?(hash)
  if hash.key?(:hotkey) || hash.key?(:key)
    false
  elsif hash.key?(:timeout)
    true
  elsif hash.keys == [:uri] # a <next> element should always be true, but evaluated as a last resort
    true
  else
    variable_scope.true_condition?(hash)
  end
end

#continue(options = {}) ⇒ Object Also known as: process

Processes screens until a non-running state is achieved, and then returns that state.

If :breakpoint is specified, execution will be suspended at the specified screen and :break will be returned.

If :ignore_display is specified, then execution will continue past any <display> elements that would normally cause execution to halt. Normally, this only occurs if there is a timeout on the screen displaying the content, since a timeout implies automatic progression.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/rtml/test/tml_application.rb', line 116

def continue(options = {})
  options = { :breakpoint => options } unless options.kind_of?(Hash)
  breakpoint = options.delete(:breakpoint)
  breakpoint = breakpoint.to_s if breakpoint && !breakpoint.kind_of?(String)
  ignore_display = options.delete(:ignore_display)
  while (state = current_state) == :running || (ignore_display && state == :display)
    return :break if current_screen_id == breakpoint
    if ignore_display && state == :display
      step_forward
    else
      step
    end
  end
  return state
end

#continue_forward(options = {}) ⇒ Object

Forces execution to continue to the next screen, even if the current screen is waiting for something to happen.



134
135
136
# File 'lib/rtml/test/tml_application.rb', line 134

def continue_forward(options = {})
  continue(options) unless step_forward == :display
end

#current_screenObject

Raises:



48
49
50
51
52
53
54
# File 'lib/rtml/test/tml_application.rb', line 48

def current_screen
  return @current_screen if defined?(@current_screen)
  raise SimulationError, "No screens in current document" if screens.empty?
  @current_screen = Rtml::Test::Screen.new(screens.first)
  process_current_screen!
  @current_screen
end

#current_screen_idObject



44
45
46
# File 'lib/rtml/test/tml_application.rb', line 44

def current_screen_id
  current_screen ? current_screen['id'] : nil
end

#current_stateObject



84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/rtml/test/tml_application.rb', line 84

def current_state
  if current_screen.nil?
    :stopped
  elsif waiting_for_input?
    :waiting
  elsif looping?
    :looping
  elsif displaying_content? && !timeout?
    :display
  else
    :running
  end
end

#declare_variable(name, options = {}) ⇒ Object



245
246
247
# File 'lib/rtml/test/tml_application.rb', line 245

def declare_variable(name, options = {})
  variable_scope.declare_variable(name, options)
end

#destinations(options = {}) ⇒ Object

Returns an array containing which of the possible screens following this one meet the current constraints. If there are no screens available, or if user intervention is required at this time, then an empty array is returned.

See also Rtml::Test::Screen#choices

options may include :ignore_interaction => true/false, defaults to false



199
200
201
202
203
# File 'lib/rtml/test/tml_application.rb', line 199

def destinations(options = {})
  return @destinations if @destinations
  return [] if !current_screen.choices.empty? && !options[:ignore_interaction]
  @destinations = possible_variants.select { |dest| conditions_match?(dest) }
end

#displaying_content?Boolean

Returns:

  • (Boolean)


102
103
104
# File 'lib/rtml/test/tml_application.rb', line 102

def displaying_content?
  current_screen.display?
end

#find_screen(id) ⇒ Object



186
187
188
189
190
# File 'lib/rtml/test/tml_application.rb', line 186

def find_screen(id)
  id = id.to_s unless id.kind_of?(String)
  id = id[1..-1] if id[0] == ?#
  (screen_element = screens.select { |s| s['id'] == id }.first) ? Rtml::Test::Screen.new(screen_element) : nil
end

#jump_to_screen(id) ⇒ Object



140
141
142
143
144
145
146
147
148
# File 'lib/rtml/test/tml_application.rb', line 140

def jump_to_screen(id)
  unmemoize!
  id = id.to_s unless id.kind_of?(String)
  if id[/^tmlvar:(.*)$/]
    id = variable_scope[$~[1]]
  end
  @current_screen = find_screen(id) || raise(Rtml::Errors::ApplicationError, "Tried to jump to missing screen #{id.inspect}")
  process_current_screen!
end

#jump_to_uri(path) ⇒ Object

If the path resembles a screen name that can be found within the current document, then #jump_to_screen is called. Otherwise, :new_document is thrown with this path as the argument.

This is mostly for internal processing, such as for hyperlinks.

(Does this belong in Rtml::Test::Simulator instead? Time will tell as that class is developed.)



178
179
180
181
182
183
184
# File 'lib/rtml/test/tml_application.rb', line 178

def jump_to_uri(path)
  if find_screen(path)
    jump_to_screen path
  else
    throw :new_document, path
  end
end

#looping?Boolean

Returns true if this application seems to be in an endless loop.

Returns:

  • (Boolean)


254
255
256
# File 'lib/rtml/test/tml_application.rb', line 254

def looping?
  snapshot_matches_previous?
end

#next_screen_id(options = {}) ⇒ Object

Returns the first element returned by #destinations, or nil if an empty array would be returned.

options may include :ignore_interaction => true/false, defaults to false



209
210
211
# File 'lib/rtml/test/tml_application.rb', line 209

def next_screen_id(options = {})
  (nxt = destinations(options).first) ? nxt[:uri] : nil
end

#possible_variantsObject

Returns an array of Hashes representing all possible non-user interaction-requiring variants from this screen. These are returned regardless of whether user interation is required at this time.

The hashes are laid out thusly:

{ :uri => "destination_uri", :key => "hotkey", :timeout => "seconds", :lo => "left_operand",
  :op => "operation", :ro => "right_operand" }

Note that some of these keys may be omitted, according to the TML rules for the variant element.

For TML next elements, all keys except :uri are omitted.



241
242
243
# File 'lib/rtml/test/tml_application.rb', line 241

def possible_variants
  current_screen.possible_variants
end

#screensObject



40
41
42
# File 'lib/rtml/test/tml_application.rb', line 40

def screens
  @screens ||= ((@tml / "screen") || [])
end

#stepObject

Takes a single “step” in the execution of the program. This has no effect and returns :waiting if user input is required.

A step is taken even if the #current_state is :looping, so this can be used to force execution even if #continue normally wouldn’t proceed.

Returns the ID of the current screen as a string otherwise.



72
73
74
75
# File 'lib/rtml/test/tml_application.rb', line 72

def step
  return current_state if ![:running, :looping].include?(current_state)
  perform_step!
end

#step_forwardObject

Like #step, except that user interaction is ignored as if the user had just pressed the “enter” button.



79
80
81
82
# File 'lib/rtml/test/tml_application.rb', line 79

def step_forward
  return current_state if ![:running, :looping, :waiting, :display].include?(current_state)
  perform_step!(:ignore_interaction => true)
end

#stop_execution!Object



56
57
58
# File 'lib/rtml/test/tml_application.rb', line 56

def stop_execution!
  @current_screen = nil
end

#timeout?Boolean

Returns:

  • (Boolean)


98
99
100
# File 'lib/rtml/test/tml_application.rb', line 98

def timeout?
  current_screen.timeout > 0
end

#trigger_button_press(which) ⇒ Object



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/rtml/test/tml_application.rb', line 150

def trigger_button_press(which)
  assert_screen_present
  which = which.to_s unless which.kind_of?(String)
  which.downcase!
  which = 'enter' if which == 'ok'

  if (uri = current_screen.uri_for_hotkey(which))
    jump_to_uri uri
  elsif which == 'enter' # a special case: it makes the terminal continue forward under most conditions.
    if uri = current_screen.autoselect_hyperlink
      visit uri
    else
      continue_forward
    end
  elsif (defaults = ((@tml / "defaults") || []).first) && %w(cancel menu).include?(which)
    jump_to_uri defaults[which]
  else
    raise Rtml::Errors::ApplicationError, "Keypress has no effect on screen #{current_screen_id.inspect}"
  end
end

#update_variables(hash) ⇒ Object



249
250
251
# File 'lib/rtml/test/tml_application.rb', line 249

def update_variables(hash)
  variable_scope.update_with(hash)
end

#waiting_for_input?Boolean

Returns true if user input is expected at this time. If true, program flow will not proceed.

Returns:

  • (Boolean)


61
62
63
# File 'lib/rtml/test/tml_application.rb', line 61

def waiting_for_input?
  current_screen.input?
end