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,103 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
# Normalizes configuration settings before passing them to RSMP nodes.
|
|
4
|
+
module ConfigNormalizer
|
|
5
|
+
def self.normalize_site_settings(settings)
|
|
6
|
+
normalized = deep_dup(settings)
|
|
7
|
+
|
|
8
|
+
if normalized['security_codes'].nil? && normalized.dig('secrets', 'security_codes').is_a?(Hash)
|
|
9
|
+
normalized['security_codes'] = deep_dup(normalized['secrets']['security_codes'])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
normalize_security_codes!(normalized)
|
|
13
|
+
normalize_input_programming!(normalized)
|
|
14
|
+
normalize_sxls_for_config!(normalized)
|
|
15
|
+
|
|
16
|
+
normalized
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.normalize_supervisor_settings(settings)
|
|
20
|
+
normalized = deep_dup(settings)
|
|
21
|
+
normalize_sxls_for_config!(normalized)
|
|
22
|
+
normalized
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.normalize_security_codes!(settings)
|
|
26
|
+
codes = settings['security_codes']
|
|
27
|
+
return unless codes.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
settings['security_codes'] = codes.each_with_object({}) do |(key, value), memo|
|
|
30
|
+
int_key = key.is_a?(String) && key.match?(/^\d+$/) ? key.to_i : key
|
|
31
|
+
memo[int_key] = value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.normalize_input_programming!(settings)
|
|
36
|
+
programming = settings.dig('inputs', 'programming')
|
|
37
|
+
return unless programming.is_a?(Hash)
|
|
38
|
+
|
|
39
|
+
normalized = normalize_programming_keys(programming)
|
|
40
|
+
return unless normalized.keys.all?(Integer)
|
|
41
|
+
|
|
42
|
+
settings['inputs'] ||= {}
|
|
43
|
+
settings['inputs']['programming'] = build_programming_array(normalized)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.normalize_programming_keys(programming)
|
|
47
|
+
programming.each_with_object({}) do |(key, value), memo|
|
|
48
|
+
int_key = key.is_a?(String) && key.match?(/^\d+$/) ? key.to_i : key
|
|
49
|
+
memo[int_key] = value
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.build_programming_array(normalized)
|
|
54
|
+
max_key = normalized.keys.max
|
|
55
|
+
program_array = Array.new(max_key + 1)
|
|
56
|
+
normalized.each do |index, value|
|
|
57
|
+
program_array[index] = value
|
|
58
|
+
end
|
|
59
|
+
program_array
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.normalize_sxls_for_config!(value)
|
|
63
|
+
case value
|
|
64
|
+
when Hash
|
|
65
|
+
value.each do |key, child|
|
|
66
|
+
value[key] = sxls_array_to_hash(child) if key == 'sxls'
|
|
67
|
+
normalize_sxls_for_config!(value[key])
|
|
68
|
+
end
|
|
69
|
+
when Array
|
|
70
|
+
value.each { |item| normalize_sxls_for_config!(item) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.sxls_array_to_hash(value)
|
|
75
|
+
return value unless value.is_a?(Array)
|
|
76
|
+
|
|
77
|
+
value.each_with_object({}) do |item, memo|
|
|
78
|
+
next unless item.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
name = item['name']
|
|
81
|
+
next unless name
|
|
82
|
+
|
|
83
|
+
sxl = item.dup
|
|
84
|
+
sxl.delete('name')
|
|
85
|
+
version = sxl.delete('version')
|
|
86
|
+
sxl.delete('prefix')
|
|
87
|
+
memo[name] = sxl.empty? ? version : sxl.merge('version' => version)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.deep_dup(value)
|
|
92
|
+
case value
|
|
93
|
+
when Hash
|
|
94
|
+
value.transform_values { |v| deep_dup(v) }
|
|
95
|
+
when Array
|
|
96
|
+
value.map { |item| deep_dup(item) }
|
|
97
|
+
else
|
|
98
|
+
value
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Configuration
|
|
4
|
+
# Private helpers for loading YAML config files and building options objects.
|
|
5
|
+
module Loader
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def build_tester_options(raw_config, config_path)
|
|
9
|
+
options_class = tester_options_class_for(raw_config)
|
|
10
|
+
build_options_from_raw(raw_config, config_path, options_class)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def apply_loaded_config(options)
|
|
14
|
+
self.config = options.to_h
|
|
15
|
+
self.config_log_settings = options.log_settings
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def load_yaml_config!(path, using_message:, missing_message:)
|
|
19
|
+
ensure_config_exists!(path, missing_message)
|
|
20
|
+
@log_stream.puts using_message if using_message && !using_message.empty?
|
|
21
|
+
raw = YAML.load_file(path)
|
|
22
|
+
validate_config_hash!(raw, path)
|
|
23
|
+
raw || {}
|
|
24
|
+
rescue Psych::SyntaxError => e
|
|
25
|
+
raise RSMP::ConfigurationError, "Cannot read config file #{path}: #{e}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ensure_config_exists!(path, missing_message)
|
|
29
|
+
abort_with_error missing_message unless File.exist?(path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def validate_config_hash!(raw, path)
|
|
33
|
+
return if raw.is_a?(Hash) || raw.nil?
|
|
34
|
+
|
|
35
|
+
raise RSMP::ConfigurationError, "Config #{path} must be a hash"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_options_from_raw(raw, path, option_class)
|
|
39
|
+
log_settings = raw.is_a?(Hash) ? raw['log'] : nil
|
|
40
|
+
options_hash = raw.is_a?(Hash) ? raw.dup : {}
|
|
41
|
+
options_hash.delete('log') if options_hash.is_a?(Hash)
|
|
42
|
+
|
|
43
|
+
option_class.new(options_hash, source: path, log_settings: log_settings)
|
|
44
|
+
rescue RSMP::ConfigurationError => e
|
|
45
|
+
abort_with_error e.message
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def tester_options_class_for(_raw)
|
|
49
|
+
case mode
|
|
50
|
+
when :site
|
|
51
|
+
RSMP::Validator::SiteTest::Options
|
|
52
|
+
when :supervisor
|
|
53
|
+
RSMP::Validator::SupervisorTest::Options
|
|
54
|
+
else
|
|
55
|
+
abort_with_error "Unknown test mode: #{mode}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def auto_node_options_class_for(raw)
|
|
60
|
+
case mode
|
|
61
|
+
when :site
|
|
62
|
+
site_options_class_for(raw)
|
|
63
|
+
when :supervisor
|
|
64
|
+
RSMP::Supervisor::Options
|
|
65
|
+
else
|
|
66
|
+
abort_with_error "Unknown test mode: #{mode}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def site_options_class_for(raw)
|
|
71
|
+
return RSMP::Site::Options unless raw.is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
type = raw['type']
|
|
74
|
+
type == 'tlc' ? RSMP::TLC::TrafficControllerSite::Options : RSMP::Site::Options
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Configuration
|
|
4
|
+
# Private helpers for loading and normalizing secrets configuration.
|
|
5
|
+
module Secrets
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def load_secrets(config_path)
|
|
9
|
+
load_secrets_file(config_path) unless config['secrets']
|
|
10
|
+
normalize_security_codes!
|
|
11
|
+
warn_missing_security_codes
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def load_secrets_file(config_path)
|
|
15
|
+
basename = File.basename(config_path, '.yaml')
|
|
16
|
+
folder = File.dirname(config_path)
|
|
17
|
+
secrets_path = File.join(folder, "#{basename}_secrets.yaml")
|
|
18
|
+
return unless File.exist?(secrets_path)
|
|
19
|
+
|
|
20
|
+
config['secrets'] = YAML.load_file(secrets_path)
|
|
21
|
+
rescue Psych::SyntaxError => e
|
|
22
|
+
abort_with_error "Cannot read secrets file #{secrets_path}: #{e}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def normalize_security_codes!
|
|
26
|
+
codes = config.dig('secrets', 'security_codes')
|
|
27
|
+
return unless codes.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
normalized = codes.each_with_object({}) do |(key, value), memo|
|
|
30
|
+
int_key = key.is_a?(String) && key.match?(/^\d+$/) ? key.to_i : key
|
|
31
|
+
memo[int_key] = value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
config['secrets']['security_codes'] = normalized
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def warn_missing_security_codes
|
|
38
|
+
return warn_no_security_code unless config.dig('secrets', 'security_codes')
|
|
39
|
+
|
|
40
|
+
warn_security_code_not_configured(1) unless config.dig('secrets', 'security_codes', 1)
|
|
41
|
+
warn_security_code_not_configured(2) unless config.dig('secrets', 'security_codes', 2)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def warn_no_security_code
|
|
45
|
+
log 'Warning: No security code configured', level: :warning
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def warn_security_code_not_configured(index)
|
|
49
|
+
log "Warning: Security code #{index} is not configured", level: :warning
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Configuration
|
|
4
|
+
# Private helpers for validating and normalizing configuration values.
|
|
5
|
+
module Validation
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def validate_mode_config!(config_path)
|
|
9
|
+
case mode
|
|
10
|
+
when :supervisor
|
|
11
|
+
validate_supervisor_mode_config!(config_path)
|
|
12
|
+
when :site
|
|
13
|
+
validate_site_mode_config!(config_path)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate_supervisor_mode_config!(config_path)
|
|
18
|
+
return if config['local_site'] && !top_level_site_settings?
|
|
19
|
+
|
|
20
|
+
if top_level_site_settings?
|
|
21
|
+
abort_with_error <<~HEREDOC
|
|
22
|
+
Error:
|
|
23
|
+
The config file at #{config_path} contains site settings at the top level.
|
|
24
|
+
For supervisor testing, put site settings under 'local_site'.
|
|
25
|
+
Check that you're using the right config file, or fix the config.
|
|
26
|
+
HEREDOC
|
|
27
|
+
else
|
|
28
|
+
abort_with_error <<~HEREDOC
|
|
29
|
+
Error:
|
|
30
|
+
The config file at #{config_path} is missing 'local_site'.
|
|
31
|
+
For supervisor testing, the config must describe the local site used during testing.
|
|
32
|
+
Check that you're using the right config file, or fix the config.
|
|
33
|
+
HEREDOC
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_site_mode_config!(config_path)
|
|
38
|
+
return if config['local_supervisor'] && !top_level_supervisor_settings?
|
|
39
|
+
|
|
40
|
+
if top_level_supervisor_settings?
|
|
41
|
+
abort_with_error <<~HEREDOC
|
|
42
|
+
Error:
|
|
43
|
+
The config file at #{config_path} contains supervisor settings at the top level.
|
|
44
|
+
For site testing, put supervisor settings under 'local_supervisor'.
|
|
45
|
+
Check that you're using the right config file, or fix the config.
|
|
46
|
+
HEREDOC
|
|
47
|
+
else
|
|
48
|
+
abort_with_error <<~HEREDOC
|
|
49
|
+
Error:
|
|
50
|
+
The config file at #{config_path} is missing 'local_supervisor'.
|
|
51
|
+
For site testing, the config must describe the local supervisor used during testing.
|
|
52
|
+
Check that you're using the right config file, or fix the config.
|
|
53
|
+
HEREDOC
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def top_level_site_settings?
|
|
58
|
+
%w[site_id supervisors type].any? { |key| config.key?(key) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def top_level_supervisor_settings?
|
|
62
|
+
%w[port ips max_sites].any? { |key| config.key?(key) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_components_config!
|
|
66
|
+
abort_with_error "Error: config 'components' settings is missing or empty" if config['components'] == {}
|
|
67
|
+
|
|
68
|
+
main_component = config.dig('components', 'main')&.keys&.first
|
|
69
|
+
abort_with_error "Error: config 'main' component settings is missing or empty" unless main_component
|
|
70
|
+
|
|
71
|
+
config['main_component'] = main_component
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def validate_timeouts_config!
|
|
75
|
+
timeouts = config['timeouts']
|
|
76
|
+
abort_with_error "Error: config 'timeouts' settings is missing or empty" if timeouts.nil? || timeouts == {}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def normalize_core_version!
|
|
80
|
+
core_version = ENV['CORE_VERSION'] || config['core_version'] || RSMP::Schema.latest_core_version
|
|
81
|
+
core_version = RSMP::Schema.latest_core_version if core_version == 'latest'
|
|
82
|
+
|
|
83
|
+
known_versions = RSMP::Schema.core_versions
|
|
84
|
+
normalized = normalized_core_version(core_version, known_versions)
|
|
85
|
+
return config['core_version'] = normalized.to_s if normalized
|
|
86
|
+
|
|
87
|
+
abort_with_error "Unknown core version #{core_version}, must be one of [#{known_versions.join(', ')}]."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def normalized_core_version(core_version, known_versions)
|
|
91
|
+
known_versions.map { |v| Gem::Version.new(v) }.sort.reverse.detect do |v|
|
|
92
|
+
Gem::Requirement.new(core_version).satisfied_by?(v)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def normalize_sxls!
|
|
97
|
+
sxls = config['sxls']
|
|
98
|
+
if sxls.nil?
|
|
99
|
+
config['sxls'] = [{ 'name' => 'tlc', 'version' => RSMP::Schema.latest_version(:tlc) }]
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sxls.each do |sxl|
|
|
104
|
+
name = sxl['name']
|
|
105
|
+
abort_with_error 'SXL name cannot be core.' if name.to_s == 'core'
|
|
106
|
+
|
|
107
|
+
RSMP::Schema.find_schema! name, sxl['version'], lenient: true
|
|
108
|
+
rescue RSMP::Schema::UnknownSchemaError => e
|
|
109
|
+
abort_with_error "Unknown SXL #{name} #{sxl['version']}: #{e}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require_relative 'configuration/loader'
|
|
3
|
+
require_relative 'configuration/validation'
|
|
4
|
+
require_relative 'configuration/secrets'
|
|
5
|
+
|
|
6
|
+
module RSMP
|
|
7
|
+
module Validator
|
|
8
|
+
# Handles loading and validating validator configuration files.
|
|
9
|
+
module Configuration
|
|
10
|
+
include Loader
|
|
11
|
+
include Validation
|
|
12
|
+
include Secrets
|
|
13
|
+
|
|
14
|
+
def load_tester_config
|
|
15
|
+
config_path = get_config_path
|
|
16
|
+
raw_config = load_yaml_config!(
|
|
17
|
+
config_path,
|
|
18
|
+
using_message: "Using #{mode} config: #{config_path}",
|
|
19
|
+
missing_message: "#{mode.capitalize} config file #{config_path} is missing"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
apply_env_overrides!(raw_config)
|
|
23
|
+
options = build_tester_options(raw_config, config_path)
|
|
24
|
+
apply_loaded_config(options)
|
|
25
|
+
validate_and_finalize_config!(config_path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def apply_env_overrides!(raw_config)
|
|
29
|
+
raw_config['core_version'] = ENV['CORE_VERSION'] if ENV['CORE_VERSION']
|
|
30
|
+
raw_config['sxls'] = parse_sxls(ENV['SXLS']) if ENV['SXLS']
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_and_finalize_config!(config_path)
|
|
34
|
+
validate_mode_config!(config_path)
|
|
35
|
+
validate_components_config!
|
|
36
|
+
validate_timeouts_config!
|
|
37
|
+
normalize_core_version!
|
|
38
|
+
normalize_sxls!
|
|
39
|
+
load_secrets config_path
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def load_auto_node_config
|
|
43
|
+
path = auto_node_config_path
|
|
44
|
+
return unless path
|
|
45
|
+
|
|
46
|
+
@log_stream.puts "Will run auto #{mode} with config: #{path}"
|
|
47
|
+
raw_config = load_yaml_config!(
|
|
48
|
+
path,
|
|
49
|
+
using_message: '',
|
|
50
|
+
missing_message: "Auto #{mode} config file #{path} is missing"
|
|
51
|
+
)
|
|
52
|
+
raw_config['sxls'] = parse_sxls(ENV['SXLS']) if ENV['SXLS']
|
|
53
|
+
options_class = auto_node_options_class_for(raw_config)
|
|
54
|
+
options = build_options_from_raw(raw_config, path, options_class)
|
|
55
|
+
self.auto_node_config = options.to_h
|
|
56
|
+
self.auto_node_log_settings = options.log_settings
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def get_config_path(local: false)
|
|
60
|
+
mode_name = mode.to_s
|
|
61
|
+
config_path = get_config_path_from_env(mode_name) || get_config_path_from_validator_yaml(mode_name)
|
|
62
|
+
abort_with_error "#{mode_name.capitalize} config path not set" unless config_path && config_path != ''
|
|
63
|
+
|
|
64
|
+
config_path = File.expand_path(config_path) if local
|
|
65
|
+
config_path
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def auto_node_config_path
|
|
69
|
+
env_key = mode == :site ? 'AUTO_SITE_CONFIG' : 'AUTO_SUPERVISOR_CONFIG'
|
|
70
|
+
env_path = ENV.fetch(env_key, nil)
|
|
71
|
+
return env_path if env_path && !env_path.empty?
|
|
72
|
+
|
|
73
|
+
ref_path = 'config/validator.yaml'
|
|
74
|
+
return nil unless File.exist? ref_path
|
|
75
|
+
|
|
76
|
+
config_ref = YAML.load_file ref_path
|
|
77
|
+
key = mode == :site ? 'auto_site' : 'auto_supervisor'
|
|
78
|
+
path = config_ref[key].to_s.strip
|
|
79
|
+
path.empty? ? nil : path
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def get_config(*path, **options)
|
|
83
|
+
value = config.dig(*path)
|
|
84
|
+
return value if value
|
|
85
|
+
|
|
86
|
+
path_name = path.inspect
|
|
87
|
+
default = options[:default]
|
|
88
|
+
assume = options[:assume]
|
|
89
|
+
if default
|
|
90
|
+
warning "Config #{path_name} not found, using default: #{default}"
|
|
91
|
+
default
|
|
92
|
+
elsif assume
|
|
93
|
+
assume
|
|
94
|
+
else
|
|
95
|
+
raise "Config #{path_name} is missing"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def get_config_path_from_env(mode_name)
|
|
102
|
+
key = "#{mode_name.upcase}_CONFIG"
|
|
103
|
+
ENV.fetch(key, nil)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def get_config_path_from_validator_yaml(mode_name)
|
|
107
|
+
ref_path = 'config/validator.yaml'
|
|
108
|
+
return nil unless File.exist? ref_path
|
|
109
|
+
|
|
110
|
+
config_ref = YAML.load_file ref_path
|
|
111
|
+
config_ref[mode_name].to_s.strip
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def warning(message)
|
|
115
|
+
log "Warning: #{message}", level: :warning
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def parse_sxls(value)
|
|
119
|
+
value.split(',').each_with_object({}) do |item, memo|
|
|
120
|
+
parts = item.split(':')
|
|
121
|
+
abort_with_error "Invalid SXLS item #{item.inspect}, expected name:version" unless parts.length == 2
|
|
122
|
+
|
|
123
|
+
name, version = parts
|
|
124
|
+
memo[name] = version
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Helpers
|
|
4
|
+
# Helper methods for testing RSMP alarm behaviour.
|
|
5
|
+
module Alarms
|
|
6
|
+
include Input
|
|
7
|
+
|
|
8
|
+
def with_alarm_activated(site_proxy, alarm_code_id, initial_deactivation: true, &block)
|
|
9
|
+
input, component_id = find_alarm_programming(alarm_code_id)
|
|
10
|
+
component_id ||= RSMP::Validator.get_config('main_component')
|
|
11
|
+
timeout = RSMP::Validator.get_config('timeouts', 'status_update')
|
|
12
|
+
force_input_and_confirm(site_proxy, input:, value: 'False', within: timeout) if initial_deactivation
|
|
13
|
+
run_alarm_lifecycle(site_proxy, alarm_code_id, component_id, input, &block)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def run_alarm_lifecycle(site_proxy, alarm_code_id, component_id, input)
|
|
19
|
+
specialization, alarm_active, alarm_inactive = build_alarm_matchers(site_proxy)
|
|
20
|
+
alarm_timeout = RSMP::Validator.get_config('timeouts', 'alarm')
|
|
21
|
+
status_timeout = RSMP::Validator.get_config('timeouts', 'status_update')
|
|
22
|
+
state = false
|
|
23
|
+
begin
|
|
24
|
+
matcher = { 'cId' => component_id, 'aCId' => alarm_code_id,
|
|
25
|
+
'aSp' => specialization, 'aS' => alarm_active }
|
|
26
|
+
collect_task = start_alarm_collector(site_proxy, matcher, alarm_timeout)
|
|
27
|
+
force_input_and_confirm site_proxy, input:, value: 'True', within: status_timeout
|
|
28
|
+
state = true
|
|
29
|
+
yield collect_task.wait, component_id
|
|
30
|
+
|
|
31
|
+
matcher = { 'cId' => component_id, 'aCId' => alarm_code_id,
|
|
32
|
+
'aSp' => /Issue/i, 'aS' => alarm_inactive }
|
|
33
|
+
collect_task = start_alarm_collector(site_proxy, matcher, alarm_timeout)
|
|
34
|
+
force_input_and_confirm site_proxy, input:, value: 'False', within: status_timeout
|
|
35
|
+
state = false
|
|
36
|
+
[collect_task.wait, component_id]
|
|
37
|
+
ensure
|
|
38
|
+
force_input_and_confirm(site_proxy, input:, value: 'False', within: status_timeout) if state == true
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_alarm_matchers(site_proxy)
|
|
43
|
+
if RSMP::Proxy.version_meets_requirement? site_proxy.core_version, '>=3.2'
|
|
44
|
+
[/Issue/, /Active/, /inActive/]
|
|
45
|
+
else
|
|
46
|
+
[/issue/i, /active/i, /inactive/i]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def start_alarm_collector(site_proxy, matcher, timeout)
|
|
51
|
+
Async::Task.current.async do
|
|
52
|
+
collector = RSMP::AlarmCollector.new(site_proxy, num: 1, matcher: matcher, timeout: timeout)
|
|
53
|
+
collector.collect!
|
|
54
|
+
collector.messages.first
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def find_alarm_programming(alarm_code_id)
|
|
59
|
+
action = RSMP::Validator.config.dig('alarm_triggers', alarm_code_id)
|
|
60
|
+
skip "Alarm trigger #{alarm_code_id} not configured" unless action
|
|
61
|
+
[action['input'], action['component']]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Helpers
|
|
4
|
+
# Helper methods for testing RSMP clock functionality.
|
|
5
|
+
module Clock
|
|
6
|
+
def with_clock_set(site_proxy, clock, within:)
|
|
7
|
+
site_proxy.tlc.set_clock(clock, within:)
|
|
8
|
+
site_proxy.clear_alarm_timestamps
|
|
9
|
+
yield
|
|
10
|
+
ensure
|
|
11
|
+
site_proxy.tlc.set_clock(Time.now.utc, within:)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module Validator
|
|
3
|
+
module Helpers
|
|
4
|
+
# Helpers for connecting to a site or supervisor in tests, with optional
|
|
5
|
+
# version-based skipping via sxl: and core: keyword arguments.
|
|
6
|
+
#
|
|
7
|
+
# with_site(:connected, sxl: '>=1.2') do |supervisor, site_proxy|
|
|
8
|
+
# ...
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# with_supervisor(:connected, core: '>=3.2') do |site, supervisor_proxy|
|
|
12
|
+
# ...
|
|
13
|
+
# end
|
|
14
|
+
module Connection
|
|
15
|
+
VALID_STATES = %i[connected reconnected isolated disconnected].freeze
|
|
16
|
+
|
|
17
|
+
# Wraps an uncaught exception from a test block, preserving the original
|
|
18
|
+
# class name, message, and backtrace for clearer test failure output.
|
|
19
|
+
class UncaughtException < StandardError
|
|
20
|
+
def initialize(original)
|
|
21
|
+
super("#{original.class}: #{original.message}")
|
|
22
|
+
set_backtrace(original.backtrace)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def with_site(state, sxl: nil, core: nil, **opts, &block)
|
|
27
|
+
validate_state!(state)
|
|
28
|
+
check_version_requirements(sxl, core)
|
|
29
|
+
if state == :disconnected
|
|
30
|
+
RSMP::Validator::SiteTester.disconnected { block.call }
|
|
31
|
+
else
|
|
32
|
+
RSMP::Validator::SiteTester.public_send(state, **opts) do |_task, _node, proxy|
|
|
33
|
+
block.call(proxy)
|
|
34
|
+
rescue RSMP::TimeoutError => e
|
|
35
|
+
@__assertions__.assert false, e.message
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
@__assertions__.error!(UncaughtException.new(e))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def with_supervisor(state, sxl: nil, core: nil, **opts, &block)
|
|
43
|
+
validate_state!(state)
|
|
44
|
+
check_version_requirements(sxl, core)
|
|
45
|
+
if state == :disconnected
|
|
46
|
+
RSMP::Validator::SupervisorTester.disconnected { block.call }
|
|
47
|
+
else
|
|
48
|
+
RSMP::Validator::SupervisorTester.public_send(state, **opts) do |_task, _node, proxy|
|
|
49
|
+
block.call(proxy)
|
|
50
|
+
rescue RSMP::TimeoutError => e
|
|
51
|
+
@__assertions__.assert false, e.message
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
@__assertions__.error!(UncaughtException.new(e))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def validate_state!(state)
|
|
61
|
+
return if VALID_STATES.include?(state)
|
|
62
|
+
|
|
63
|
+
raise ArgumentError, "Unknown state #{state.inspect}, must be one of #{VALID_STATES}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def check_version_requirements(sxl, core)
|
|
67
|
+
skip "requires sxl #{sxl}" unless sxl.nil? || RSMP::Validator.sxl_matches?(sxl)
|
|
68
|
+
skip "requires core #{core}" unless core.nil? || RSMP::Validator.core_matches?(core)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|