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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/config/cross_rs4s.yaml +55 -0
  3. data/config/gem_supervisor.yaml +56 -0
  4. data/config/gem_tlc.yaml +56 -0
  5. data/config/gem_tlc_secrets.yaml +3 -0
  6. data/config/kapsch_etx.yaml +54 -0
  7. data/config/lightmotion_satellite.yaml +56 -0
  8. data/config/secrets.yaml +3 -0
  9. data/config/secrets_example.yaml +6 -0
  10. data/config/semaforica_cartesio.yaml +56 -0
  11. data/config/simulator/node_log.yaml +17 -0
  12. data/config/simulator/supervisor.yaml +11 -0
  13. data/config/simulator/tlc.yaml +56 -0
  14. data/config/sus.rb +13 -0
  15. data/config/swarco_itc3.yaml +55 -0
  16. data/config/tecsen_tmacs_supervisor.yaml +57 -0
  17. data/config/validator.rb +37 -0
  18. data/config/validator.yaml +5 -0
  19. data/config/validator_example.yaml +23 -0
  20. data/config/validator_log.yaml +19 -0
  21. data/exe/rsmp-validator +121 -0
  22. data/lib/doc_gen/parser.rb +276 -0
  23. data/lib/doc_gen/renderer.rb +153 -0
  24. data/lib/rsmp/validator/async_context.rb +15 -0
  25. data/lib/rsmp/validator/auto_node.rb +82 -0
  26. data/lib/rsmp/validator/auto_site.rb +30 -0
  27. data/lib/rsmp/validator/auto_supervisor.rb +23 -0
  28. data/lib/rsmp/validator/config_normalizer.rb +103 -0
  29. data/lib/rsmp/validator/configuration/loader.rb +79 -0
  30. data/lib/rsmp/validator/configuration/secrets.rb +54 -0
  31. data/lib/rsmp/validator/configuration/validation.rb +115 -0
  32. data/lib/rsmp/validator/configuration.rb +129 -0
  33. data/lib/rsmp/validator/helpers/alarms.rb +66 -0
  34. data/lib/rsmp/validator/helpers/clock.rb +16 -0
  35. data/lib/rsmp/validator/helpers/connection.rb +73 -0
  36. data/lib/rsmp/validator/helpers/handshake.rb +110 -0
  37. data/lib/rsmp/validator/helpers/input.rb +42 -0
  38. data/lib/rsmp/validator/helpers/security.rb +26 -0
  39. data/lib/rsmp/validator/helpers/signal_plans.rb +37 -0
  40. data/lib/rsmp/validator/helpers/signal_priority.rb +130 -0
  41. data/lib/rsmp/validator/helpers/startup.rb +157 -0
  42. data/lib/rsmp/validator/helpers/status.rb +22 -0
  43. data/lib/rsmp/validator/lifecycle.rb +99 -0
  44. data/lib/rsmp/validator/log.rb +11 -0
  45. data/lib/rsmp/validator/mode_detection.rb +84 -0
  46. data/lib/rsmp/validator/options/site_test_options.rb +58 -0
  47. data/lib/rsmp/validator/options/supervisor_test_options.rb +51 -0
  48. data/lib/rsmp/validator/site_tester.rb +113 -0
  49. data/lib/rsmp/validator/supervisor_tester.rb +76 -0
  50. data/lib/rsmp/validator/tester.rb +101 -0
  51. data/lib/rsmp/validator/version.rb +5 -0
  52. data/lib/rsmp/validator/version_filter.rb +44 -0
  53. data/lib/rsmp/validator.rb +50 -0
  54. data/schemas/site_test.json +36 -0
  55. data/schemas/supervisor_test.json +28 -0
  56. data/test/site/core/aggregated_status_spec.rb +43 -0
  57. data/test/site/core/connect_spec.rb +104 -0
  58. data/test/site/core/core_spec.rb +9 -0
  59. data/test/site/core/disconnect_spec.rb +54 -0
  60. data/test/site/site_spec.rb +5 -0
  61. data/test/site/tlc/alarm_spec.rb +134 -0
  62. data/test/site/tlc/clock_spec.rb +252 -0
  63. data/test/site/tlc/detector_logics_spec.rb +76 -0
  64. data/test/site/tlc/emergency_routes_spec.rb +106 -0
  65. data/test/site/tlc/input_spec.rb +102 -0
  66. data/test/site/tlc/invalid_command_spec.rb +103 -0
  67. data/test/site/tlc/invalid_status_spec.rb +70 -0
  68. data/test/site/tlc/modes_spec.rb +260 -0
  69. data/test/site/tlc/output_spec.rb +58 -0
  70. data/test/site/tlc/signal_groups_spec.rb +96 -0
  71. data/test/site/tlc/signal_plans_spec.rb +287 -0
  72. data/test/site/tlc/signal_priority_spec.rb +144 -0
  73. data/test/site/tlc/subscribe_spec.rb +71 -0
  74. data/test/site/tlc/system_spec.rb +76 -0
  75. data/test/site/tlc/tlc_spec.rb +7 -0
  76. data/test/site/tlc/traffic_data_spec.rb +151 -0
  77. data/test/site/tlc/traffic_situations_spec.rb +50 -0
  78. data/test/supervisor/aggregated_status_spec.rb +18 -0
  79. data/test/supervisor/connect_spec.rb +219 -0
  80. data/test/supervisor/supervisor_spec.rb +11 -0
  81. 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
@@ -0,0 +1,11 @@
1
+ module RSMP
2
+ module Validator
3
+ # Logging helpers for use in tests and validator infrastructure.
4
+ module Log
5
+ # Log the start of an action
6
+ def log(action, **options)
7
+ RSMP::Validator.log(action, **options)
8
+ end
9
+ end
10
+ end
11
+ end