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.
- data/History.txt +3 -0
- data/Manifest.txt +51 -13
- data/Rakefile +6 -1
- data/builtin/controllers/rtml_controller.rb +12 -1
- data/builtin/models/rtml/document.rb +24 -18
- data/builtin/models/rtml/document_model_object.rb +6 -0
- data/builtin/models/rtml/dom/collections/element_set.rb +6 -0
- data/builtin/models/rtml/dom/collections/property_set.rb +11 -0
- data/builtin/models/rtml/dom/element.rb +79 -27
- data/builtin/models/rtml/dom/frontend_element.rb +41 -20
- data/builtin/models/rtml/dom/property.rb +43 -26
- data/builtin/models/rtml/dom/screen_element.rb +13 -0
- data/builtin/widgets/document_variable_processing.rb +42 -18
- data/builtin/widgets/element_builder.rb +4 -4
- data/builtin/widgets/screen_variable_processing.rb +2 -2
- data/builtin/widgets/screen_variants.rb +31 -4
- data/builtin/widgets/screens.rb +13 -1
- data/builtin/widgets/static_content.rb +20 -6
- data/do_profile.rb +15 -0
- data/lib/extensions/action_controller/response.rb +0 -18
- data/lib/extensions/action_controller/routing/route_set.rb +28 -18
- data/lib/extensions/hpricot/doc.rb +3 -3
- data/lib/extensions/hpricot/elem.rb +3 -3
- data/lib/extensions/string.rb +2 -18
- data/lib/rtml.rb +0 -12
- data/lib/rtml/assigns.rb +32 -0
- data/lib/rtml/controller/document_generator.rb +9 -0
- data/lib/rtml/controller/render_helpers.rb +42 -18
- data/lib/rtml/controller/state.rb +2 -1
- data/lib/rtml/dependencies.rb +20 -15
- data/lib/rtml/dsl.rb +10 -10
- data/lib/rtml/environment.rb +13 -1
- data/lib/rtml/errors/application_error.rb +5 -0
- data/lib/rtml/errors/simulation_error.rb +4 -0
- data/lib/rtml/errors/variable_error.rb +5 -0
- data/lib/rtml/high_level/variable_manager.rb +11 -7
- data/lib/rtml/inherited_instance_variables.rb +8 -1
- data/lib/rtml/links.rb +17 -0
- data/lib/rtml/rules/dom_validation.rb +1 -0
- data/lib/rtml/test/builtin_variables.rb +33 -0
- data/lib/rtml/test/resemblance_test.rb +97 -0
- data/lib/rtml/test/screen.rb +126 -0
- data/lib/rtml/test/simulator.rb +240 -0
- data/lib/rtml/test/simulator_post_processors/base.rb +7 -0
- data/lib/rtml/test/simulator_post_processors/card_parsers.rb +32 -0
- data/lib/rtml/test/simulator_post_processors/receipt.rb +15 -0
- data/lib/rtml/test/simulator_post_processors/submit.rb +15 -0
- data/lib/rtml/test/spec.rb +14 -7
- data/lib/rtml/test/spec/matchers.rb +24 -0
- data/lib/rtml/test/tml_application.rb +331 -0
- data/lib/rtml/test/unit.rb +13 -0
- data/lib/rtml/test/variable_scope.rb +146 -0
- data/lib/rtml/version.rb +1 -1
- data/lib/rtml/widget.rb +26 -14
- data/lib/rtml/widget_core/class_methods.rb +8 -4
- data/lib/rtml/widget_core/widget_accessor_instance_methods.rb +6 -6
- data/lib/rtml/widgets.rb +22 -3
- data/lib/rtml_routes.rb +1 -1
- data/rails_generators/rtml/rtml_generator.rb +3 -0
- data/rails_generators/rtml/templates/db/migrate/20100513165226_add_options_to_rtml_documents.rb +9 -0
- data/rails_generators/rtml/templates/db/migrate/20100513165242_remove_dom_elements_mirror.rb +16 -0
- data/rails_generators/rtml/templates/db/migrate/20100513165249_remove_dom_properties_mirror.rb +16 -0
- data/rails_generators/rtml/templates/lib/tasks/rtml.rake +1 -1
- data/rtml.gemspec +65 -0
- data/spec/controllers/rtml_controller_spec.rb +1 -1
- data/spec/integration/post_tests_spec.rb +8 -0
- data/spec/lib/rtml/high_level/variable_manager_spec.rb +8 -0
- data/spec/lib/rtml/routes_spec.rb +23 -22
- data/spec/lib/rtml/test/simulator/receipt_spec.rb +18 -0
- data/spec/lib/rtml/test/simulator_spec.rb +185 -0
- data/spec/lib/rtml/test/tml_application_spec.rb +119 -0
- data/spec/lib/rtml/test/variable_scope_spec.rb +65 -0
- data/spec/lib/rtml/widget_spec.rb +1 -0
- data/spec/lib/rtml/widgets_spec.rb +30 -0
- data/spec/models/rtml/document_spec.rb +8 -0
- data/spec/models/rtml/dom/screen_element_spec.rb +15 -0
- data/spec/models/rtml/instruction_spec.rb +2 -2
- data/spec/rtml_action_spec.rb +25 -0
- data/spec/spec_helper.rb +31 -1
- data/spec/support/app/controllers/post_tests_controller.rb +11 -0
- data/spec/support/app/views/inherited/instance_variables_test/display.rtml.erb +1 -0
- data/spec/support/config/boot.rb +1 -0
- data/spec/support/config/routes.rb +3 -2
- data/spec/support/db/rtml_test_db.sqlite3 +0 -0
- data/spec/support/raw_tml/avs.tml +27 -0
- data/spec/support/raw_tml/document_level_events.tml +18 -0
- data/spec/support/raw_tml/empty_screen.tml +15 -0
- data/spec/support/raw_tml/enter_amount.tml +40 -0
- data/spec/support/raw_tml/foreign_receiver.tml +10 -0
- data/spec/support/raw_tml/foreign_reference.tml +10 -0
- data/spec/support/raw_tml/hello_world.tml +13 -0
- data/spec/support/raw_tml/loop_x_times.tml +39 -0
- data/spec/support/raw_tml/one_screen_with_setvar.tml +8 -0
- data/spec/support/raw_tml/receipt.tml +15 -0
- data/spec/support/raw_tml/simulator.tml +122 -0
- data/spec/support/raw_tml/tmlvar_reference.tml +34 -0
- data/spec/support/raw_tml/user_input.tml +47 -0
- data/spec/support/raw_tml/valid_document.tml +6 -0
- data/spec/support/rspec/example_groups.rb +1 -1
- data/spec/support/rspec/matchers.rb +0 -11
- data/spec/widgets/document_variable_processing_spec.rb +25 -39
- data/spec/widgets/element_builder_spec.rb +4 -0
- data/spec/widgets/event_listener_spec.rb +9 -0
- data/spec/widgets/highlevel_variable_processing_spec.rb +27 -2
- data/spec/widgets/screen_variable_processing_spec.rb +34 -0
- data/spec/widgets/screens_spec.rb +22 -0
- data/spec/widgets/simulator_post_processors/card_parsers_spec.rb +70 -0
- data/spec/widgets/simulator_post_processors/submit_spec.rb +44 -0
- data/tasks/stats.rake +10 -0
- data/test/test_rtml_generator.rb +3 -0
- metadata +55 -49
- data/builtin/widgets/subroutine.rb +0 -54
- data/lib/rtml/high_level/subroutine.rb +0 -22
- data/lib/rtml/reverse_engineering/crawler.rb +0 -58
- data/lib/rtml/reverse_engineering/simulator.rb +0 -269
- data/lib/rtml/reverse_engineering/simulator/casting.rb +0 -9
- data/lib/rtml/reverse_engineering/simulator/snapshot.rb +0 -18
- data/lib/rtml/reverse_engineering/simulator/variable_lookup.rb +0 -32
- data/lib/rtml/reverse_engineering/simulator/variable_value.rb +0 -105
- data/spec/lib/rtml/reverse_engineering/crawler_spec.rb +0 -24
- data/spec/lib/rtml/reverse_engineering/simulator/variable_value_spec.rb +0 -120
- data/spec/lib/rtml/reverse_engineering/simulator_spec.rb +0 -96
- data/spec/support/config/tml_dom_ruleset.rb +0 -82
- data/spec/widgets/subroutine_spec.rb +0 -109
|
@@ -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
|
data/lib/rtml/test/spec.rb
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
begin
|
|
2
2
|
require 'webrat'
|
|
3
3
|
rescue LoadError
|
|
4
|
-
# optional.
|
|
4
|
+
# optional. Really should be phased out completely.
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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::
|
|
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
|