rtml 2.0.3 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. data/History.txt +3 -0
  2. data/Manifest.txt +51 -13
  3. data/Rakefile +6 -1
  4. data/builtin/controllers/rtml_controller.rb +12 -1
  5. data/builtin/models/rtml/document.rb +24 -18
  6. data/builtin/models/rtml/document_model_object.rb +6 -0
  7. data/builtin/models/rtml/dom/collections/element_set.rb +6 -0
  8. data/builtin/models/rtml/dom/collections/property_set.rb +11 -0
  9. data/builtin/models/rtml/dom/element.rb +79 -27
  10. data/builtin/models/rtml/dom/frontend_element.rb +41 -20
  11. data/builtin/models/rtml/dom/property.rb +43 -26
  12. data/builtin/models/rtml/dom/screen_element.rb +13 -0
  13. data/builtin/widgets/document_variable_processing.rb +42 -18
  14. data/builtin/widgets/element_builder.rb +4 -4
  15. data/builtin/widgets/screen_variable_processing.rb +2 -2
  16. data/builtin/widgets/screen_variants.rb +31 -4
  17. data/builtin/widgets/screens.rb +13 -1
  18. data/builtin/widgets/static_content.rb +20 -6
  19. data/do_profile.rb +15 -0
  20. data/lib/extensions/action_controller/response.rb +0 -18
  21. data/lib/extensions/action_controller/routing/route_set.rb +28 -18
  22. data/lib/extensions/hpricot/doc.rb +3 -3
  23. data/lib/extensions/hpricot/elem.rb +3 -3
  24. data/lib/extensions/string.rb +2 -18
  25. data/lib/rtml.rb +0 -12
  26. data/lib/rtml/assigns.rb +32 -0
  27. data/lib/rtml/controller/document_generator.rb +9 -0
  28. data/lib/rtml/controller/render_helpers.rb +42 -18
  29. data/lib/rtml/controller/state.rb +2 -1
  30. data/lib/rtml/dependencies.rb +20 -15
  31. data/lib/rtml/dsl.rb +10 -10
  32. data/lib/rtml/environment.rb +13 -1
  33. data/lib/rtml/errors/application_error.rb +5 -0
  34. data/lib/rtml/errors/simulation_error.rb +4 -0
  35. data/lib/rtml/errors/variable_error.rb +5 -0
  36. data/lib/rtml/high_level/variable_manager.rb +11 -7
  37. data/lib/rtml/inherited_instance_variables.rb +8 -1
  38. data/lib/rtml/links.rb +17 -0
  39. data/lib/rtml/rules/dom_validation.rb +1 -0
  40. data/lib/rtml/test/builtin_variables.rb +33 -0
  41. data/lib/rtml/test/resemblance_test.rb +97 -0
  42. data/lib/rtml/test/screen.rb +126 -0
  43. data/lib/rtml/test/simulator.rb +240 -0
  44. data/lib/rtml/test/simulator_post_processors/base.rb +7 -0
  45. data/lib/rtml/test/simulator_post_processors/card_parsers.rb +32 -0
  46. data/lib/rtml/test/simulator_post_processors/receipt.rb +15 -0
  47. data/lib/rtml/test/simulator_post_processors/submit.rb +15 -0
  48. data/lib/rtml/test/spec.rb +14 -7
  49. data/lib/rtml/test/spec/matchers.rb +24 -0
  50. data/lib/rtml/test/tml_application.rb +331 -0
  51. data/lib/rtml/test/unit.rb +13 -0
  52. data/lib/rtml/test/variable_scope.rb +146 -0
  53. data/lib/rtml/version.rb +1 -1
  54. data/lib/rtml/widget.rb +26 -14
  55. data/lib/rtml/widget_core/class_methods.rb +8 -4
  56. data/lib/rtml/widget_core/widget_accessor_instance_methods.rb +6 -6
  57. data/lib/rtml/widgets.rb +22 -3
  58. data/lib/rtml_routes.rb +1 -1
  59. data/rails_generators/rtml/rtml_generator.rb +3 -0
  60. data/rails_generators/rtml/templates/db/migrate/20100513165226_add_options_to_rtml_documents.rb +9 -0
  61. data/rails_generators/rtml/templates/db/migrate/20100513165242_remove_dom_elements_mirror.rb +16 -0
  62. data/rails_generators/rtml/templates/db/migrate/20100513165249_remove_dom_properties_mirror.rb +16 -0
  63. data/rails_generators/rtml/templates/lib/tasks/rtml.rake +1 -1
  64. data/rtml.gemspec +65 -0
  65. data/spec/controllers/rtml_controller_spec.rb +1 -1
  66. data/spec/integration/post_tests_spec.rb +8 -0
  67. data/spec/lib/rtml/high_level/variable_manager_spec.rb +8 -0
  68. data/spec/lib/rtml/routes_spec.rb +23 -22
  69. data/spec/lib/rtml/test/simulator/receipt_spec.rb +18 -0
  70. data/spec/lib/rtml/test/simulator_spec.rb +185 -0
  71. data/spec/lib/rtml/test/tml_application_spec.rb +119 -0
  72. data/spec/lib/rtml/test/variable_scope_spec.rb +65 -0
  73. data/spec/lib/rtml/widget_spec.rb +1 -0
  74. data/spec/lib/rtml/widgets_spec.rb +30 -0
  75. data/spec/models/rtml/document_spec.rb +8 -0
  76. data/spec/models/rtml/dom/screen_element_spec.rb +15 -0
  77. data/spec/models/rtml/instruction_spec.rb +2 -2
  78. data/spec/rtml_action_spec.rb +25 -0
  79. data/spec/spec_helper.rb +31 -1
  80. data/spec/support/app/controllers/post_tests_controller.rb +11 -0
  81. data/spec/support/app/views/inherited/instance_variables_test/display.rtml.erb +1 -0
  82. data/spec/support/config/boot.rb +1 -0
  83. data/spec/support/config/routes.rb +3 -2
  84. data/spec/support/db/rtml_test_db.sqlite3 +0 -0
  85. data/spec/support/raw_tml/avs.tml +27 -0
  86. data/spec/support/raw_tml/document_level_events.tml +18 -0
  87. data/spec/support/raw_tml/empty_screen.tml +15 -0
  88. data/spec/support/raw_tml/enter_amount.tml +40 -0
  89. data/spec/support/raw_tml/foreign_receiver.tml +10 -0
  90. data/spec/support/raw_tml/foreign_reference.tml +10 -0
  91. data/spec/support/raw_tml/hello_world.tml +13 -0
  92. data/spec/support/raw_tml/loop_x_times.tml +39 -0
  93. data/spec/support/raw_tml/one_screen_with_setvar.tml +8 -0
  94. data/spec/support/raw_tml/receipt.tml +15 -0
  95. data/spec/support/raw_tml/simulator.tml +122 -0
  96. data/spec/support/raw_tml/tmlvar_reference.tml +34 -0
  97. data/spec/support/raw_tml/user_input.tml +47 -0
  98. data/spec/support/raw_tml/valid_document.tml +6 -0
  99. data/spec/support/rspec/example_groups.rb +1 -1
  100. data/spec/support/rspec/matchers.rb +0 -11
  101. data/spec/widgets/document_variable_processing_spec.rb +25 -39
  102. data/spec/widgets/element_builder_spec.rb +4 -0
  103. data/spec/widgets/event_listener_spec.rb +9 -0
  104. data/spec/widgets/highlevel_variable_processing_spec.rb +27 -2
  105. data/spec/widgets/screen_variable_processing_spec.rb +34 -0
  106. data/spec/widgets/screens_spec.rb +22 -0
  107. data/spec/widgets/simulator_post_processors/card_parsers_spec.rb +70 -0
  108. data/spec/widgets/simulator_post_processors/submit_spec.rb +44 -0
  109. data/tasks/stats.rake +10 -0
  110. data/test/test_rtml_generator.rb +3 -0
  111. metadata +55 -49
  112. data/builtin/widgets/subroutine.rb +0 -54
  113. data/lib/rtml/high_level/subroutine.rb +0 -22
  114. data/lib/rtml/reverse_engineering/crawler.rb +0 -58
  115. data/lib/rtml/reverse_engineering/simulator.rb +0 -269
  116. data/lib/rtml/reverse_engineering/simulator/casting.rb +0 -9
  117. data/lib/rtml/reverse_engineering/simulator/snapshot.rb +0 -18
  118. data/lib/rtml/reverse_engineering/simulator/variable_lookup.rb +0 -32
  119. data/lib/rtml/reverse_engineering/simulator/variable_value.rb +0 -105
  120. data/spec/lib/rtml/reverse_engineering/crawler_spec.rb +0 -24
  121. data/spec/lib/rtml/reverse_engineering/simulator/variable_value_spec.rb +0 -120
  122. data/spec/lib/rtml/reverse_engineering/simulator_spec.rb +0 -96
  123. data/spec/support/config/tml_dom_ruleset.rb +0 -82
  124. data/spec/widgets/subroutine_spec.rb +0 -109
@@ -0,0 +1,7 @@
1
+ class Rtml::Test::SimulatorPostProcessors::Base < Rtml::Widget
2
+ delegate :variables, :process, :on_current_screen, :current_screen, :current_screen_id, :to => :parent
3
+
4
+ def continue_forward
5
+ process(:forward => true)
6
+ end
7
+ end
@@ -0,0 +1,32 @@
1
+ class Rtml::Test::SimulatorPostProcessors::CardParsers < Rtml::Test::SimulatorPostProcessors::Base
2
+ affects 'Rtml::Test::Simulator', 'simulator'
3
+ entry_point :check_card_parsers
4
+
5
+ def check_card_parsers
6
+ if !(card_readers = current_screen.card_readers).empty?
7
+ card_readers.each do |card_reader|
8
+ case card_reader['parser']
9
+ when 'mag'
10
+ process_mag_reader(card_reader['parser_params'])
11
+ when 'emv'
12
+ process_emv_reader(card_reader['parser_params'])
13
+ else raise ""
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def process_mag_reader(params)
20
+ case params
21
+ when 'read_data' ; # nothing because this needs user input
22
+ when 'risk_mgmt'
23
+ variables['card.parser.verdict'] = 'online'
24
+ continue_forward
25
+ else raise Rtml::Errors::SimulatorError, "Invalid mag params: #{params}"
26
+ end
27
+ end
28
+
29
+ def process_emv_reader(params)
30
+ raise "EMV params not supported: #{params}"
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ class Rtml::Test::SimulatorPostProcessors::Receipt < Rtml::Test::SimulatorPostProcessors::Base
2
+ affects 'Rtml::Test::Simulator', 'simulator'
3
+ entry_point :check_print_directive
4
+
5
+ def check_print_directive
6
+ if print = on_current_screen("print").first
7
+ # We reconstruct it because changing its child <getvar>'s will change the XML itself, which we don't want.
8
+ print = Hpricot::XML(print.to_s).root
9
+ ((print / "getvar") || []).each { |getvar| getvar.inner_html = variables.literal_value(variables[getvar['name']]) }
10
+
11
+ parent.receipt << print.inner_html
12
+ parent.process(true)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ class Rtml::Test::SimulatorPostProcessors::Submit < Rtml::Test::SimulatorPostProcessors::Base
2
+ affects 'Rtml::Test::Simulator', 'simulator'
3
+ entry_point :check_submit_directive
4
+
5
+ def check_submit_directive
6
+ if submit = on_current_screen("submit").first
7
+ target = submit['tgt']
8
+ getvars = ((submit / "getvar") || []).inject({}) do |hash, getvar|
9
+ hash[variables.literal_value(getvar['name'])] = variables.literal_value(variables[getvar['name']])
10
+ hash
11
+ end
12
+ parent.post_data variables.literal_value(target), getvars
13
+ end
14
+ end
15
+ end
@@ -1,14 +1,21 @@
1
1
  begin
2
2
  require 'webrat'
3
3
  rescue LoadError
4
- # optional. Or should it be?
4
+ # optional. Really should be phased out completely.
5
5
  end
6
6
 
7
- class RtmlExampleGroup < (defined?(Test::Unit::TestCase) ? Test::Unit::TestCase : Object)
8
- extend Spec::Example::ExampleGroupMethods
9
- include Spec::Example::ExampleMethods
10
- include(Webrat::Matchers) if defined?(Webrat)
11
- include(Rtml::Rules::DomValidation)
7
+ module Rtml::Test::Spec
8
+ class RtmlExampleGroup < (defined?(Test::Unit::TestCase) ? Test::Unit::TestCase : Object)
9
+ extend Spec::Example::ExampleGroupMethods
10
+ include Spec::Example::ExampleMethods
11
+ include(Webrat::Matchers) if defined?(Webrat)
12
+ include(Rtml::Rules::DomValidation)
13
+ include Rtml::Test::Spec::Matchers
14
+ end
12
15
  end
13
16
 
14
- Spec::Example::ExampleGroupFactory.register(:rtml, RtmlExampleGroup)
17
+ Spec::Runner.configure do |config|
18
+ config.include Rtml::Test::Spec::Matchers
19
+ end
20
+
21
+ Spec::Example::ExampleGroupFactory.register(:rtml, Rtml::Test::Spec::RtmlExampleGroup)
@@ -0,0 +1,24 @@
1
+ module Rtml::Test::Spec::Matchers
2
+ class ResemblanceMatcher
3
+ def initialize(expected_hash)
4
+ @expected_hash = expected_hash
5
+ end
6
+
7
+ def matches?(target)
8
+ @test = Rtml::Test::ResemblanceTest.new(@expected_hash, target)
9
+ @test.pass?
10
+ end
11
+
12
+ def failure_message
13
+ "Expected to resemble #{@expected_hash.inspect}; found:\n#{@test.tml}"
14
+ end
15
+
16
+ def negative_failure_message
17
+ "Expected to not resemble #{@expected_hash.inspect}; found:\n#{@test.tml}"
18
+ end
19
+ end
20
+
21
+ def resemble_tml(expectation)
22
+ Rtml::Test::Spec::Matchers::ResemblanceMatcher.new(expectation)
23
+ end
24
+ end
@@ -0,0 +1,331 @@
1
+ # Loads a TML application and performs automated processing when told to do so.
2
+ #
3
+ # Note that at any given time, the "current" screen has already been processed. For
4
+ # example, calling #current_screen immediately after loading a document will
5
+ # return the first screen that appears in the document, but that screen will have
6
+ # already been processed. When you call #step or #continue, the next screen will
7
+ # be processed, and calling #current_screen again will return the name of that screen.
8
+ #
9
+ # Whenever user input would be required, the program halts, waiting for you to
10
+ # simulate that input. This class does not provide a direct interface to simulating
11
+ # user interaction; instead you should set the related variables if applicable, and
12
+ # jump directly to the desired screen.
13
+ #
14
+ # For a proper high-level simulator that allows you to call methods like #follow_link
15
+ # and #swipe_card, see Rtml::Test::Simulator instead.
16
+ #
17
+ # It is important to bear in mind that for the purposes of this class, "user input"
18
+ # refers to anything that can't be calculated directly, such as EMV app selection.
19
+ #
20
+ class Rtml::Test::TmlApplication
21
+ include Rtml::Test::BuiltinVariables
22
+ class SimulationError < StandardError; end
23
+
24
+ # A list of TML elements which generally require user input. This doesn't count
25
+ # elements that *optionally* require input such as +variant+ -- only those that
26
+ # usually *require* input such as card parser.
27
+ USER_INPUT_ELEMENTS = %w(card form submit print)
28
+
29
+ attr_reader :screenflow, :variable_scope, :receipt
30
+
31
+ # accepts the raw TML document.
32
+ def initialize(tml)
33
+ @tml = Hpricot::XML(tml)
34
+ @variable_scope = Rtml::Test::VariableScope.new
35
+ @screenflow = []
36
+ @receipt = Receipt.new
37
+ declare_variables!
38
+ end
39
+
40
+ def screens
41
+ @screens ||= ((@tml / "screen") || [])
42
+ end
43
+
44
+ def current_screen_id
45
+ current_screen ? current_screen['id'] : nil
46
+ end
47
+
48
+ def current_screen
49
+ return @current_screen if defined?(@current_screen)
50
+ raise SimulationError, "No screens in current document" if screens.empty?
51
+ @current_screen = Rtml::Test::Screen.new(screens.first)
52
+ process_current_screen!
53
+ @current_screen
54
+ end
55
+
56
+ def stop_execution!
57
+ @current_screen = nil
58
+ end
59
+
60
+ # Returns true if user input is expected at this time. If true, program flow will not proceed.
61
+ def waiting_for_input?
62
+ current_screen.input?
63
+ end
64
+
65
+ # Takes a single "step" in the execution of the program. This has no effect and
66
+ # returns :waiting if user input is required.
67
+ #
68
+ # A step is taken even if the #current_state is :looping, so this can be used to force
69
+ # execution even if #continue normally wouldn't proceed.
70
+ #
71
+ # Returns the ID of the current screen as a string otherwise.
72
+ def step
73
+ return current_state if ![:running, :looping].include?(current_state)
74
+ perform_step!
75
+ end
76
+
77
+ # Like #step, except that user interaction is ignored as if the user had just pressed
78
+ # the "enter" button.
79
+ def step_forward
80
+ return current_state if ![:running, :looping, :waiting, :display].include?(current_state)
81
+ perform_step!(:ignore_interaction => true)
82
+ end
83
+
84
+ def current_state
85
+ if current_screen.nil?
86
+ :stopped
87
+ elsif waiting_for_input?
88
+ :waiting
89
+ elsif looping?
90
+ :looping
91
+ elsif displaying_content? && !timeout?
92
+ :display
93
+ else
94
+ :running
95
+ end
96
+ end
97
+
98
+ def timeout?
99
+ current_screen.timeout > 0
100
+ end
101
+
102
+ def displaying_content?
103
+ current_screen.display?
104
+ end
105
+
106
+ # Processes screens until a non-running state is achieved, and
107
+ # then returns that state.
108
+ #
109
+ # If :breakpoint is specified, execution will be suspended at the specified screen and
110
+ # :break will be returned.
111
+ #
112
+ # If :ignore_display is specified, then execution will continue past any <display> elements
113
+ # that would normally cause execution to halt. Normally, this only occurs if there is a
114
+ # timeout on the screen displaying the content, since a timeout implies automatic
115
+ # progression.
116
+ def continue(options = {})
117
+ options = { :breakpoint => options } unless options.kind_of?(Hash)
118
+ breakpoint = options.delete(:breakpoint)
119
+ breakpoint = breakpoint.to_s if breakpoint && !breakpoint.kind_of?(String)
120
+ ignore_display = options.delete(:ignore_display)
121
+ while (state = current_state) == :running || (ignore_display && state == :display)
122
+ return :break if current_screen_id == breakpoint
123
+ if ignore_display && state == :display
124
+ step_forward
125
+ else
126
+ step
127
+ end
128
+ end
129
+ return state
130
+ end
131
+
132
+ # Forces execution to continue to the next screen, even if the current screen is waiting
133
+ # for something to happen.
134
+ def continue_forward(options = {})
135
+ continue(options) unless step_forward == :display
136
+ end
137
+
138
+ alias_method :process, :continue
139
+
140
+ def jump_to_screen(id)
141
+ unmemoize!
142
+ id = id.to_s unless id.kind_of?(String)
143
+ if id[/^tmlvar:(.*)$/]
144
+ id = variable_scope[$~[1]]
145
+ end
146
+ @current_screen = find_screen(id) || raise(Rtml::Errors::ApplicationError, "Tried to jump to missing screen #{id.inspect}")
147
+ process_current_screen!
148
+ end
149
+
150
+ def trigger_button_press(which)
151
+ assert_screen_present
152
+ which = which.to_s unless which.kind_of?(String)
153
+ which.downcase!
154
+ which = 'enter' if which == 'ok'
155
+
156
+ if (uri = current_screen.uri_for_hotkey(which))
157
+ jump_to_uri uri
158
+ elsif which == 'enter' # a special case: it makes the terminal continue forward under most conditions.
159
+ if uri = current_screen.autoselect_hyperlink
160
+ visit uri
161
+ else
162
+ continue_forward
163
+ end
164
+ elsif (defaults = ((@tml / "defaults") || []).first) && %w(cancel menu).include?(which)
165
+ jump_to_uri defaults[which]
166
+ else
167
+ raise Rtml::Errors::ApplicationError, "Keypress has no effect on screen #{current_screen_id.inspect}"
168
+ end
169
+ end
170
+
171
+ # If the path resembles a screen name that can be found within the current document, then
172
+ # #jump_to_screen is called. Otherwise, :new_document is thrown with this path as the argument.
173
+ #
174
+ # This is mostly for internal processing, such as for hyperlinks.
175
+ #
176
+ # (Does this belong in Rtml::Test::Simulator instead? Time will tell as that class is developed.)
177
+ #
178
+ def jump_to_uri(path)
179
+ if find_screen(path)
180
+ jump_to_screen path
181
+ else
182
+ throw :new_document, path
183
+ end
184
+ end
185
+
186
+ def find_screen(id)
187
+ id = id.to_s unless id.kind_of?(String)
188
+ id = id[1..-1] if id[0] == ?#
189
+ (screen_element = screens.select { |s| s['id'] == id }.first) ? Rtml::Test::Screen.new(screen_element) : nil
190
+ end
191
+
192
+ # Returns an array containing which of the possible screens following this one meet
193
+ # the current constraints. If there are no screens available, or if user intervention
194
+ # is required at this time, then an empty array is returned.
195
+ #
196
+ # See also Rtml::Test::Screen#choices
197
+ #
198
+ # options may include :ignore_interaction => true/false, defaults to false
199
+ def destinations(options = {})
200
+ return @destinations if @destinations
201
+ return [] if !current_screen.choices.empty? && !options[:ignore_interaction]
202
+ @destinations = possible_variants.select { |dest| conditions_match?(dest) }
203
+ end
204
+
205
+ # Returns the first element returned by #destinations, or nil if an empty array would
206
+ # be returned.
207
+ #
208
+ # options may include :ignore_interaction => true/false, defaults to false
209
+ def next_screen_id(options = {})
210
+ (nxt = destinations(options).first) ? nxt[:uri] : nil
211
+ end
212
+
213
+ # Analyzes a hash as returned by #possible_variants and returns true if the conditions
214
+ # match the current application state (mostly involving the current value of TML variables).
215
+ # Conditions based on hotkeys return false since this implies user interaction, and
216
+ # conditions based on timeouts return true since this implies user interaction never
217
+ # took place.
218
+ def conditions_match?(hash)
219
+ if hash.key?(:hotkey) || hash.key?(:key)
220
+ false
221
+ elsif hash.key?(:timeout)
222
+ true
223
+ elsif hash.keys == [:uri] # a <next> element should always be true, but evaluated as a last resort
224
+ true
225
+ else
226
+ variable_scope.true_condition?(hash)
227
+ end
228
+ end
229
+
230
+ # Returns an array of Hashes representing all possible non-user interaction-requiring variants
231
+ # from this screen. These are returned regardless of whether user interation is required at this time.
232
+ #
233
+ # The hashes are laid out thusly:
234
+ # { :uri => "destination_uri", :key => "hotkey", :timeout => "seconds", :lo => "left_operand",
235
+ # :op => "operation", :ro => "right_operand" }
236
+ #
237
+ # Note that some of these keys may be omitted, according to the TML rules for the +variant+ element.
238
+ #
239
+ # For TML +next+ elements, all keys except :uri are omitted.
240
+ #
241
+ def possible_variants
242
+ current_screen.possible_variants
243
+ end
244
+
245
+ def declare_variable(name, options = {})
246
+ variable_scope.declare_variable(name, options)
247
+ end
248
+
249
+ def update_variables(hash)
250
+ variable_scope.update_with(hash)
251
+ end
252
+
253
+ # Returns true if this application seems to be in an endless loop.
254
+ def looping?
255
+ snapshot_matches_previous?
256
+ end
257
+
258
+ private
259
+ def assert_screen_present
260
+ unless current_screen
261
+ raise Rtml::Errors::ApplicationError,
262
+ "Could not complete action: no screen. (Has processing been terminated by a dead end?)"
263
+ end
264
+ end
265
+
266
+ # Called just before moving from one screen to the next, this method sets the various memoized objects
267
+ # back to nil so that they are re-evaluated for the next screen.
268
+ def unmemoize!
269
+ @possible_variants = nil
270
+ @destinations = nil
271
+ @current_screen = nil
272
+ end
273
+
274
+ def declare_variables!
275
+ declare_builtin_variables!
276
+ (@tml / "vardcl").each do |vardec|
277
+ options = { }
278
+ %w(type value format perms).each { |key| options[key] = vardec[key] if vardec[key] }
279
+ declare_variable(vardec['name'], options)
280
+ end
281
+ end
282
+
283
+ def process_current_screen!
284
+ current_screen.setvars.each do |setvar|
285
+ #screenflow.clear
286
+ variable_scope.perform_operation_on(setvar['name'],
287
+ {:lo => setvar['lo'], :op => setvar['op'], :ro => setvar['ro']}.optionalize)
288
+ end
289
+ end
290
+
291
+ # see #step, except no state checking whatsoever is performed.
292
+ # options may include :ignore_interaction => true/false, defaults to false
293
+ def perform_step!(options = {})
294
+ take_snapshot!
295
+ if next_screen_id(options)
296
+ jump_to_uri(next_screen_id(options))
297
+ else # dead end
298
+ stop_execution!
299
+ end
300
+ current_screen_id
301
+ end
302
+
303
+ # Takes a snapshot and stores it in the screenflow.
304
+ def take_snapshot!
305
+ screenflow << Snapshot.new(current_screen, variable_scope)
306
+ end
307
+
308
+ # Returns true if a new snapshot would be identical to any previous snapshot.
309
+ def snapshot_matches_previous?
310
+ screenflow.include?(Snapshot.new(current_screen, variable_scope))
311
+ end
312
+
313
+ class Snapshot
314
+ attr_reader :screen, :variables
315
+
316
+ def initialize(screen, variables)
317
+ @screen = screen
318
+ @variables = variables.snapshot
319
+ end
320
+
321
+ def ==(a_snapshot)
322
+ @screen == a_snapshot.screen && @variables == a_snapshot.variables
323
+ end
324
+ end
325
+
326
+ class Receipt < String
327
+ def clear
328
+ replace("")
329
+ end
330
+ end
331
+ end