rsmp-validator 0.1.0
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.
- checksums.yaml +7 -0
- data/config/cross_rs4s.yaml +55 -0
- data/config/gem_supervisor.yaml +56 -0
- data/config/gem_tlc.yaml +56 -0
- data/config/gem_tlc_secrets.yaml +3 -0
- data/config/kapsch_etx.yaml +54 -0
- data/config/lightmotion_satellite.yaml +56 -0
- data/config/secrets.yaml +3 -0
- data/config/secrets_example.yaml +6 -0
- data/config/semaforica_cartesio.yaml +56 -0
- data/config/simulator/node_log.yaml +17 -0
- data/config/simulator/supervisor.yaml +11 -0
- data/config/simulator/tlc.yaml +56 -0
- data/config/sus.rb +13 -0
- data/config/swarco_itc3.yaml +55 -0
- data/config/tecsen_tmacs_supervisor.yaml +57 -0
- data/config/validator.rb +37 -0
- data/config/validator.yaml +5 -0
- data/config/validator_example.yaml +23 -0
- data/config/validator_log.yaml +19 -0
- data/exe/rsmp-validator +121 -0
- data/lib/doc_gen/parser.rb +276 -0
- data/lib/doc_gen/renderer.rb +153 -0
- data/lib/rsmp/validator/async_context.rb +15 -0
- data/lib/rsmp/validator/auto_node.rb +82 -0
- data/lib/rsmp/validator/auto_site.rb +30 -0
- data/lib/rsmp/validator/auto_supervisor.rb +23 -0
- data/lib/rsmp/validator/config_normalizer.rb +103 -0
- data/lib/rsmp/validator/configuration/loader.rb +79 -0
- data/lib/rsmp/validator/configuration/secrets.rb +54 -0
- data/lib/rsmp/validator/configuration/validation.rb +115 -0
- data/lib/rsmp/validator/configuration.rb +129 -0
- data/lib/rsmp/validator/helpers/alarms.rb +66 -0
- data/lib/rsmp/validator/helpers/clock.rb +16 -0
- data/lib/rsmp/validator/helpers/connection.rb +73 -0
- data/lib/rsmp/validator/helpers/handshake.rb +110 -0
- data/lib/rsmp/validator/helpers/input.rb +42 -0
- data/lib/rsmp/validator/helpers/security.rb +26 -0
- data/lib/rsmp/validator/helpers/signal_plans.rb +37 -0
- data/lib/rsmp/validator/helpers/signal_priority.rb +130 -0
- data/lib/rsmp/validator/helpers/startup.rb +157 -0
- data/lib/rsmp/validator/helpers/status.rb +22 -0
- data/lib/rsmp/validator/lifecycle.rb +99 -0
- data/lib/rsmp/validator/log.rb +11 -0
- data/lib/rsmp/validator/mode_detection.rb +84 -0
- data/lib/rsmp/validator/options/site_test_options.rb +58 -0
- data/lib/rsmp/validator/options/supervisor_test_options.rb +51 -0
- data/lib/rsmp/validator/site_tester.rb +113 -0
- data/lib/rsmp/validator/supervisor_tester.rb +76 -0
- data/lib/rsmp/validator/tester.rb +101 -0
- data/lib/rsmp/validator/version.rb +5 -0
- data/lib/rsmp/validator/version_filter.rb +44 -0
- data/lib/rsmp/validator.rb +50 -0
- data/schemas/site_test.json +36 -0
- data/schemas/supervisor_test.json +28 -0
- data/test/site/core/aggregated_status_spec.rb +43 -0
- data/test/site/core/connect_spec.rb +104 -0
- data/test/site/core/core_spec.rb +9 -0
- data/test/site/core/disconnect_spec.rb +54 -0
- data/test/site/site_spec.rb +5 -0
- data/test/site/tlc/alarm_spec.rb +134 -0
- data/test/site/tlc/clock_spec.rb +252 -0
- data/test/site/tlc/detector_logics_spec.rb +76 -0
- data/test/site/tlc/emergency_routes_spec.rb +106 -0
- data/test/site/tlc/input_spec.rb +102 -0
- data/test/site/tlc/invalid_command_spec.rb +103 -0
- data/test/site/tlc/invalid_status_spec.rb +70 -0
- data/test/site/tlc/modes_spec.rb +260 -0
- data/test/site/tlc/output_spec.rb +58 -0
- data/test/site/tlc/signal_groups_spec.rb +96 -0
- data/test/site/tlc/signal_plans_spec.rb +287 -0
- data/test/site/tlc/signal_priority_spec.rb +144 -0
- data/test/site/tlc/subscribe_spec.rb +71 -0
- data/test/site/tlc/system_spec.rb +76 -0
- data/test/site/tlc/tlc_spec.rb +7 -0
- data/test/site/tlc/traffic_data_spec.rb +151 -0
- data/test/site/tlc/traffic_situations_spec.rb +50 -0
- data/test/supervisor/aggregated_status_spec.rb +18 -0
- data/test/supervisor/connect_spec.rb +219 -0
- data/test/supervisor/supervisor_spec.rb +11 -0
- metadata +190 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Helpers
|
|
4
|
+
# Helpers for validating the sequence of messages during RSMP connection establishment.
|
|
5
|
+
module Handshake
|
|
6
|
+
EXPECTED_VERSION_EXCHANGE_MESSAGES = [
|
|
7
|
+
'in:Version',
|
|
8
|
+
'out:MessageAck',
|
|
9
|
+
'out:Version',
|
|
10
|
+
'in:MessageAck'
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
EXPECTED_WATCHDOG_EXCHANGE_MESSAGES = [
|
|
14
|
+
'in:Watchdog',
|
|
15
|
+
'out:MessageAck',
|
|
16
|
+
'out:Watchdog',
|
|
17
|
+
'in:MessageAck'
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
EXPECTED_COMPONENT_LIST_MESSAGES = [
|
|
21
|
+
'in:ComponentList',
|
|
22
|
+
'out:MessageAck'
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def get_connection_message(core_version, length)
|
|
26
|
+
timeout = RSMP::Validator.get_config('timeouts', 'ready')
|
|
27
|
+
got = nil
|
|
28
|
+
|
|
29
|
+
RSMP::Validator::SiteTester.isolated(
|
|
30
|
+
'collect' => { timeout: timeout, num: length, ingoing: true, outgoing: true },
|
|
31
|
+
'sites' => { 'default' => { 'rsmp_versions' => [core_version] } }
|
|
32
|
+
) do |task, _supervisor, site|
|
|
33
|
+
assert(site.ready?, 'expected site to be ready')
|
|
34
|
+
collector = site.collector
|
|
35
|
+
collector.use_task task
|
|
36
|
+
collector.wait!
|
|
37
|
+
got = collector.messages.map { |message| "#{message.direction}:#{message.type}" }
|
|
38
|
+
end
|
|
39
|
+
got
|
|
40
|
+
rescue Async::TimeoutError
|
|
41
|
+
raise "Did not collect #{length} messages within #{timeout}s"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def check_sequence_v311_to_v313(core_version)
|
|
45
|
+
expected_version_messages = EXPECTED_VERSION_EXCHANGE_MESSAGES
|
|
46
|
+
expected_watchdog_messages = EXPECTED_WATCHDOG_EXCHANGE_MESSAGES
|
|
47
|
+
|
|
48
|
+
length = expected_version_messages.length + expected_watchdog_messages.length
|
|
49
|
+
got = get_connection_message core_version, length
|
|
50
|
+
|
|
51
|
+
expect_sequence_part!(
|
|
52
|
+
got[0..3],
|
|
53
|
+
expected: expected_version_messages,
|
|
54
|
+
forbidden: ['in:AggregatedStatus', 'in:Watchdog', 'in:Alarm'],
|
|
55
|
+
context: 'version exchange'
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
expect_sequence_part!(
|
|
59
|
+
got[4..7],
|
|
60
|
+
expected: expected_watchdog_messages,
|
|
61
|
+
forbidden: ['in:AggregatedStatus', 'in:Alarm'],
|
|
62
|
+
context: 'watchdog exchange'
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def expect_sequence_part!(got_part, expected:, forbidden:, context:)
|
|
67
|
+
forbidden.each do |message|
|
|
68
|
+
type = message.split(':').last
|
|
69
|
+
assert(
|
|
70
|
+
!got_part.include?(message),
|
|
71
|
+
"#{type} not allowed during #{context}: #{got_part}"
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
assert(
|
|
76
|
+
got_part.tally == expected.tally,
|
|
77
|
+
"Wrong #{context} part, must contain #{expected}, got #{got_part}"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def check_sequence_v314_or_later(version)
|
|
82
|
+
expected = EXPECTED_VERSION_EXCHANGE_MESSAGES + EXPECTED_WATCHDOG_EXCHANGE_MESSAGES
|
|
83
|
+
got = get_connection_message version, expected.length
|
|
84
|
+
assert(got == expected, "Expected connection sequence #{expected.inspect}, got #{got.inspect}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def check_sequence_v330(version)
|
|
88
|
+
expected = EXPECTED_VERSION_EXCHANGE_MESSAGES +
|
|
89
|
+
EXPECTED_WATCHDOG_EXCHANGE_MESSAGES +
|
|
90
|
+
EXPECTED_COMPONENT_LIST_MESSAGES
|
|
91
|
+
got = get_connection_message version, expected.length
|
|
92
|
+
assert(got == expected, "Expected connection sequence #{expected.inspect}, got #{got.inspect}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def check_sequence(version)
|
|
96
|
+
case version
|
|
97
|
+
when '3.1.1', '3.1.2', '3.1.3'
|
|
98
|
+
check_sequence_v311_to_v313 version
|
|
99
|
+
when '3.1.4', '3.1.5', '3.2', '3.2.1', '3.2.2'
|
|
100
|
+
check_sequence_v314_or_later version
|
|
101
|
+
when '3.3.0'
|
|
102
|
+
check_sequence_v330 version
|
|
103
|
+
else
|
|
104
|
+
raise "Unknown rsmp version #{version}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Helpers
|
|
4
|
+
# Helper methods for testing RSMP input/output functionality.
|
|
5
|
+
module Input
|
|
6
|
+
include Status
|
|
7
|
+
|
|
8
|
+
def force_input_and_confirm(site_proxy, input:, value:, within:)
|
|
9
|
+
site_proxy.tlc.force_input(input:, status: 'True', value:, within:)
|
|
10
|
+
digit = (value == 'True' ? '1' : '0')
|
|
11
|
+
|
|
12
|
+
wait_for_status(
|
|
13
|
+
site_proxy,
|
|
14
|
+
"input #{input} to be #{value}",
|
|
15
|
+
[
|
|
16
|
+
{ 'sCI' => 'S0003', 'n' => 'inputstatus', 's' => /^.{#{input - 1}}#{digit}/ }
|
|
17
|
+
]
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def switch_input(site_proxy, indx, within:)
|
|
22
|
+
site_proxy.tlc.set_input(input: indx.to_s, status: 'True', within:)
|
|
23
|
+
|
|
24
|
+
wait_for_status(
|
|
25
|
+
site_proxy,
|
|
26
|
+
"input #{indx} to be True",
|
|
27
|
+
[
|
|
28
|
+
{ 'sCI' => 'S0003', 'n' => 'inputstatus', 's' => /^.{#{indx - 1}}1/ }
|
|
29
|
+
]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
site_proxy.tlc.set_input(input: indx.to_s, status: 'False', within:)
|
|
33
|
+
wait_for_status(
|
|
34
|
+
site_proxy,
|
|
35
|
+
"input #{indx} to be False",
|
|
36
|
+
[{ 'sCI' => 'S0003', 'n' => 'inputstatus', 's' => /^.{#{indx - 1}}0/ }]
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Helpers
|
|
4
|
+
# Helper methods for testing RSMP security code handling.
|
|
5
|
+
module Security
|
|
6
|
+
def wrong_security_code(site_proxy)
|
|
7
|
+
log 'Try to force detector logic with wrong security code'
|
|
8
|
+
command_list = RSMP::CommandList.new(:M0008, :setForceDetectorLogic,
|
|
9
|
+
securityCode: '1111',
|
|
10
|
+
status: 'True',
|
|
11
|
+
mode: 'True').to_a
|
|
12
|
+
component = RSMP::Validator.get_config('components', 'detector_logic').keys[0]
|
|
13
|
+
timeout = RSMP::Validator.get_config('timeouts', 'command_response')
|
|
14
|
+
site_proxy.send_command_and_collect(command_list, component: component,
|
|
15
|
+
within: timeout).ok!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def require_security_codes
|
|
19
|
+
return if RSMP::Validator.config.dig 'secrets', 'security_codes'
|
|
20
|
+
|
|
21
|
+
skip 'Security codes are not configured'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Helpers
|
|
4
|
+
# Helper methods for testing RSMP signal plan functionality.
|
|
5
|
+
module SignalPlans
|
|
6
|
+
def with_cycle_time_extended(site_proxy, extension = 5, &block)
|
|
7
|
+
timeout = RSMP::Validator.get_config('timeouts', 'command_response')
|
|
8
|
+
plan = site_proxy.tlc.read_current_plan
|
|
9
|
+
time = read_plan_cycle_time(site_proxy, plan)
|
|
10
|
+
need_to_reset = true
|
|
11
|
+
time_extended = time + extension
|
|
12
|
+
site_proxy.tlc.set_cycle_time(plan: plan, cycle_time: time_extended, within: timeout)
|
|
13
|
+
verify_cycle_time(site_proxy, plan, time_extended)
|
|
14
|
+
block.yield
|
|
15
|
+
ensure
|
|
16
|
+
if need_to_reset
|
|
17
|
+
log 'Reset cycle time'
|
|
18
|
+
site_proxy.tlc.set_cycle_time(plan: plan, cycle_time: time, within: timeout)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def read_plan_cycle_time(site_proxy, plan)
|
|
25
|
+
time = site_proxy.tlc.read_cycle_times[plan]
|
|
26
|
+
assert(!time.nil?, 'Site returned empty cycle times list')
|
|
27
|
+
time
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def verify_cycle_time(site_proxy, plan, expected)
|
|
31
|
+
actual = site_proxy.tlc.read_cycle_times[plan]
|
|
32
|
+
assert(actual == expected, "Expected cycle time #{expected}, got #{actual}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
|
|
3
|
+
module RSMP
|
|
4
|
+
module Validator
|
|
5
|
+
module Helpers
|
|
6
|
+
module SignalPriority
|
|
7
|
+
# Match a specific status response or update
|
|
8
|
+
class S0033Matcher < RSMP::StatusMatcher
|
|
9
|
+
attr_accessor :state
|
|
10
|
+
|
|
11
|
+
def initialize(want, request_id:, state: nil)
|
|
12
|
+
super(want)
|
|
13
|
+
@request_id = request_id
|
|
14
|
+
@state = state
|
|
15
|
+
@latest_state = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def match(item)
|
|
19
|
+
super_matched = super
|
|
20
|
+
if super_matched == true
|
|
21
|
+
state = find_request_state item['s']
|
|
22
|
+
if state == @state.to_s && state != @latest_state
|
|
23
|
+
@latest_state = state
|
|
24
|
+
true
|
|
25
|
+
else
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
super_matched
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def find_request_state(list)
|
|
34
|
+
priority = list.find { |prio| prio['r'] == @request_id }
|
|
35
|
+
priority['s'] if priority
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Helper queue for managing signal priority requests during tests.
|
|
40
|
+
class RequestHelper < RSMP::Queue
|
|
41
|
+
include RSMP::Validator::Helpers::Status
|
|
42
|
+
|
|
43
|
+
def initialize(site_proxy, component:, signal_group_id:, timeout:, task:)
|
|
44
|
+
super(site_proxy,
|
|
45
|
+
filter: RSMP::Filter.new(
|
|
46
|
+
type: 'StatusUpdate',
|
|
47
|
+
ingoing: true,
|
|
48
|
+
outgoing: false,
|
|
49
|
+
component: component
|
|
50
|
+
),
|
|
51
|
+
task: task)
|
|
52
|
+
@site_proxy = site_proxy
|
|
53
|
+
@component = component
|
|
54
|
+
@signal_group_id = signal_group_id
|
|
55
|
+
@request_id = SecureRandom.uuid[0..3]
|
|
56
|
+
@matcher = S0033Matcher.new({ 'cCI' => 'S0033', 'q' => 'recent' }, request_id: @request_id)
|
|
57
|
+
@subscribe_list = [{ 'sCI' => 'S0033', 'n' => 'status', 'uRt' => '0' }]
|
|
58
|
+
@subscribe_list.map! { |item| item.merge!('sOc' => true) } if @site_proxy.tlc.use_soc?
|
|
59
|
+
@unsubscribe_list = [{ 'sCI' => 'S0033', 'n' => 'status' }]
|
|
60
|
+
@got = []
|
|
61
|
+
@timeout = timeout
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run
|
|
65
|
+
start
|
|
66
|
+
yield
|
|
67
|
+
ensure
|
|
68
|
+
stop
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def request(level: 7, eta: 2, vehicle_type: 'car')
|
|
72
|
+
command_list = RSMP::CommandList.new(:M0022, :requestPriority,
|
|
73
|
+
'requestId' => @request_id,
|
|
74
|
+
'signalGroupId' => @signal_group_id,
|
|
75
|
+
'type' => 'new',
|
|
76
|
+
'level' => level,
|
|
77
|
+
'eta' => eta,
|
|
78
|
+
'vehicleType' => vehicle_type).to_a
|
|
79
|
+
@site_proxy.send_command(command_list, component: @component)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def request_unrelated(level: 7, eta: 2, vehicle_type: 'car')
|
|
83
|
+
command_list = RSMP::CommandList.new(:M0022, :requestPriority,
|
|
84
|
+
'requestId' => SecureRandom.uuid[0..3],
|
|
85
|
+
'signalGroupId' => @signal_group_id,
|
|
86
|
+
'type' => 'new',
|
|
87
|
+
'level' => level,
|
|
88
|
+
'eta' => eta,
|
|
89
|
+
'vehicleType' => vehicle_type).to_a
|
|
90
|
+
@site_proxy.send_command(command_list, component: @component)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def cancel
|
|
94
|
+
command_list = RSMP::CommandList.new(:M0022, :requestPriority,
|
|
95
|
+
requestId: @request_id,
|
|
96
|
+
type: 'cancel').to_a
|
|
97
|
+
@site_proxy.send_command(command_list, component: @component)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def expect(state)
|
|
101
|
+
@matcher.state = state
|
|
102
|
+
wait_for_message timeout: @timeout
|
|
103
|
+
rescue RSMP::TimeoutError
|
|
104
|
+
raise RSMP::TimeoutError, "Priority request did not reach state #{state} within #{@timeout}s"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def accept_message?(message)
|
|
110
|
+
super && get_items(message).any? { |item| @matcher.match(item) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def start
|
|
114
|
+
start_receiving
|
|
115
|
+
@site_proxy.subscribe_to_status @subscribe_list, component: @component
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def stop
|
|
119
|
+
@site_proxy.unsubscribe_to_status @unsubscribe_list, component: @component
|
|
120
|
+
stop_receiving
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def get_items(message)
|
|
124
|
+
message.attributes['sS'] || []
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Helpers
|
|
4
|
+
# Helper methods for testing RSMP site startup sequences.
|
|
5
|
+
module Startup
|
|
6
|
+
include Status
|
|
7
|
+
|
|
8
|
+
# Tracks the startup signal group sequence for validation.
|
|
9
|
+
class SignalGroupSequence
|
|
10
|
+
attr_reader :sequence, :latest
|
|
11
|
+
|
|
12
|
+
def initialize(sequence)
|
|
13
|
+
@pos = []
|
|
14
|
+
@prev = []
|
|
15
|
+
@sequence = sequence
|
|
16
|
+
@num_groups = 0
|
|
17
|
+
@latest = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def num_started
|
|
21
|
+
@pos.count { |v| !v.nil? }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def num_done
|
|
25
|
+
@pos.count { |pos| pos == @sequence.length - 1 }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def done?
|
|
29
|
+
num_done == @num_groups
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def check(states)
|
|
33
|
+
initialize_check(states)
|
|
34
|
+
|
|
35
|
+
states.each_char.with_index do |state, group_index|
|
|
36
|
+
position = @pos[group_index]
|
|
37
|
+
error = if position
|
|
38
|
+
check_started_group(group_index, state, position)
|
|
39
|
+
else
|
|
40
|
+
check_not_started_group(group_index, state)
|
|
41
|
+
end
|
|
42
|
+
return error if error
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
:ok
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def initialize_check(states)
|
|
51
|
+
@latest = states
|
|
52
|
+
@num_groups = states.size
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check_not_started_group(group_index, state)
|
|
56
|
+
prev = @prev[group_index]
|
|
57
|
+
start = @sequence[0]
|
|
58
|
+
@pos[group_index] = 0 if state == start && !prev.nil? && prev != start
|
|
59
|
+
@prev[group_index] = state
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check_started_group(group_index, state, position)
|
|
64
|
+
current = @sequence[position]
|
|
65
|
+
return nil if state == current
|
|
66
|
+
|
|
67
|
+
expected, next_position = expected_transition(position)
|
|
68
|
+
if state != expected
|
|
69
|
+
return "Group #{group_index} changed from #{current} to #{state}, must go to #{expected}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@pos[group_index] = next_position
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def expected_transition(pos)
|
|
77
|
+
last = @sequence.length - 1
|
|
78
|
+
return [@sequence[last], last] if pos == last
|
|
79
|
+
|
|
80
|
+
next_pos = pos + 1
|
|
81
|
+
[@sequence[next_pos], next_pos]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def wait_normal_control(site_proxy, timeout: RSMP::Validator.get_config('timeouts', 'startup_sequence'))
|
|
86
|
+
site_proxy.tlc.wait_for_normal_control(timeout: timeout)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def verify_startup_sequence(site_proxy)
|
|
90
|
+
status_list = [{ 'sCI' => 'S0001', 'n' => 'signalgroupstatus' }]
|
|
91
|
+
subscribe_list, unsubscribe_list = build_subscribe_lists(site_proxy, status_list)
|
|
92
|
+
component = RSMP::Validator.get_config('main_component')
|
|
93
|
+
timeout = RSMP::Validator.get_config('timeouts', 'startup_sequence')
|
|
94
|
+
collector = RSMP::StatusCollector.new site_proxy, status_list, timeout: timeout
|
|
95
|
+
sequencer = SignalGroupSequence.new RSMP::Validator.get_config('startup_sequence')
|
|
96
|
+
collector_task = start_sequence_collector(collector, sequencer)
|
|
97
|
+
site_proxy.subscribe_to_status subscribe_list, component: component
|
|
98
|
+
yield
|
|
99
|
+
handle_startup_sequence_result(collector_task.wait, sequencer, collector, timeout)
|
|
100
|
+
wait_for_status(site_proxy, 'control mode to be startup',
|
|
101
|
+
[{ 'sCI' => 'S0020', 'n' => 'controlmode', 's' => 'control' }])
|
|
102
|
+
ensure
|
|
103
|
+
site_proxy.unsubscribe_to_status unsubscribe_list, component: component
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def build_subscribe_lists(site_proxy, status_list)
|
|
109
|
+
raw_list = RSMP::StatusList.new(status_list).to_a
|
|
110
|
+
subscribe_list = raw_list.map { |item| item.merge('uRt' => 0.to_s) }
|
|
111
|
+
subscribe_list.map! { |item| item.merge!('sOc' => true) } if site_proxy.tlc.use_soc?
|
|
112
|
+
[subscribe_list, raw_list]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def start_sequence_collector(collector, sequencer)
|
|
116
|
+
Async::Task.current.async do
|
|
117
|
+
log 'Verifying startup sequence'
|
|
118
|
+
collector.collect do |_message, item|
|
|
119
|
+
next unless item
|
|
120
|
+
|
|
121
|
+
handle_startup_sequence_item(item['s'], sequencer, collector)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def handle_startup_sequence_item(states, sequencer, collector)
|
|
127
|
+
status = sequencer.check(states)
|
|
128
|
+
|
|
129
|
+
if status == :ok
|
|
130
|
+
log "Startup sequence #{states}: OK"
|
|
131
|
+
return collector.complete if sequencer.done?
|
|
132
|
+
|
|
133
|
+
return false
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
log "Startup sequence #{states}: Fail"
|
|
137
|
+
collector.cancel status
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def handle_startup_sequence_result(result, sequencer, collector, timeout)
|
|
141
|
+
case result
|
|
142
|
+
when :ok
|
|
143
|
+
log 'Startup sequence verified'
|
|
144
|
+
when :timeout
|
|
145
|
+
raise(
|
|
146
|
+
"Startup sequence '#{sequencer.sequence}' didn't complete in #{timeout}s, " \
|
|
147
|
+
"reached #{sequencer.latest}, #{sequencer.num_started} started, " \
|
|
148
|
+
"#{sequencer.num_done} done"
|
|
149
|
+
)
|
|
150
|
+
when :cancelled
|
|
151
|
+
raise "Startup sequence '#{sequencer.sequence}' not followed: #{collector.error}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Helpers
|
|
4
|
+
# Helper methods for requesting and subscribing to RSMP status values.
|
|
5
|
+
module Status
|
|
6
|
+
def wait_for_status(site_proxy, description, status_list, **options)
|
|
7
|
+
update_rate = options.fetch(:update_rate, 0)
|
|
8
|
+
timeout = options.fetch(:timeout, RSMP::Validator.get_config('timeouts', 'command'))
|
|
9
|
+
component_id = options.fetch(:component_id, RSMP::Validator.get_config('main_component'))
|
|
10
|
+
log "Wait for #{description}"
|
|
11
|
+
site_proxy.tlc.wait_for_status(
|
|
12
|
+
description,
|
|
13
|
+
RSMP::StatusList.new(status_list).to_a,
|
|
14
|
+
update_rate: update_rate,
|
|
15
|
+
timeout: timeout,
|
|
16
|
+
component_id: component_id
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
# Suite lifecycle: startup and shutdown of the Async reactor and auto nodes.
|
|
4
|
+
module Lifecycle
|
|
5
|
+
# Initialize the validator system at sus startup.
|
|
6
|
+
def setup(sus_config)
|
|
7
|
+
@verbose = sus_config.verbose?
|
|
8
|
+
@log_stream = determine_log_stream(sus_config)
|
|
9
|
+
determine_mode sus_config
|
|
10
|
+
initialize_logging log_settings: {} # minimal init so log() works during config loading
|
|
11
|
+
load_tester_config
|
|
12
|
+
load_auto_node_config
|
|
13
|
+
setup_logging # reinitialize with config-specific settings
|
|
14
|
+
build_auto_node
|
|
15
|
+
build_tester
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Determine the log stream based on sus config.
|
|
19
|
+
def determine_log_stream(sus_config)
|
|
20
|
+
if sus_config.respond_to?(:log_file_io) && sus_config.log_file_io
|
|
21
|
+
sus_config.log_file_io
|
|
22
|
+
elsif sus_config.respond_to?(:log_path) && sus_config.log_path
|
|
23
|
+
File.open(sus_config.log_path, 'w')
|
|
24
|
+
elsif sus_config.respond_to?(:log_to_stdout) && sus_config.log_to_stdout
|
|
25
|
+
$stdout
|
|
26
|
+
else
|
|
27
|
+
File.open(File::NULL, 'w')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Set up logging with configuration-specific settings.
|
|
32
|
+
def setup_logging
|
|
33
|
+
settings = load_log_defaults('validator_log').merge('stream' => @log_stream)
|
|
34
|
+
settings = settings.deep_merge(config_log_settings) if config_log_settings
|
|
35
|
+
settings = settings.deep_merge(config['log']) if config.is_a?(Hash) && config['log']
|
|
36
|
+
initialize_logging log_settings: settings
|
|
37
|
+
|
|
38
|
+
self.node_log_settings = load_log_defaults('simulator/node_log').merge('stream' => @log_stream)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Called at sus startup: initializes the Async reactor and checks connectivity.
|
|
42
|
+
def before_suite
|
|
43
|
+
setup_reactor
|
|
44
|
+
error = run_startup_checks
|
|
45
|
+
raise error if error
|
|
46
|
+
rescue RSMP::ConnectionError => e
|
|
47
|
+
abort_startup(e, e.message)
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
abort_startup(e, e.inspect)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Called at sus shutdown: stops the auto node and reactor.
|
|
53
|
+
def after_suite
|
|
54
|
+
reactor.run do |_task|
|
|
55
|
+
auto_node&.stop
|
|
56
|
+
ensure
|
|
57
|
+
reactor.interrupt
|
|
58
|
+
end
|
|
59
|
+
# Explicitly close the reactor now, while the log stream is still open.
|
|
60
|
+
# Without this, Ruby's fiber scheduler hook fires after the File.open block
|
|
61
|
+
# has closed the log file, causing IOError when cancelled tasks try to log.
|
|
62
|
+
reactor.close
|
|
63
|
+
rescue StandardError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Initialize the Async reactor.
|
|
68
|
+
def setup_reactor
|
|
69
|
+
@reactor = Async::Reactor.new
|
|
70
|
+
reactor.annotate 'reactor'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def load_log_defaults(name)
|
|
76
|
+
path = File.expand_path("../../../config/#{name}.yaml", __dir__)
|
|
77
|
+
YAML.load_file(path)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def run_startup_checks
|
|
81
|
+
error = nil
|
|
82
|
+
reactor.run do |_task|
|
|
83
|
+
auto_node&.start
|
|
84
|
+
check_connection
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
error = e
|
|
87
|
+
ensure
|
|
88
|
+
reactor.interrupt
|
|
89
|
+
end
|
|
90
|
+
error
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def abort_startup(exception, message)
|
|
94
|
+
warn "Aborting: #{message}".colorize(:red)
|
|
95
|
+
raise exception
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|